mirror of
https://github.com/logos-storage/nim-chronos.git
synced 2026-01-09 17:03:11 +00:00
Compare commits
110 Commits
codex-stab
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b55e2816eb | ||
|
|
0646c444fc | ||
|
|
36d8ee5617 | ||
|
|
7c5cbf04a6 | ||
|
|
70cbe346e2 | ||
|
|
03f4328de6 | ||
|
|
9186950e03 | ||
|
|
c04576d829 | ||
|
|
dc3847e4d6 | ||
|
|
8f609b6c17 | ||
|
|
13d28a5b71 | ||
|
|
4ad38079de | ||
|
|
7630f39471 | ||
|
|
c44406594f | ||
|
|
1b9d9253e8 | ||
|
|
8a306763ce | ||
|
|
1ff81c60ea | ||
|
|
52b02b9977 | ||
|
|
72f560f049 | ||
|
|
bb96f02ae8 | ||
|
|
0f0ed1d654 | ||
|
|
d184a92227 | ||
|
|
7a3eaffa4f | ||
|
|
bd7d84fbcb | ||
|
|
e4cb48088c | ||
|
|
0d050d5823 | ||
|
|
8e49df1400 | ||
|
|
2d85229dce | ||
|
|
402914f4cf | ||
|
|
ef1b077adf | ||
|
|
b8b4e1fc47 | ||
|
|
0e806d59ae | ||
|
|
d5bc90fef2 | ||
|
|
035288f3f0 | ||
|
|
d4f1487b0c | ||
|
|
47cc17719f | ||
|
|
17b7a76c7e | ||
|
|
c5a5ece487 | ||
|
|
03d82475d9 | ||
|
|
f6c7ecfa0a | ||
|
|
4ed0cd6be7 | ||
|
|
1eb834a2f9 | ||
|
|
5dfa3fd7fa | ||
|
|
7b02247ce7 | ||
|
|
2e37a6e26c | ||
|
|
be4923be19 | ||
|
|
a81961a3c6 | ||
|
|
8cf2d69aaa | ||
|
|
08db79fe63 | ||
|
|
672db137b7 | ||
|
|
09a0b11719 | ||
|
|
e296ae30c8 | ||
|
|
3ca2c5e6b5 | ||
|
|
1021a7d294 | ||
|
|
92acf68b04 | ||
|
|
b02b9608c3 | ||
|
|
f0a2d4df61 | ||
|
|
e15dc3b41f | ||
|
|
41f77d261e | ||
|
|
1598471ed2 | ||
|
|
c41599a6d6 | ||
|
|
e38ceb5378 | ||
|
|
48b2b08cfb | ||
|
|
28a100b135 | ||
|
|
b18d471629 | ||
|
|
fa0bf405e6 | ||
|
|
f03cdfcc40 | ||
|
|
0b136b33c8 | ||
|
|
1306170255 | ||
|
|
f5ff9e32ca | ||
|
|
24be151cf3 | ||
|
|
9c93ab48de | ||
|
|
0d55475c29 | ||
|
|
f0eb7a0ae9 | ||
|
|
8156e2997a | ||
|
|
9896316599 | ||
|
|
9759f01016 | ||
|
|
c252ce68d8 | ||
|
|
53690f4717 | ||
|
|
5ebd771d35 | ||
|
|
cd6369c048 | ||
|
|
be2edab3ac | ||
|
|
a70b145964 | ||
|
|
8375770fe5 | ||
|
|
12dc36cfee | ||
|
|
f56d286687 | ||
|
|
e3c5a86a14 | ||
|
|
be9eef7a09 | ||
|
|
a759c11ce4 | ||
|
|
253bc3cfc0 | ||
|
|
2e8551b0d9 | ||
|
|
00614476c6 | ||
|
|
db6410f835 | ||
|
|
e706167a53 | ||
|
|
300fbaaf09 | ||
|
|
60e6fc55bf | ||
|
|
a7f708bea8 | ||
|
|
6c2ea67512 | ||
|
|
466241aa95 | ||
|
|
194226a0e0 | ||
|
|
c4b066a2c4 | ||
|
|
38c31e21d3 | ||
|
|
a1eb30360b | ||
|
|
c546a4329c | ||
|
|
6b4f5a1d23 | ||
|
|
5c39bf47be | ||
|
|
d214bcfb4f | ||
|
|
926956bcbe | ||
|
|
53e9f75735 | ||
|
|
f91ac169dc |
@ -1,40 +0,0 @@
|
||||
version: '{build}'
|
||||
|
||||
image: Visual Studio 2015
|
||||
|
||||
cache:
|
||||
- NimBinaries
|
||||
|
||||
matrix:
|
||||
# We always want 32 and 64-bit compilation
|
||||
fast_finish: false
|
||||
|
||||
platform:
|
||||
- x86
|
||||
- x64
|
||||
|
||||
# when multiple CI builds are queued, the tested commit needs to be in the last X commits cloned with "--depth X"
|
||||
clone_depth: 10
|
||||
|
||||
install:
|
||||
# use the newest versions documented here: https://www.appveyor.com/docs/windows-images-software/#mingw-msys-cygwin
|
||||
- IF "%PLATFORM%" == "x86" SET PATH=C:\mingw-w64\i686-6.3.0-posix-dwarf-rt_v5-rev1\mingw32\bin;%PATH%
|
||||
- IF "%PLATFORM%" == "x64" SET PATH=C:\mingw-w64\x86_64-8.1.0-posix-seh-rt_v6-rev0\mingw64\bin;%PATH%
|
||||
|
||||
# build nim from our own branch - this to avoid the day-to-day churn and
|
||||
# regressions of the fast-paced Nim development while maintaining the
|
||||
# flexibility to apply patches
|
||||
- curl -O -L -s -S https://raw.githubusercontent.com/status-im/nimbus-build-system/master/scripts/build_nim.sh
|
||||
- env MAKE="mingw32-make -j2" ARCH_OVERRIDE=%PLATFORM% bash build_nim.sh Nim csources dist/nimble NimBinaries
|
||||
- SET PATH=%CD%\Nim\bin;%PATH%
|
||||
|
||||
build_script:
|
||||
- cd C:\projects\%APPVEYOR_PROJECT_SLUG%
|
||||
- nimble install -y --depsOnly
|
||||
- nimble install -y libbacktrace
|
||||
|
||||
test_script:
|
||||
- nimble test
|
||||
|
||||
deploy: off
|
||||
|
||||
158
.github/workflows/ci.yml
vendored
158
.github/workflows/ci.yml
vendored
@ -6,156 +6,12 @@ on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency: # Cancel stale PR builds (but not push builds)
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- os: linux
|
||||
cpu: amd64
|
||||
- os: linux
|
||||
cpu: i386
|
||||
- os: macos
|
||||
cpu: amd64
|
||||
- os: windows
|
||||
cpu: amd64
|
||||
#- os: windows
|
||||
#cpu: i386
|
||||
branch: [version-1-6, version-2-0, devel]
|
||||
include:
|
||||
- target:
|
||||
os: linux
|
||||
builder: ubuntu-20.04
|
||||
shell: bash
|
||||
- target:
|
||||
os: macos
|
||||
builder: macos-11
|
||||
shell: bash
|
||||
- target:
|
||||
os: windows
|
||||
builder: windows-2019
|
||||
shell: msys2 {0}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: ${{ matrix.shell }}
|
||||
|
||||
name: '${{ matrix.target.os }}-${{ matrix.target.cpu }} (Nim ${{ matrix.branch }})'
|
||||
runs-on: ${{ matrix.builder }}
|
||||
continue-on-error: ${{ matrix.branch == 'devel' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install build dependencies (Linux i386)
|
||||
if: runner.os == 'Linux' && matrix.target.cpu == 'i386'
|
||||
run: |
|
||||
sudo dpkg --add-architecture i386
|
||||
sudo apt-fast update -qq
|
||||
sudo DEBIAN_FRONTEND='noninteractive' apt-fast install \
|
||||
--no-install-recommends -yq gcc-multilib g++-multilib \
|
||||
libssl-dev:i386
|
||||
mkdir -p external/bin
|
||||
cat << EOF > external/bin/gcc
|
||||
#!/bin/bash
|
||||
exec $(which gcc) -m32 "\$@"
|
||||
EOF
|
||||
cat << EOF > external/bin/g++
|
||||
#!/bin/bash
|
||||
exec $(which g++) -m32 "\$@"
|
||||
EOF
|
||||
chmod 755 external/bin/gcc external/bin/g++
|
||||
echo '${{ github.workspace }}/external/bin' >> $GITHUB_PATH
|
||||
|
||||
- name: MSYS2 (Windows i386)
|
||||
if: runner.os == 'Windows' && matrix.target.cpu == 'i386'
|
||||
uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
path-type: inherit
|
||||
msystem: MINGW32
|
||||
install: >-
|
||||
base-devel
|
||||
git
|
||||
mingw-w64-i686-toolchain
|
||||
|
||||
- name: MSYS2 (Windows amd64)
|
||||
if: runner.os == 'Windows' && matrix.target.cpu == 'amd64'
|
||||
uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
path-type: inherit
|
||||
install: >-
|
||||
base-devel
|
||||
git
|
||||
mingw-w64-x86_64-toolchain
|
||||
|
||||
- name: Restore Nim DLLs dependencies (Windows) from cache
|
||||
if: runner.os == 'Windows'
|
||||
id: windows-dlls-cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: external/dlls-${{ matrix.target.cpu }}
|
||||
key: 'dlls-${{ matrix.target.cpu }}'
|
||||
|
||||
- name: Install DLLs dependencies (Windows)
|
||||
if: >
|
||||
steps.windows-dlls-cache.outputs.cache-hit != 'true' &&
|
||||
runner.os == 'Windows'
|
||||
run: |
|
||||
mkdir -p external
|
||||
curl -L "https://nim-lang.org/download/windeps.zip" -o external/windeps.zip
|
||||
7z x -y external/windeps.zip -oexternal/dlls-${{ matrix.target.cpu }}
|
||||
|
||||
- name: Path to cached dependencies (Windows)
|
||||
if: >
|
||||
runner.os == 'Windows'
|
||||
run: |
|
||||
echo "${{ github.workspace }}/external/dlls-${{ matrix.target.cpu }}" >> $GITHUB_PATH
|
||||
|
||||
- name: Derive environment variables
|
||||
run: |
|
||||
if [[ '${{ matrix.target.cpu }}' == 'amd64' ]]; then
|
||||
PLATFORM=x64
|
||||
else
|
||||
PLATFORM=x86
|
||||
fi
|
||||
echo "PLATFORM=$PLATFORM" >> $GITHUB_ENV
|
||||
|
||||
ncpu=
|
||||
MAKE_CMD="make"
|
||||
case '${{ runner.os }}' in
|
||||
'Linux')
|
||||
ncpu=$(nproc)
|
||||
;;
|
||||
'macOS')
|
||||
ncpu=$(sysctl -n hw.ncpu)
|
||||
;;
|
||||
'Windows')
|
||||
ncpu=$NUMBER_OF_PROCESSORS
|
||||
MAKE_CMD="mingw32-make"
|
||||
;;
|
||||
esac
|
||||
[[ -z "$ncpu" || $ncpu -le 0 ]] && ncpu=1
|
||||
echo "ncpu=$ncpu" >> $GITHUB_ENV
|
||||
echo "MAKE_CMD=${MAKE_CMD}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build Nim and Nimble
|
||||
run: |
|
||||
curl -O -L -s -S https://raw.githubusercontent.com/status-im/nimbus-build-system/master/scripts/build_nim.sh
|
||||
env MAKE="${MAKE_CMD} -j${ncpu}" ARCH_OVERRIDE=${PLATFORM} NIM_COMMIT=${{ matrix.branch }} \
|
||||
QUICK_AND_DIRTY_COMPILER=1 QUICK_AND_DIRTY_NIMBLE=1 CC=gcc \
|
||||
bash build_nim.sh nim csources dist/nimble NimBinaries
|
||||
echo '${{ github.workspace }}/nim/bin' >> $GITHUB_PATH
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
nim --version
|
||||
nimble --version
|
||||
nimble install -y --depsOnly
|
||||
nimble install -y libbacktrace
|
||||
nimble test
|
||||
nimble test_libbacktrace
|
||||
uses: status-im/nimbus-common-workflow/.github/workflows/common.yml@main
|
||||
with:
|
||||
test-command: |
|
||||
nimble install -y libbacktrace
|
||||
nimble test
|
||||
nimble test_libbacktrace
|
||||
nimble examples
|
||||
|
||||
60
.github/workflows/doc.yml
vendored
60
.github/workflows/doc.yml
vendored
@ -15,48 +15,44 @@ jobs:
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions-rs/install@v0.1
|
||||
with:
|
||||
crate: mdbook
|
||||
use-tool-cache: true
|
||||
version: "0.4.36"
|
||||
- uses: actions-rs/install@v0.1
|
||||
with:
|
||||
crate: mdbook-toc
|
||||
use-tool-cache: true
|
||||
version: "0.14.1"
|
||||
- uses: actions-rs/install@v0.1
|
||||
with:
|
||||
crate: mdbook-open-on-gh
|
||||
use-tool-cache: true
|
||||
version: "2.4.1"
|
||||
- uses: actions-rs/install@v0.1
|
||||
with:
|
||||
crate: mdbook-admonish
|
||||
use-tool-cache: true
|
||||
version: "1.14.0"
|
||||
|
||||
- uses: jiro4989/setup-nim-action@v1
|
||||
with:
|
||||
nim-version: '1.6.6'
|
||||
nim-version: '1.6.20'
|
||||
|
||||
- name: Generate doc
|
||||
run: |
|
||||
nim --version
|
||||
nimble --version
|
||||
nimble install -dy
|
||||
# nim doc can "fail", but the doc is still generated
|
||||
nim doc --git.url:https://github.com/status-im/nim-chronos --git.commit:master --outdir:docs --project chronos || true
|
||||
nimble docs || true
|
||||
|
||||
# check that the folder exists
|
||||
ls docs
|
||||
|
||||
- name: Clone the gh-pages branch
|
||||
uses: actions/checkout@v2
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
repository: status-im/nim-chronos
|
||||
ref: gh-pages
|
||||
path: subdoc
|
||||
submodules: true
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Commit & push
|
||||
run: |
|
||||
cd subdoc
|
||||
|
||||
# Update / create this branch doc
|
||||
rm -rf docs
|
||||
mv ../docs .
|
||||
|
||||
# Remove .idx files
|
||||
# NOTE: git also uses idx files in his
|
||||
# internal folder, hence the `*` instead of `.`
|
||||
find * -name "*.idx" -delete
|
||||
git add .
|
||||
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
|
||||
git config --global user.name = "${{ github.actor }}"
|
||||
git commit -a -m "update docs"
|
||||
git push origin gh-pages
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs/book
|
||||
force_orphan: true
|
||||
|
||||
27
.travis.yml
27
.travis.yml
@ -1,27 +0,0 @@
|
||||
language: c
|
||||
|
||||
# https://docs.travis-ci.com/user/caching/
|
||||
cache:
|
||||
directories:
|
||||
- NimBinaries
|
||||
|
||||
git:
|
||||
# when multiple CI builds are queued, the tested commit needs to be in the last X commits cloned with "--depth X"
|
||||
depth: 10
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
install:
|
||||
# build nim from our own branch - this to avoid the day-to-day churn and
|
||||
# regressions of the fast-paced Nim development while maintaining the
|
||||
# flexibility to apply patches
|
||||
- curl -O -L -s -S https://raw.githubusercontent.com/status-im/nimbus-build-system/master/scripts/build_nim.sh
|
||||
- env MAKE="make -j2" bash build_nim.sh Nim csources dist/nimble NimBinaries
|
||||
- export PATH="$PWD/Nim/bin:$PATH"
|
||||
|
||||
script:
|
||||
- nimble install -y
|
||||
- nimble test
|
||||
|
||||
340
README.md
340
README.md
@ -1,6 +1,6 @@
|
||||
# Chronos - An efficient library for asynchronous programming
|
||||
|
||||
[](https://github.com/status-im/nim-chronos/actions/workflows/ci.yml)
|
||||
[](https://github.com/status-im/nim-chronos/actions/workflows/ci.yml)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||

|
||||
@ -9,16 +9,16 @@
|
||||
|
||||
Chronos is an efficient [async/await](https://en.wikipedia.org/wiki/Async/await) framework for Nim. Features include:
|
||||
|
||||
* Efficient dispatch pipeline for asynchronous execution
|
||||
* Asynchronous socket and process I/O
|
||||
* HTTP server with SSL/TLS support out of the box (no OpenSSL needed)
|
||||
* Cancellation support
|
||||
* Synchronization primitivies like queues, events and locks
|
||||
* FIFO processing order of dispatch queue
|
||||
* Minimal exception effect support (see [exception effects](#exception-effects))
|
||||
* Cancellation
|
||||
* Efficient dispatch pipeline with excellent multi-platform support
|
||||
* Exceptional error handling features, including `raises` tracking
|
||||
|
||||
## Installation
|
||||
## Getting started
|
||||
|
||||
You can use Nim's official package manager Nimble to install Chronos:
|
||||
Install `chronos` using `nimble`:
|
||||
|
||||
```text
|
||||
nimble install chronos
|
||||
@ -30,6 +30,30 @@ or add a dependency to your `.nimble` file:
|
||||
requires "chronos"
|
||||
```
|
||||
|
||||
and start using it:
|
||||
|
||||
```nim
|
||||
import chronos/apps/http/httpclient
|
||||
|
||||
proc retrievePage(uri: string): Future[string] {.async.} =
|
||||
# Create a new HTTP session
|
||||
let httpSession = HttpSessionRef.new()
|
||||
try:
|
||||
# Fetch page contents
|
||||
let resp = await httpSession.fetch(parseUri(uri))
|
||||
# Convert response to a string, assuming its encoding matches the terminal!
|
||||
bytesToString(resp.data)
|
||||
finally: # Close the session
|
||||
await noCancel(httpSession.closeWait())
|
||||
|
||||
echo waitFor retrievePage(
|
||||
"https://raw.githubusercontent.com/status-im/nim-chronos/master/README.md")
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
See the [user guide](https://status-im.github.io/nim-chronos/).
|
||||
|
||||
## Projects using `chronos`
|
||||
|
||||
* [libp2p](https://github.com/status-im/nim-libp2p) - Peer-to-Peer networking stack implemented in many languages
|
||||
@ -42,305 +66,7 @@ requires "chronos"
|
||||
|
||||
Submit a PR to add yours!
|
||||
|
||||
## Documentation
|
||||
|
||||
### Concepts
|
||||
|
||||
Chronos implements the async/await paradigm in a self-contained library, using
|
||||
macros, with no specific helpers from the compiler.
|
||||
|
||||
Our event loop is called a "dispatcher" and a single instance per thread is
|
||||
created, as soon as one is needed.
|
||||
|
||||
To trigger a dispatcher's processing step, we need to call `poll()` - either
|
||||
directly or through a wrapper like `runForever()` or `waitFor()`. This step
|
||||
handles any file descriptors, timers and callbacks that are ready to be
|
||||
processed.
|
||||
|
||||
`Future` objects encapsulate the result of an async procedure, upon successful
|
||||
completion, and a list of callbacks to be scheduled after any type of
|
||||
completion - be that success, failure or cancellation.
|
||||
|
||||
(These explicit callbacks are rarely used outside Chronos, being replaced by
|
||||
implicit ones generated by async procedure execution and `await` chaining.)
|
||||
|
||||
Async procedures (those using the `{.async.}` pragma) return `Future` objects.
|
||||
|
||||
Inside an async procedure, you can `await` the future returned by another async
|
||||
procedure. At this point, control will be handled to the event loop until that
|
||||
future is completed.
|
||||
|
||||
Future completion is tested with `Future.finished()` and is defined as success,
|
||||
failure or cancellation. This means that a future is either pending or completed.
|
||||
|
||||
To differentiate between completion states, we have `Future.failed()` and
|
||||
`Future.cancelled()`.
|
||||
|
||||
### Dispatcher
|
||||
|
||||
You can run the "dispatcher" event loop forever, with `runForever()` which is defined as:
|
||||
|
||||
```nim
|
||||
proc runForever*() =
|
||||
while true:
|
||||
poll()
|
||||
```
|
||||
|
||||
You can also run it until a certain future is completed, with `waitFor()` which
|
||||
will also call `Future.read()` on it:
|
||||
|
||||
```nim
|
||||
proc p(): Future[int] {.async.} =
|
||||
await sleepAsync(100.milliseconds)
|
||||
return 1
|
||||
|
||||
echo waitFor p() # prints "1"
|
||||
```
|
||||
|
||||
`waitFor()` is defined like this:
|
||||
|
||||
```nim
|
||||
proc waitFor*[T](fut: Future[T]): T =
|
||||
while not(fut.finished()):
|
||||
poll()
|
||||
return fut.read()
|
||||
```
|
||||
|
||||
### Async procedures and methods
|
||||
|
||||
The `{.async.}` pragma will transform a procedure (or a method) returning a
|
||||
specialised `Future` type into a closure iterator. If there is no return type
|
||||
specified, a `Future[void]` is returned.
|
||||
|
||||
```nim
|
||||
proc p() {.async.} =
|
||||
await sleepAsync(100.milliseconds)
|
||||
|
||||
echo p().type # prints "Future[system.void]"
|
||||
```
|
||||
|
||||
Whenever `await` is encountered inside an async procedure, control is passed
|
||||
back to the dispatcher for as many steps as it's necessary for the awaited
|
||||
future to complete successfully, fail or be cancelled. `await` calls the
|
||||
equivalent of `Future.read()` on the completed future and returns the
|
||||
encapsulated value.
|
||||
|
||||
```nim
|
||||
proc p1() {.async.} =
|
||||
await sleepAsync(1.seconds)
|
||||
|
||||
proc p2() {.async.} =
|
||||
await sleepAsync(1.seconds)
|
||||
|
||||
proc p3() {.async.} =
|
||||
let
|
||||
fut1 = p1()
|
||||
fut2 = p2()
|
||||
# Just by executing the async procs, both resulting futures entered the
|
||||
# dispatcher's queue and their "clocks" started ticking.
|
||||
await fut1
|
||||
await fut2
|
||||
# Only one second passed while awaiting them both, not two.
|
||||
|
||||
waitFor p3()
|
||||
```
|
||||
|
||||
Don't let `await`'s behaviour of giving back control to the dispatcher surprise
|
||||
you. If an async procedure modifies global state, and you can't predict when it
|
||||
will start executing, the only way to avoid that state changing underneath your
|
||||
feet, in a certain section, is to not use `await` in it.
|
||||
|
||||
### Error handling
|
||||
|
||||
Exceptions inheriting from `CatchableError` are caught by hidden `try` blocks
|
||||
and placed in the `Future.error` field, changing the future's status to
|
||||
`Failed`.
|
||||
|
||||
When a future is awaited, that exception is re-raised, only to be caught again
|
||||
by a hidden `try` block in the calling async procedure. That's how these
|
||||
exceptions move up the async chain.
|
||||
|
||||
A failed future's callbacks will still be scheduled, but it's not possible to
|
||||
resume execution from the point an exception was raised.
|
||||
|
||||
```nim
|
||||
proc p1() {.async.} =
|
||||
await sleepAsync(1.seconds)
|
||||
raise newException(ValueError, "ValueError inherits from CatchableError")
|
||||
|
||||
proc p2() {.async.} =
|
||||
await sleepAsync(1.seconds)
|
||||
|
||||
proc p3() {.async.} =
|
||||
let
|
||||
fut1 = p1()
|
||||
fut2 = p2()
|
||||
await fut1
|
||||
echo "unreachable code here"
|
||||
await fut2
|
||||
|
||||
# `waitFor()` would call `Future.read()` unconditionally, which would raise the
|
||||
# exception in `Future.error`.
|
||||
let fut3 = p3()
|
||||
while not(fut3.finished()):
|
||||
poll()
|
||||
|
||||
echo "fut3.state = ", fut3.state # "Failed"
|
||||
if fut3.failed():
|
||||
echo "p3() failed: ", fut3.error.name, ": ", fut3.error.msg
|
||||
# prints "p3() failed: ValueError: ValueError inherits from CatchableError"
|
||||
```
|
||||
|
||||
You can put the `await` in a `try` block, to deal with that exception sooner:
|
||||
|
||||
```nim
|
||||
proc p3() {.async.} =
|
||||
let
|
||||
fut1 = p1()
|
||||
fut2 = p2()
|
||||
try:
|
||||
await fut1
|
||||
except CachableError:
|
||||
echo "p1() failed: ", fut1.error.name, ": ", fut1.error.msg
|
||||
echo "reachable code here"
|
||||
await fut2
|
||||
```
|
||||
|
||||
Chronos does not allow that future continuations and other callbacks raise
|
||||
`CatchableError` - as such, calls to `poll` will never raise exceptions caused
|
||||
originating from tasks on the dispatcher queue. It is however possible that
|
||||
`Defect` that happen in tasks bubble up through `poll` as these are not caught
|
||||
by the transformation.
|
||||
|
||||
### Platform independence
|
||||
|
||||
Several functions in `chronos` are backed by the operating system, such as
|
||||
waiting for network events, creating files and sockets etc. The specific
|
||||
exceptions that are raised by the OS is platform-dependent, thus such functions
|
||||
are declared as raising `CatchableError` but will in general raise something
|
||||
more specific. In particular, it's possible that some functions that are
|
||||
annotated as raising `CatchableError` only raise on _some_ platforms - in order
|
||||
to work on all platforms, calling code must assume that they will raise even
|
||||
when they don't seem to do so on one platform.
|
||||
|
||||
### Exception effects
|
||||
|
||||
`chronos` currently offers minimal support for exception effects and `raises`
|
||||
annotations. In general, during the `async` transformation, a generic
|
||||
`except CatchableError` handler is added around the entire function being
|
||||
transformed, in order to catch any exceptions and transfer them to the `Future`.
|
||||
Because of this, the effect system thinks no exceptions are "leaking" because in
|
||||
fact, exception _handling_ is deferred to when the future is being read.
|
||||
|
||||
Effectively, this means that while code can be compiled with
|
||||
`{.push raises: [Defect]}`, the intended effect propagation and checking is
|
||||
**disabled** for `async` functions.
|
||||
|
||||
To enable checking exception effects in `async` code, enable strict mode with
|
||||
`-d:chronosStrictException`.
|
||||
|
||||
In the strict mode, `async` functions are checked such that they only raise
|
||||
`CatchableError` and thus must make sure to explicitly specify exception
|
||||
effects on forward declarations, callbacks and methods using
|
||||
`{.raises: [CatchableError].}` (or more strict) annotations.
|
||||
|
||||
### Cancellation support
|
||||
|
||||
Any running `Future` can be cancelled. This can be used to launch multiple
|
||||
futures, and wait for one of them to finish, and cancel the rest of them,
|
||||
to add timeout, or to let the user cancel a running task.
|
||||
|
||||
```nim
|
||||
# Simple cancellation
|
||||
let future = sleepAsync(10.minutes)
|
||||
future.cancel()
|
||||
|
||||
# Wait for cancellation
|
||||
let future2 = sleepAsync(10.minutes)
|
||||
await future2.cancelAndWait()
|
||||
|
||||
# Race between futures
|
||||
proc retrievePage(uri: string): Future[string] {.async.} =
|
||||
# requires to import uri, chronos/apps/http/httpclient, stew/byteutils
|
||||
let httpSession = HttpSessionRef.new()
|
||||
try:
|
||||
resp = await httpSession.fetch(parseUri(uri))
|
||||
result = string.fromBytes(resp.data)
|
||||
finally:
|
||||
# be sure to always close the session
|
||||
await httpSession.closeWait()
|
||||
|
||||
let
|
||||
futs =
|
||||
@[
|
||||
retrievePage("https://duckduckgo.com/?q=chronos"),
|
||||
retrievePage("https://www.google.fr/search?q=chronos")
|
||||
]
|
||||
|
||||
let finishedFut = await one(futs)
|
||||
for fut in futs:
|
||||
if not fut.finished:
|
||||
fut.cancel()
|
||||
echo "Result: ", await finishedFut
|
||||
```
|
||||
|
||||
When an `await` is cancelled, it will raise a `CancelledError`:
|
||||
```nim
|
||||
proc c1 {.async.} =
|
||||
echo "Before sleep"
|
||||
try:
|
||||
await sleepAsync(10.minutes)
|
||||
echo "After sleep" # not reach due to cancellation
|
||||
except CancelledError as exc:
|
||||
echo "We got cancelled!"
|
||||
raise exc
|
||||
|
||||
proc c2 {.async.} =
|
||||
await c1()
|
||||
echo "Never reached, since the CancelledError got re-raised"
|
||||
|
||||
let work = c2()
|
||||
waitFor(work.cancelAndWait())
|
||||
```
|
||||
|
||||
The `CancelledError` will now travel up the stack like any other exception.
|
||||
It can be caught and handled (for instance, freeing some resources)
|
||||
|
||||
### Multiple async backend support
|
||||
|
||||
Thanks to its powerful macro support, Nim allows `async`/`await` to be
|
||||
implemented in libraries with only minimal support from the language - as such,
|
||||
multiple `async` libraries exist, including `chronos` and `asyncdispatch`, and
|
||||
more may come to be developed in the futures.
|
||||
|
||||
Libraries built on top of `async`/`await` may wish to support multiple async
|
||||
backends - the best way to do so is to create separate modules for each backend
|
||||
that may be imported side-by-side - see [nim-metrics](https://github.com/status-im/nim-metrics/blob/master/metrics/)
|
||||
for an example.
|
||||
|
||||
An alternative way is to select backend using a global compile flag - this
|
||||
method makes it diffucult to compose applications that use both backends as may
|
||||
happen with transitive dependencies, but may be appropriate in some cases -
|
||||
libraries choosing this path should call the flag `asyncBackend`, allowing
|
||||
applications to choose the backend with `-d:asyncBackend=<backend_name>`.
|
||||
|
||||
Known `async` backends include:
|
||||
|
||||
* `chronos` - this library (`-d:asyncBackend=chronos`)
|
||||
* `asyncdispatch` the standard library `asyncdispatch` [module](https://nim-lang.org/docs/asyncdispatch.html) (`-d:asyncBackend=asyncdispatch`)
|
||||
* `none` - ``-d:asyncBackend=none`` - disable ``async`` support completely
|
||||
|
||||
``none`` can be used when a library supports both a synchronous and
|
||||
asynchronous API, to disable the latter.
|
||||
|
||||
### Compile-time configuration
|
||||
|
||||
`chronos` contains several compile-time [configuration options](./chronos/config.nim) enabling stricter compile-time checks and debugging helpers whose runtime cost may be significant.
|
||||
|
||||
Strictness options generally will become default in future chronos releases and allow adapting existing code without changing the new version - see the [`config.nim`](./chronos/config.nim) module for more information.
|
||||
|
||||
## TODO
|
||||
* Pipe/Subprocess Transports.
|
||||
* Multithreading Stream/Datagram servers
|
||||
|
||||
## Contributing
|
||||
@ -349,10 +75,6 @@ When submitting pull requests, please add test cases for any new features or fix
|
||||
|
||||
`chronos` follows the [Status Nim Style Guide](https://status-im.github.io/nim-style-guide/).
|
||||
|
||||
## Other resources
|
||||
|
||||
* [Historical differences with asyncdispatch](https://github.com/status-im/nim-chronos/wiki/AsyncDispatch-comparison)
|
||||
|
||||
## License
|
||||
|
||||
Licensed and distributed under either of
|
||||
|
||||
10
chronos.nim
10
chronos.nim
@ -5,6 +5,10 @@
|
||||
# Licensed under either of
|
||||
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
||||
# MIT license (LICENSE-MIT)
|
||||
import chronos/[asyncloop, asyncsync, handles, transport, timer,
|
||||
asyncproc, debugutils]
|
||||
export asyncloop, asyncsync, handles, transport, timer, asyncproc, debugutils
|
||||
|
||||
## `async`/`await` framework for [Nim](https://nim-lang.org)
|
||||
##
|
||||
## See https://status-im.github.io/nim-chronos/ for documentation
|
||||
|
||||
import chronos/[asyncloop, asyncsync, handles, transport, timer, debugutils]
|
||||
export asyncloop, asyncsync, handles, transport, timer, debugutils
|
||||
|
||||
@ -1,50 +1,85 @@
|
||||
mode = ScriptMode.Verbose
|
||||
|
||||
packageName = "chronos"
|
||||
version = "3.2.0"
|
||||
version = "4.0.4"
|
||||
author = "Status Research & Development GmbH"
|
||||
description = "Networking framework with async/await support"
|
||||
license = "MIT or Apache License 2.0"
|
||||
skipDirs = @["tests"]
|
||||
|
||||
requires "nim >= 1.2.0",
|
||||
requires "nim >= 1.6.16",
|
||||
"results",
|
||||
"stew",
|
||||
"bearssl",
|
||||
"bearssl >= 0.2.5",
|
||||
"httputils",
|
||||
"unittest2"
|
||||
|
||||
import os, strutils
|
||||
|
||||
let nimc = getEnv("NIMC", "nim") # Which nim compiler to use
|
||||
let lang = getEnv("NIMLANG", "c") # Which backend (c/cpp/js)
|
||||
let flags = getEnv("NIMFLAGS", "") # Extra flags for the compiler
|
||||
let verbose = getEnv("V", "") notin ["", "0"]
|
||||
let platform = getEnv("PLATFORM", "")
|
||||
let testArguments =
|
||||
when defined(windows):
|
||||
[
|
||||
"-d:debug -d:chronosDebug -d:useSysAssert -d:useGcAssert",
|
||||
"-d:release",
|
||||
]
|
||||
else:
|
||||
[
|
||||
"-d:debug -d:chronosDebug -d:useSysAssert -d:useGcAssert",
|
||||
"-d:debug -d:chronosDebug -d:chronosEventEngine=poll -d:useSysAssert -d:useGcAssert",
|
||||
"-d:release",
|
||||
]
|
||||
|
||||
let styleCheckStyle = if (NimMajor, NimMinor) < (1, 6): "hint" else: "error"
|
||||
let cfg =
|
||||
" --styleCheck:usages --styleCheck:" & styleCheckStyle &
|
||||
" --styleCheck:usages --styleCheck:error" &
|
||||
(if verbose: "" else: " --verbosity:0 --hints:off") &
|
||||
" --skipParentCfg --skipUserCfg --outdir:build --nimcache:build/nimcache -f"
|
||||
" --skipParentCfg --skipUserCfg --outdir:build " &
|
||||
quoteShell("--nimcache:build/nimcache/$projectName")
|
||||
|
||||
proc build(args, path: string) =
|
||||
exec nimc & " " & lang & " " & cfg & " " & flags & " " & args & " " & path
|
||||
|
||||
proc run(args, path: string) =
|
||||
build args & " -r", path
|
||||
build args, path
|
||||
exec "build/" & path.splitPath[1]
|
||||
|
||||
task examples, "Build examples":
|
||||
# Build book examples
|
||||
for file in listFiles("docs/examples"):
|
||||
if file.endsWith(".nim"):
|
||||
build "--threads:on", file
|
||||
|
||||
task test, "Run all tests":
|
||||
for args in [
|
||||
"-d:debug -d:chronosDebug",
|
||||
"-d:debug -d:chronosPreviewV4",
|
||||
"-d:debug -d:chronosDebug -d:useSysAssert -d:useGcAssert",
|
||||
"-d:release",
|
||||
"-d:release -d:chronosPreviewV4"]:
|
||||
run args, "tests/testall"
|
||||
for args in testArguments:
|
||||
# First run tests with `refc` memory manager.
|
||||
run args & " --mm:refc", "tests/testall"
|
||||
if (NimMajor, NimMinor) > (1, 6):
|
||||
run args & " --mm:refc", "tests/testall"
|
||||
run args & " --mm:orc", "tests/testall"
|
||||
|
||||
task test_v3_compat, "Run all tests in v3 compatibility mode":
|
||||
for args in testArguments:
|
||||
if (NimMajor, NimMinor) > (1, 6):
|
||||
# First run tests with `refc` memory manager.
|
||||
run args & " --mm:refc -d:chronosHandleException", "tests/testall"
|
||||
|
||||
run args & " -d:chronosHandleException", "tests/testall"
|
||||
|
||||
task test_libbacktrace, "test with libbacktrace":
|
||||
var allArgs = @[
|
||||
if platform != "x86":
|
||||
let allArgs = @[
|
||||
"-d:release --debugger:native -d:chronosStackTrace -d:nimStackTraceOverride --import:libbacktrace",
|
||||
]
|
||||
|
||||
for args in allArgs:
|
||||
run args, "tests/testall"
|
||||
for args in allArgs:
|
||||
# First run tests with `refc` memory manager.
|
||||
run args & " --mm:refc", "tests/testall"
|
||||
if (NimMajor, NimMinor) > (1, 6):
|
||||
run args & " --mm:orc", "tests/testall"
|
||||
|
||||
task docs, "Generate API documentation":
|
||||
exec "mdbook build docs"
|
||||
exec nimc & " doc " & "--git.url:https://github.com/status-im/nim-chronos --git.commit:master --outdir:docs/book/api --project chronos"
|
||||
|
||||
@ -6,6 +6,9 @@
|
||||
# Licensed under either of
|
||||
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
||||
# MIT license (LICENSE-MIT)
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import strutils
|
||||
|
||||
const
|
||||
|
||||
@ -6,6 +6,9 @@
|
||||
# Licensed under either of
|
||||
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
||||
# MIT license (LICENSE-MIT)
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import ../../asyncloop, ../../asyncsync
|
||||
import ../../streams/[asyncstream, boundstream]
|
||||
import httpcommon
|
||||
@ -36,17 +39,17 @@ proc newHttpBodyReader*(streams: varargs[AsyncStreamReader]): HttpBodyReader =
|
||||
trackCounter(HttpBodyReaderTrackerName)
|
||||
res
|
||||
|
||||
proc closeWait*(bstream: HttpBodyReader) {.async.} =
|
||||
proc closeWait*(bstream: HttpBodyReader) {.async: (raises: []).} =
|
||||
## Close and free resource allocated by body reader.
|
||||
if bstream.bstate == HttpState.Alive:
|
||||
bstream.bstate = HttpState.Closing
|
||||
var res = newSeq[Future[void]]()
|
||||
var res = newSeq[Future[void].Raising([])]()
|
||||
# We closing streams in reversed order because stream at position [0], uses
|
||||
# data from stream at position [1].
|
||||
for index in countdown((len(bstream.streams) - 1), 0):
|
||||
res.add(bstream.streams[index].closeWait())
|
||||
await allFutures(res)
|
||||
await procCall(closeWait(AsyncStreamReader(bstream)))
|
||||
res.add(procCall(closeWait(AsyncStreamReader(bstream))))
|
||||
await noCancel(allFutures(res))
|
||||
bstream.bstate = HttpState.Closed
|
||||
untrackCounter(HttpBodyReaderTrackerName)
|
||||
|
||||
@ -61,19 +64,19 @@ proc newHttpBodyWriter*(streams: varargs[AsyncStreamWriter]): HttpBodyWriter =
|
||||
trackCounter(HttpBodyWriterTrackerName)
|
||||
res
|
||||
|
||||
proc closeWait*(bstream: HttpBodyWriter) {.async.} =
|
||||
proc closeWait*(bstream: HttpBodyWriter) {.async: (raises: []).} =
|
||||
## Close and free all the resources allocated by body writer.
|
||||
if bstream.bstate == HttpState.Alive:
|
||||
bstream.bstate = HttpState.Closing
|
||||
var res = newSeq[Future[void]]()
|
||||
var res = newSeq[Future[void].Raising([])]()
|
||||
for index in countdown(len(bstream.streams) - 1, 0):
|
||||
res.add(bstream.streams[index].closeWait())
|
||||
await allFutures(res)
|
||||
await noCancel(allFutures(res))
|
||||
await procCall(closeWait(AsyncStreamWriter(bstream)))
|
||||
bstream.bstate = HttpState.Closed
|
||||
untrackCounter(HttpBodyWriterTrackerName)
|
||||
|
||||
proc hasOverflow*(bstream: HttpBodyReader): bool {.raises: [].} =
|
||||
proc hasOverflow*(bstream: HttpBodyReader): bool =
|
||||
if len(bstream.streams) == 1:
|
||||
# If HttpBodyReader has only one stream it has ``BoundedStreamReader``, in
|
||||
# such case its impossible to get more bytes then expected amount.
|
||||
@ -89,6 +92,5 @@ proc hasOverflow*(bstream: HttpBodyReader): bool {.raises: [].} =
|
||||
else:
|
||||
false
|
||||
|
||||
proc closed*(bstream: HttpBodyReader | HttpBodyWriter): bool {.
|
||||
raises: [].} =
|
||||
proc closed*(bstream: HttpBodyReader | HttpBodyWriter): bool =
|
||||
bstream.bstate != HttpState.Alive
|
||||
|
||||
@ -6,14 +6,17 @@
|
||||
# Licensed under either of
|
||||
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
||||
# MIT license (LICENSE-MIT)
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[uri, tables, sequtils]
|
||||
import stew/[results, base10, base64, byteutils], httputils
|
||||
import stew/[base10, base64, byteutils], httputils, results
|
||||
import ../../asyncloop, ../../asyncsync
|
||||
import ../../streams/[asyncstream, tlsstream, chunkstream, boundstream]
|
||||
import httptable, httpcommon, httpagent, httpbodyrw, multipart
|
||||
export results, asyncloop, asyncsync, asyncstream, tlsstream, chunkstream,
|
||||
boundstream, httptable, httpcommon, httpagent, httpbodyrw, multipart,
|
||||
httputils
|
||||
httputils, uri, results
|
||||
export SocketFlags
|
||||
|
||||
const
|
||||
@ -108,6 +111,7 @@ type
|
||||
remoteHostname*: string
|
||||
flags*: set[HttpClientConnectionFlag]
|
||||
timestamp*: Moment
|
||||
duration*: Duration
|
||||
|
||||
HttpClientConnectionRef* = ref HttpClientConnection
|
||||
|
||||
@ -119,12 +123,13 @@ type
|
||||
headersTimeout*: Duration
|
||||
idleTimeout: Duration
|
||||
idlePeriod: Duration
|
||||
watcherFut: Future[void]
|
||||
watcherFut: Future[void].Raising([])
|
||||
connectionBufferSize*: int
|
||||
maxConnections*: int
|
||||
connectionsCount*: int
|
||||
socketFlags*: set[SocketFlags]
|
||||
flags*: HttpClientFlags
|
||||
dualstack*: DualStackType
|
||||
|
||||
HttpAddress* = object
|
||||
id*: string
|
||||
@ -154,6 +159,7 @@ type
|
||||
redirectCount: int
|
||||
timestamp*: Moment
|
||||
duration*: Duration
|
||||
headersBuffer: seq[byte]
|
||||
|
||||
HttpClientRequestRef* = ref HttpClientRequest
|
||||
|
||||
@ -194,6 +200,8 @@ type
|
||||
name*: string
|
||||
data*: string
|
||||
|
||||
HttpAddressResult* = Result[HttpAddress, HttpAddressErrorType]
|
||||
|
||||
# HttpClientRequestRef valid states are:
|
||||
# Ready -> Open -> (Finished, Error) -> (Closing, Closed)
|
||||
#
|
||||
@ -233,6 +241,12 @@ template setDuration(
|
||||
reqresp.duration = timestamp - reqresp.timestamp
|
||||
reqresp.connection.setTimestamp(timestamp)
|
||||
|
||||
template setDuration(conn: HttpClientConnectionRef): untyped =
|
||||
if not(isNil(conn)):
|
||||
let timestamp = Moment.now()
|
||||
conn.duration = timestamp - conn.timestamp
|
||||
conn.setTimestamp(timestamp)
|
||||
|
||||
template isReady(conn: HttpClientConnectionRef): bool =
|
||||
(conn.state == HttpClientConnectionState.Ready) and
|
||||
(HttpClientConnectionFlag.KeepAlive in conn.flags) and
|
||||
@ -243,7 +257,7 @@ template isIdle(conn: HttpClientConnectionRef, timestamp: Moment,
|
||||
timeout: Duration): bool =
|
||||
(timestamp - conn.timestamp) >= timeout
|
||||
|
||||
proc sessionWatcher(session: HttpSessionRef) {.async.}
|
||||
proc sessionWatcher(session: HttpSessionRef) {.async: (raises: []).}
|
||||
|
||||
proc new*(t: typedesc[HttpSessionRef],
|
||||
flags: HttpClientFlags = {},
|
||||
@ -254,8 +268,8 @@ proc new*(t: typedesc[HttpSessionRef],
|
||||
maxConnections = -1,
|
||||
idleTimeout = HttpConnectionIdleTimeout,
|
||||
idlePeriod = HttpConnectionCheckPeriod,
|
||||
socketFlags: set[SocketFlags] = {}): HttpSessionRef {.
|
||||
raises: [] .} =
|
||||
socketFlags: set[SocketFlags] = {},
|
||||
dualstack = DualStackType.Auto): HttpSessionRef =
|
||||
## Create new HTTP session object.
|
||||
##
|
||||
## ``maxRedirections`` - maximum number of HTTP 3xx redirections
|
||||
@ -274,16 +288,17 @@ proc new*(t: typedesc[HttpSessionRef],
|
||||
idleTimeout: idleTimeout,
|
||||
idlePeriod: idlePeriod,
|
||||
connections: initTable[string, seq[HttpClientConnectionRef]](),
|
||||
socketFlags: socketFlags
|
||||
socketFlags: socketFlags,
|
||||
dualstack: dualstack
|
||||
)
|
||||
res.watcherFut =
|
||||
if HttpClientFlag.Http11Pipeline in flags:
|
||||
sessionWatcher(res)
|
||||
else:
|
||||
newFuture[void]("session.watcher.placeholder")
|
||||
nil
|
||||
res
|
||||
|
||||
proc getTLSFlags(flags: HttpClientFlags): set[TLSFlags] {.raises: [] .} =
|
||||
proc getTLSFlags(flags: HttpClientFlags): set[TLSFlags] =
|
||||
var res: set[TLSFlags]
|
||||
if HttpClientFlag.NoVerifyHost in flags:
|
||||
res.incl(TLSFlags.NoVerifyHost)
|
||||
@ -291,8 +306,90 @@ proc getTLSFlags(flags: HttpClientFlags): set[TLSFlags] {.raises: [] .} =
|
||||
res.incl(TLSFlags.NoVerifyServerName)
|
||||
res
|
||||
|
||||
proc getAddress*(session: HttpSessionRef, url: Uri): HttpResult[HttpAddress] {.
|
||||
raises: [] .} =
|
||||
proc getHttpAddress*(
|
||||
url: Uri,
|
||||
flags: HttpClientFlags = {}
|
||||
): HttpAddressResult =
|
||||
let
|
||||
scheme =
|
||||
if len(url.scheme) == 0:
|
||||
HttpClientScheme.NonSecure
|
||||
else:
|
||||
case toLowerAscii(url.scheme)
|
||||
of "http":
|
||||
HttpClientScheme.NonSecure
|
||||
of "https":
|
||||
HttpClientScheme.Secure
|
||||
else:
|
||||
return err(HttpAddressErrorType.InvalidUrlScheme)
|
||||
port =
|
||||
if len(url.port) == 0:
|
||||
case scheme
|
||||
of HttpClientScheme.NonSecure:
|
||||
80'u16
|
||||
of HttpClientScheme.Secure:
|
||||
443'u16
|
||||
else:
|
||||
Base10.decode(uint16, url.port).valueOr:
|
||||
return err(HttpAddressErrorType.InvalidPortNumber)
|
||||
hostname =
|
||||
block:
|
||||
if len(url.hostname) == 0:
|
||||
return err(HttpAddressErrorType.MissingHostname)
|
||||
url.hostname
|
||||
id = hostname & ":" & Base10.toString(port)
|
||||
addresses =
|
||||
if (HttpClientFlag.NoInet4Resolution in flags) and
|
||||
(HttpClientFlag.NoInet6Resolution in flags):
|
||||
# DNS resolution is disabled.
|
||||
try:
|
||||
@[initTAddress(hostname, Port(port))]
|
||||
except TransportAddressError:
|
||||
return err(HttpAddressErrorType.InvalidIpHostname)
|
||||
else:
|
||||
try:
|
||||
if (HttpClientFlag.NoInet4Resolution notin flags) and
|
||||
(HttpClientFlag.NoInet6Resolution notin flags):
|
||||
# DNS resolution for both IPv4 and IPv6 addresses.
|
||||
resolveTAddress(hostname, Port(port))
|
||||
else:
|
||||
if HttpClientFlag.NoInet6Resolution in flags:
|
||||
# DNS resolution only for IPv4 addresses.
|
||||
resolveTAddress(hostname, Port(port), AddressFamily.IPv4)
|
||||
else:
|
||||
# DNS resolution only for IPv6 addresses
|
||||
resolveTAddress(hostname, Port(port), AddressFamily.IPv6)
|
||||
except TransportAddressError:
|
||||
return err(HttpAddressErrorType.NameLookupFailed)
|
||||
|
||||
if len(addresses) == 0:
|
||||
return err(HttpAddressErrorType.NoAddressResolved)
|
||||
|
||||
ok(HttpAddress(id: id, scheme: scheme, hostname: hostname, port: port,
|
||||
path: url.path, query: url.query, anchor: url.anchor,
|
||||
username: url.username, password: url.password,
|
||||
addresses: addresses))
|
||||
|
||||
proc getHttpAddress*(
|
||||
url: string,
|
||||
flags: HttpClientFlags = {}
|
||||
): HttpAddressResult =
|
||||
getHttpAddress(parseUri(url), flags)
|
||||
|
||||
proc getHttpAddress*(
|
||||
session: HttpSessionRef,
|
||||
url: Uri
|
||||
): HttpAddressResult =
|
||||
getHttpAddress(url, session.flags)
|
||||
|
||||
proc getHttpAddress*(
|
||||
session: HttpSessionRef,
|
||||
url: string
|
||||
): HttpAddressResult =
|
||||
## Create new HTTP address using URL string ``url`` and .
|
||||
getHttpAddress(parseUri(url), session.flags)
|
||||
|
||||
proc getAddress*(session: HttpSessionRef, url: Uri): HttpResult[HttpAddress] =
|
||||
let scheme =
|
||||
if len(url.scheme) == 0:
|
||||
HttpClientScheme.NonSecure
|
||||
@ -356,13 +453,13 @@ proc getAddress*(session: HttpSessionRef, url: Uri): HttpResult[HttpAddress] {.
|
||||
addresses: addresses))
|
||||
|
||||
proc getAddress*(session: HttpSessionRef,
|
||||
url: string): HttpResult[HttpAddress] {.raises: [].} =
|
||||
url: string): HttpResult[HttpAddress] =
|
||||
## Create new HTTP address using URL string ``url`` and .
|
||||
session.getAddress(parseUri(url))
|
||||
|
||||
proc getAddress*(address: TransportAddress,
|
||||
ctype: HttpClientScheme = HttpClientScheme.NonSecure,
|
||||
queryString: string = "/"): HttpAddress {.raises: [].} =
|
||||
queryString: string = "/"): HttpAddress =
|
||||
## Create new HTTP address using Transport address ``address``, connection
|
||||
## type ``ctype`` and query string ``queryString``.
|
||||
let uri = parseUri(queryString)
|
||||
@ -445,8 +542,12 @@ proc getUniqueConnectionId(session: HttpSessionRef): uint64 =
|
||||
inc(session.counter)
|
||||
session.counter
|
||||
|
||||
proc new(t: typedesc[HttpClientConnectionRef], session: HttpSessionRef,
|
||||
ha: HttpAddress, transp: StreamTransport): HttpClientConnectionRef =
|
||||
proc new(
|
||||
t: typedesc[HttpClientConnectionRef],
|
||||
session: HttpSessionRef,
|
||||
ha: HttpAddress,
|
||||
transp: StreamTransport
|
||||
): Result[HttpClientConnectionRef, string] =
|
||||
case ha.scheme
|
||||
of HttpClientScheme.NonSecure:
|
||||
let res = HttpClientConnectionRef(
|
||||
@ -459,108 +560,123 @@ proc new(t: typedesc[HttpClientConnectionRef], session: HttpSessionRef,
|
||||
remoteHostname: ha.id
|
||||
)
|
||||
trackCounter(HttpClientConnectionTrackerName)
|
||||
res
|
||||
ok(res)
|
||||
of HttpClientScheme.Secure:
|
||||
let treader = newAsyncStreamReader(transp)
|
||||
let twriter = newAsyncStreamWriter(transp)
|
||||
let tls = newTLSClientAsyncStream(treader, twriter, ha.hostname,
|
||||
flags = session.flags.getTLSFlags())
|
||||
let res = HttpClientConnectionRef(
|
||||
id: session.getUniqueConnectionId(),
|
||||
kind: HttpClientScheme.Secure,
|
||||
transp: transp,
|
||||
treader: treader,
|
||||
twriter: twriter,
|
||||
reader: tls.reader,
|
||||
writer: tls.writer,
|
||||
tls: tls,
|
||||
state: HttpClientConnectionState.Connecting,
|
||||
remoteHostname: ha.id
|
||||
)
|
||||
trackCounter(HttpClientConnectionTrackerName)
|
||||
res
|
||||
let
|
||||
treader = newAsyncStreamReader(transp)
|
||||
twriter = newAsyncStreamWriter(transp)
|
||||
tls =
|
||||
try:
|
||||
newTLSClientAsyncStream(treader, twriter, ha.hostname,
|
||||
flags = session.flags.getTLSFlags(),
|
||||
bufferSize = session.connectionBufferSize)
|
||||
except TLSStreamInitError as exc:
|
||||
return err(exc.msg)
|
||||
|
||||
proc setError(request: HttpClientRequestRef, error: ref HttpError) {.
|
||||
raises: [] .} =
|
||||
res = HttpClientConnectionRef(
|
||||
id: session.getUniqueConnectionId(),
|
||||
kind: HttpClientScheme.Secure,
|
||||
transp: transp,
|
||||
treader: treader,
|
||||
twriter: twriter,
|
||||
reader: tls.reader,
|
||||
writer: tls.writer,
|
||||
tls: tls,
|
||||
state: HttpClientConnectionState.Connecting,
|
||||
remoteHostname: ha.id
|
||||
)
|
||||
trackCounter(HttpClientConnectionTrackerName)
|
||||
ok(res)
|
||||
|
||||
proc setError(request: HttpClientRequestRef, error: ref HttpError) =
|
||||
request.error = error
|
||||
request.state = HttpReqRespState.Error
|
||||
if not(isNil(request.connection)):
|
||||
request.connection.state = HttpClientConnectionState.Error
|
||||
request.connection.error = error
|
||||
|
||||
proc setError(response: HttpClientResponseRef, error: ref HttpError) {.
|
||||
raises: [] .} =
|
||||
proc setError(response: HttpClientResponseRef, error: ref HttpError) =
|
||||
response.error = error
|
||||
response.state = HttpReqRespState.Error
|
||||
if not(isNil(response.connection)):
|
||||
response.connection.state = HttpClientConnectionState.Error
|
||||
response.connection.error = error
|
||||
|
||||
proc closeWait(conn: HttpClientConnectionRef) {.async.} =
|
||||
proc closeWait(conn: HttpClientConnectionRef) {.async: (raises: []).} =
|
||||
## Close HttpClientConnectionRef instance ``conn`` and free all the resources.
|
||||
if conn.state notin {HttpClientConnectionState.Closing,
|
||||
HttpClientConnectionState.Closed}:
|
||||
conn.state = HttpClientConnectionState.Closing
|
||||
let pending =
|
||||
block:
|
||||
var res: seq[Future[void]]
|
||||
var res: seq[Future[void].Raising([])]
|
||||
if not(isNil(conn.reader)) and not(conn.reader.closed()):
|
||||
res.add(conn.reader.closeWait())
|
||||
if not(isNil(conn.writer)) and not(conn.writer.closed()):
|
||||
res.add(conn.writer.closeWait())
|
||||
if conn.kind == HttpClientScheme.Secure:
|
||||
res.add(conn.treader.closeWait())
|
||||
res.add(conn.twriter.closeWait())
|
||||
res.add(conn.transp.closeWait())
|
||||
res
|
||||
if len(pending) > 0: await allFutures(pending)
|
||||
case conn.kind
|
||||
of HttpClientScheme.Secure:
|
||||
await allFutures(conn.treader.closeWait(), conn.twriter.closeWait())
|
||||
of HttpClientScheme.NonSecure:
|
||||
discard
|
||||
await conn.transp.closeWait()
|
||||
if len(pending) > 0: await noCancel(allFutures(pending))
|
||||
conn.state = HttpClientConnectionState.Closed
|
||||
untrackCounter(HttpClientConnectionTrackerName)
|
||||
|
||||
proc connect(session: HttpSessionRef,
|
||||
ha: HttpAddress): Future[HttpClientConnectionRef] {.async.} =
|
||||
ha: HttpAddress): Future[HttpClientConnectionRef] {.
|
||||
async: (raises: [CancelledError, HttpConnectionError]).} =
|
||||
## Establish new connection with remote server using ``url`` and ``flags``.
|
||||
## On success returns ``HttpClientConnectionRef`` object.
|
||||
|
||||
var lastError = ""
|
||||
# Here we trying to connect to every possible remote host address we got after
|
||||
# DNS resolution.
|
||||
for address in ha.addresses:
|
||||
let transp =
|
||||
try:
|
||||
await connect(address, bufferSize = session.connectionBufferSize,
|
||||
flags = session.socketFlags)
|
||||
flags = session.socketFlags,
|
||||
dualstack = session.dualstack)
|
||||
except CancelledError as exc:
|
||||
raise exc
|
||||
except CatchableError:
|
||||
except TransportError:
|
||||
nil
|
||||
if not(isNil(transp)):
|
||||
let conn =
|
||||
block:
|
||||
let res = HttpClientConnectionRef.new(session, ha, transp)
|
||||
case res.kind
|
||||
of HttpClientScheme.Secure:
|
||||
let res = HttpClientConnectionRef.new(session, ha, transp).valueOr:
|
||||
raiseHttpConnectionError(
|
||||
"Could not connect to remote host, reason: " & error)
|
||||
if res.kind == HttpClientScheme.Secure:
|
||||
try:
|
||||
await res.tls.handshake()
|
||||
res.state = HttpClientConnectionState.Ready
|
||||
except CancelledError as exc:
|
||||
await res.closeWait()
|
||||
raise exc
|
||||
except AsyncStreamError:
|
||||
except TLSStreamProtocolError as exc:
|
||||
await res.closeWait()
|
||||
res.state = HttpClientConnectionState.Error
|
||||
of HttpClientScheme.Nonsecure:
|
||||
lastError = $exc.msg
|
||||
except AsyncStreamError as exc:
|
||||
await res.closeWait()
|
||||
res.state = HttpClientConnectionState.Error
|
||||
lastError = $exc.msg
|
||||
else:
|
||||
res.state = HttpClientConnectionState.Ready
|
||||
res
|
||||
if conn.state == HttpClientConnectionState.Ready:
|
||||
return conn
|
||||
|
||||
# If all attempts to connect to the remote host have failed.
|
||||
raiseHttpConnectionError("Could not connect to remote host")
|
||||
if len(lastError) > 0:
|
||||
raiseHttpConnectionError("Could not connect to remote host, reason: " &
|
||||
lastError)
|
||||
else:
|
||||
raiseHttpConnectionError("Could not connect to remote host")
|
||||
|
||||
proc removeConnection(session: HttpSessionRef,
|
||||
conn: HttpClientConnectionRef) {.async.} =
|
||||
conn: HttpClientConnectionRef) {.async: (raises: []).} =
|
||||
let removeHost =
|
||||
block:
|
||||
var res = false
|
||||
@ -584,12 +700,13 @@ proc acquireConnection(
|
||||
session: HttpSessionRef,
|
||||
ha: HttpAddress,
|
||||
flags: set[HttpClientRequestFlag]
|
||||
): Future[HttpClientConnectionRef] {.async.} =
|
||||
): Future[HttpClientConnectionRef] {.
|
||||
async: (raises: [CancelledError, HttpConnectionError]).} =
|
||||
## Obtain connection from ``session`` or establish a new one.
|
||||
var default: seq[HttpClientConnectionRef]
|
||||
let timestamp = Moment.now()
|
||||
if session.connectionPoolEnabled(flags):
|
||||
# Trying to reuse existing connection from our connection's pool.
|
||||
let timestamp = Moment.now()
|
||||
# We looking for non-idle connection at `Ready` state, all idle connections
|
||||
# will be freed by sessionWatcher().
|
||||
for connection in session.connections.getOrDefault(ha.id):
|
||||
@ -606,10 +723,13 @@ proc acquireConnection(
|
||||
connection.state = HttpClientConnectionState.Acquired
|
||||
session.connections.mgetOrPut(ha.id, default).add(connection)
|
||||
inc(session.connectionsCount)
|
||||
return connection
|
||||
connection.setTimestamp(timestamp)
|
||||
connection.setDuration()
|
||||
connection
|
||||
|
||||
proc releaseConnection(session: HttpSessionRef,
|
||||
connection: HttpClientConnectionRef) {.async.} =
|
||||
connection: HttpClientConnectionRef) {.
|
||||
async: (raises: []).} =
|
||||
## Return connection back to the ``session``.
|
||||
let removeConnection =
|
||||
if HttpClientFlag.Http11Pipeline notin session.flags:
|
||||
@ -647,7 +767,7 @@ proc releaseConnection(session: HttpSessionRef,
|
||||
HttpClientConnectionFlag.Response,
|
||||
HttpClientConnectionFlag.NoBody})
|
||||
|
||||
proc releaseConnection(request: HttpClientRequestRef) {.async.} =
|
||||
proc releaseConnection(request: HttpClientRequestRef) {.async: (raises: []).} =
|
||||
let
|
||||
session = request.session
|
||||
connection = request.connection
|
||||
@ -659,7 +779,8 @@ proc releaseConnection(request: HttpClientRequestRef) {.async.} =
|
||||
if HttpClientConnectionFlag.Response notin connection.flags:
|
||||
await session.releaseConnection(connection)
|
||||
|
||||
proc releaseConnection(response: HttpClientResponseRef) {.async.} =
|
||||
proc releaseConnection(response: HttpClientResponseRef) {.
|
||||
async: (raises: []).} =
|
||||
let
|
||||
session = response.session
|
||||
connection = response.connection
|
||||
@ -671,7 +792,7 @@ proc releaseConnection(response: HttpClientResponseRef) {.async.} =
|
||||
if HttpClientConnectionFlag.Request notin connection.flags:
|
||||
await session.releaseConnection(connection)
|
||||
|
||||
proc closeWait*(session: HttpSessionRef) {.async.} =
|
||||
proc closeWait*(session: HttpSessionRef) {.async: (raises: []).} =
|
||||
## Closes HTTP session object.
|
||||
##
|
||||
## This closes all the connections opened to remote servers.
|
||||
@ -682,9 +803,9 @@ proc closeWait*(session: HttpSessionRef) {.async.} =
|
||||
for connections in session.connections.values():
|
||||
for conn in connections:
|
||||
pending.add(closeWait(conn))
|
||||
await allFutures(pending)
|
||||
await noCancel(allFutures(pending))
|
||||
|
||||
proc sessionWatcher(session: HttpSessionRef) {.async.} =
|
||||
proc sessionWatcher(session: HttpSessionRef) {.async: (raises: []).} =
|
||||
while true:
|
||||
let firstBreak =
|
||||
try:
|
||||
@ -715,45 +836,53 @@ proc sessionWatcher(session: HttpSessionRef) {.async.} =
|
||||
var pending: seq[Future[void]]
|
||||
let secondBreak =
|
||||
try:
|
||||
pending = idleConnections.mapIt(it.closeWait())
|
||||
for conn in idleConnections:
|
||||
pending.add(conn.closeWait())
|
||||
await allFutures(pending)
|
||||
false
|
||||
except CancelledError:
|
||||
# We still want to close connections to avoid socket leaks.
|
||||
await allFutures(pending)
|
||||
await noCancel(allFutures(pending))
|
||||
true
|
||||
|
||||
if secondBreak:
|
||||
break
|
||||
|
||||
proc closeWait*(request: HttpClientRequestRef) {.async.} =
|
||||
proc closeWait*(request: HttpClientRequestRef) {.async: (raises: []).} =
|
||||
var pending: seq[Future[void].Raising([])]
|
||||
if request.state notin {HttpReqRespState.Closing, HttpReqRespState.Closed}:
|
||||
request.state = HttpReqRespState.Closing
|
||||
if not(isNil(request.writer)):
|
||||
if not(request.writer.closed()):
|
||||
await request.writer.closeWait()
|
||||
pending.add(request.writer.closeWait())
|
||||
request.writer = nil
|
||||
await request.releaseConnection()
|
||||
pending.add(request.releaseConnection())
|
||||
await noCancel(allFutures(pending))
|
||||
request.session = nil
|
||||
request.error = nil
|
||||
request.headersBuffer.reset()
|
||||
request.state = HttpReqRespState.Closed
|
||||
untrackCounter(HttpClientRequestTrackerName)
|
||||
|
||||
proc closeWait*(response: HttpClientResponseRef) {.async.} =
|
||||
proc closeWait*(response: HttpClientResponseRef) {.async: (raises: []).} =
|
||||
var pending: seq[Future[void].Raising([])]
|
||||
if response.state notin {HttpReqRespState.Closing, HttpReqRespState.Closed}:
|
||||
response.state = HttpReqRespState.Closing
|
||||
if not(isNil(response.reader)):
|
||||
if not(response.reader.closed()):
|
||||
await response.reader.closeWait()
|
||||
pending.add(response.reader.closeWait())
|
||||
response.reader = nil
|
||||
await response.releaseConnection()
|
||||
pending.add(response.releaseConnection())
|
||||
await noCancel(allFutures(pending))
|
||||
response.session = nil
|
||||
response.error = nil
|
||||
response.state = HttpReqRespState.Closed
|
||||
untrackCounter(HttpClientResponseTrackerName)
|
||||
|
||||
proc prepareResponse(request: HttpClientRequestRef, data: openArray[byte]
|
||||
): HttpResult[HttpClientResponseRef] {.raises: [] .} =
|
||||
proc prepareResponse(
|
||||
request: HttpClientRequestRef,
|
||||
data: openArray[byte]
|
||||
): HttpResult[HttpClientResponseRef] =
|
||||
## Process response headers.
|
||||
let resp = parseResponse(data, false)
|
||||
if resp.failed():
|
||||
@ -864,39 +993,41 @@ proc prepareResponse(request: HttpClientRequestRef, data: openArray[byte]
|
||||
ok(res)
|
||||
|
||||
proc getResponse(req: HttpClientRequestRef): Future[HttpClientResponseRef] {.
|
||||
async.} =
|
||||
var buffer: array[HttpMaxHeadersSize, byte]
|
||||
async: (raises: [CancelledError, HttpError]).} =
|
||||
let timestamp = Moment.now()
|
||||
req.connection.setTimestamp(timestamp)
|
||||
let
|
||||
bytesRead =
|
||||
try:
|
||||
await req.connection.reader.readUntil(addr buffer[0],
|
||||
len(buffer), HeadersMark).wait(
|
||||
await req.connection.reader.readUntil(addr req.headersBuffer[0],
|
||||
len(req.headersBuffer),
|
||||
HeadersMark).wait(
|
||||
req.session.headersTimeout)
|
||||
except AsyncTimeoutError:
|
||||
raiseHttpReadError("Reading response headers timed out")
|
||||
except AsyncStreamError:
|
||||
raiseHttpReadError("Could not read response headers")
|
||||
except AsyncStreamError as exc:
|
||||
raiseHttpReadError(
|
||||
"Could not read response headers, reason: " & $exc.msg)
|
||||
|
||||
let response = prepareResponse(req, buffer.toOpenArray(0, bytesRead - 1))
|
||||
if response.isErr():
|
||||
raiseHttpProtocolError(response.error())
|
||||
let res = response.get()
|
||||
res.setTimestamp(timestamp)
|
||||
return res
|
||||
let response =
|
||||
prepareResponse(req,
|
||||
req.headersBuffer.toOpenArray(0, bytesRead - 1)).valueOr:
|
||||
raiseHttpProtocolError(error)
|
||||
response.setTimestamp(timestamp)
|
||||
response
|
||||
|
||||
proc new*(t: typedesc[HttpClientRequestRef], session: HttpSessionRef,
|
||||
ha: HttpAddress, meth: HttpMethod = MethodGet,
|
||||
version: HttpVersion = HttpVersion11,
|
||||
flags: set[HttpClientRequestFlag] = {},
|
||||
maxResponseHeadersSize: int = HttpMaxHeadersSize,
|
||||
headers: openArray[HttpHeaderTuple] = [],
|
||||
body: openArray[byte] = []): HttpClientRequestRef {.
|
||||
raises: [].} =
|
||||
body: openArray[byte] = []): HttpClientRequestRef =
|
||||
let res = HttpClientRequestRef(
|
||||
state: HttpReqRespState.Ready, session: session, meth: meth,
|
||||
version: version, flags: flags, headers: HttpTable.init(headers),
|
||||
address: ha, bodyFlag: HttpClientBodyFlag.Custom, buffer: @body
|
||||
address: ha, bodyFlag: HttpClientBodyFlag.Custom, buffer: @body,
|
||||
headersBuffer: newSeq[byte](max(maxResponseHeadersSize, HttpMaxHeadersSize))
|
||||
)
|
||||
trackCounter(HttpClientRequestTrackerName)
|
||||
res
|
||||
@ -905,14 +1036,15 @@ proc new*(t: typedesc[HttpClientRequestRef], session: HttpSessionRef,
|
||||
url: string, meth: HttpMethod = MethodGet,
|
||||
version: HttpVersion = HttpVersion11,
|
||||
flags: set[HttpClientRequestFlag] = {},
|
||||
maxResponseHeadersSize: int = HttpMaxHeadersSize,
|
||||
headers: openArray[HttpHeaderTuple] = [],
|
||||
body: openArray[byte] = []): HttpResult[HttpClientRequestRef] {.
|
||||
raises: [].} =
|
||||
body: openArray[byte] = []): HttpResult[HttpClientRequestRef] =
|
||||
let address = ? session.getAddress(parseUri(url))
|
||||
let res = HttpClientRequestRef(
|
||||
state: HttpReqRespState.Ready, session: session, meth: meth,
|
||||
version: version, flags: flags, headers: HttpTable.init(headers),
|
||||
address: address, bodyFlag: HttpClientBodyFlag.Custom, buffer: @body
|
||||
address: address, bodyFlag: HttpClientBodyFlag.Custom, buffer: @body,
|
||||
headersBuffer: newSeq[byte](max(maxResponseHeadersSize, HttpMaxHeadersSize))
|
||||
)
|
||||
trackCounter(HttpClientRequestTrackerName)
|
||||
ok(res)
|
||||
@ -920,55 +1052,61 @@ proc new*(t: typedesc[HttpClientRequestRef], session: HttpSessionRef,
|
||||
proc get*(t: typedesc[HttpClientRequestRef], session: HttpSessionRef,
|
||||
url: string, version: HttpVersion = HttpVersion11,
|
||||
flags: set[HttpClientRequestFlag] = {},
|
||||
maxResponseHeadersSize: int = HttpMaxHeadersSize,
|
||||
headers: openArray[HttpHeaderTuple] = []
|
||||
): HttpResult[HttpClientRequestRef] {.raises: [].} =
|
||||
HttpClientRequestRef.new(session, url, MethodGet, version, flags, headers)
|
||||
): HttpResult[HttpClientRequestRef] =
|
||||
HttpClientRequestRef.new(session, url, MethodGet, version, flags,
|
||||
maxResponseHeadersSize, headers)
|
||||
|
||||
proc get*(t: typedesc[HttpClientRequestRef], session: HttpSessionRef,
|
||||
ha: HttpAddress, version: HttpVersion = HttpVersion11,
|
||||
flags: set[HttpClientRequestFlag] = {},
|
||||
maxResponseHeadersSize: int = HttpMaxHeadersSize,
|
||||
headers: openArray[HttpHeaderTuple] = []
|
||||
): HttpClientRequestRef {.raises: [].} =
|
||||
HttpClientRequestRef.new(session, ha, MethodGet, version, flags, headers)
|
||||
): HttpClientRequestRef =
|
||||
HttpClientRequestRef.new(session, ha, MethodGet, version, flags,
|
||||
maxResponseHeadersSize, headers)
|
||||
|
||||
proc post*(t: typedesc[HttpClientRequestRef], session: HttpSessionRef,
|
||||
url: string, version: HttpVersion = HttpVersion11,
|
||||
flags: set[HttpClientRequestFlag] = {},
|
||||
maxResponseHeadersSize: int = HttpMaxHeadersSize,
|
||||
headers: openArray[HttpHeaderTuple] = [],
|
||||
body: openArray[byte] = []
|
||||
): HttpResult[HttpClientRequestRef] {.raises: [].} =
|
||||
HttpClientRequestRef.new(session, url, MethodPost, version, flags, headers,
|
||||
body)
|
||||
): HttpResult[HttpClientRequestRef] =
|
||||
HttpClientRequestRef.new(session, url, MethodPost, version, flags,
|
||||
maxResponseHeadersSize, headers, body)
|
||||
|
||||
proc post*(t: typedesc[HttpClientRequestRef], session: HttpSessionRef,
|
||||
url: string, version: HttpVersion = HttpVersion11,
|
||||
flags: set[HttpClientRequestFlag] = {},
|
||||
maxResponseHeadersSize: int = HttpMaxHeadersSize,
|
||||
headers: openArray[HttpHeaderTuple] = [],
|
||||
body: openArray[char] = []): HttpResult[HttpClientRequestRef] {.
|
||||
raises: [].} =
|
||||
HttpClientRequestRef.new(session, url, MethodPost, version, flags, headers,
|
||||
body: openArray[char] = []): HttpResult[HttpClientRequestRef] =
|
||||
HttpClientRequestRef.new(session, url, MethodPost, version, flags,
|
||||
maxResponseHeadersSize, headers,
|
||||
body.toOpenArrayByte(0, len(body) - 1))
|
||||
|
||||
proc post*(t: typedesc[HttpClientRequestRef], session: HttpSessionRef,
|
||||
ha: HttpAddress, version: HttpVersion = HttpVersion11,
|
||||
flags: set[HttpClientRequestFlag] = {},
|
||||
maxResponseHeadersSize: int = HttpMaxHeadersSize,
|
||||
headers: openArray[HttpHeaderTuple] = [],
|
||||
body: openArray[byte] = []): HttpClientRequestRef {.
|
||||
raises: [].} =
|
||||
HttpClientRequestRef.new(session, ha, MethodPost, version, flags, headers,
|
||||
body)
|
||||
body: openArray[byte] = []): HttpClientRequestRef =
|
||||
HttpClientRequestRef.new(session, ha, MethodPost, version, flags,
|
||||
maxResponseHeadersSize, headers, body)
|
||||
|
||||
proc post*(t: typedesc[HttpClientRequestRef], session: HttpSessionRef,
|
||||
ha: HttpAddress, version: HttpVersion = HttpVersion11,
|
||||
flags: set[HttpClientRequestFlag] = {},
|
||||
maxResponseHeadersSize: int = HttpMaxHeadersSize,
|
||||
headers: openArray[HttpHeaderTuple] = [],
|
||||
body: openArray[char] = []): HttpClientRequestRef {.
|
||||
raises: [].} =
|
||||
HttpClientRequestRef.new(session, ha, MethodPost, version, flags, headers,
|
||||
body: openArray[char] = []): HttpClientRequestRef =
|
||||
HttpClientRequestRef.new(session, ha, MethodPost, version, flags,
|
||||
maxResponseHeadersSize, headers,
|
||||
body.toOpenArrayByte(0, len(body) - 1))
|
||||
|
||||
proc prepareRequest(request: HttpClientRequestRef): string {.
|
||||
raises: [].} =
|
||||
proc prepareRequest(request: HttpClientRequestRef): string =
|
||||
template hasChunkedEncoding(request: HttpClientRequestRef): bool =
|
||||
toLowerAscii(request.headers.getString(TransferEncodingHeader)) == "chunked"
|
||||
|
||||
@ -1043,7 +1181,7 @@ proc prepareRequest(request: HttpClientRequestRef): string {.
|
||||
res
|
||||
|
||||
proc send*(request: HttpClientRequestRef): Future[HttpClientResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError, HttpError]).} =
|
||||
doAssert(request.state == HttpReqRespState.Ready,
|
||||
"Request's state is " & $request.state)
|
||||
let connection =
|
||||
@ -1076,25 +1214,24 @@ proc send*(request: HttpClientRequestRef): Future[HttpClientResponseRef] {.
|
||||
request.setDuration()
|
||||
request.setError(newHttpInterruptError())
|
||||
raise exc
|
||||
except AsyncStreamError:
|
||||
except AsyncStreamError as exc:
|
||||
request.setDuration()
|
||||
let error = newHttpWriteError("Could not send request headers")
|
||||
let error = newHttpWriteError(
|
||||
"Could not send request headers, reason: " & $exc.msg)
|
||||
request.setError(error)
|
||||
raise error
|
||||
|
||||
let resp =
|
||||
try:
|
||||
await request.getResponse()
|
||||
except CancelledError as exc:
|
||||
request.setError(newHttpInterruptError())
|
||||
raise exc
|
||||
except HttpError as exc:
|
||||
request.setError(exc)
|
||||
raise exc
|
||||
return resp
|
||||
try:
|
||||
await request.getResponse()
|
||||
except CancelledError as exc:
|
||||
request.setError(newHttpInterruptError())
|
||||
raise exc
|
||||
except HttpError as exc:
|
||||
request.setError(exc)
|
||||
raise exc
|
||||
|
||||
proc open*(request: HttpClientRequestRef): Future[HttpBodyWriter] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError, HttpError]).} =
|
||||
## Start sending request's headers and return `HttpBodyWriter`, which can be
|
||||
## used to send request's body.
|
||||
doAssert(request.state == HttpReqRespState.Ready,
|
||||
@ -1124,8 +1261,9 @@ proc open*(request: HttpClientRequestRef): Future[HttpBodyWriter] {.
|
||||
request.setDuration()
|
||||
request.setError(newHttpInterruptError())
|
||||
raise exc
|
||||
except AsyncStreamError:
|
||||
let error = newHttpWriteError("Could not send request headers")
|
||||
except AsyncStreamError as exc:
|
||||
let error = newHttpWriteError(
|
||||
"Could not send request headers, reason: " & $exc.msg)
|
||||
request.setDuration()
|
||||
request.setError(error)
|
||||
raise error
|
||||
@ -1147,10 +1285,10 @@ proc open*(request: HttpClientRequestRef): Future[HttpBodyWriter] {.
|
||||
request.writer = writer
|
||||
request.state = HttpReqRespState.Open
|
||||
request.connection.state = HttpClientConnectionState.RequestBodySending
|
||||
return writer
|
||||
writer
|
||||
|
||||
proc finish*(request: HttpClientRequestRef): Future[HttpClientResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError, HttpError]).} =
|
||||
## Finish sending request and receive response.
|
||||
doAssert(not(isNil(request.connection)),
|
||||
"Request missing connection instance")
|
||||
@ -1187,7 +1325,8 @@ proc getNewLocation*(resp: HttpClientResponseRef): HttpResult[HttpAddress] =
|
||||
else:
|
||||
err("Location header is missing")
|
||||
|
||||
proc getBodyReader*(response: HttpClientResponseRef): HttpBodyReader =
|
||||
proc getBodyReader*(response: HttpClientResponseRef): HttpBodyReader {.
|
||||
raises: [HttpUseClosedError].} =
|
||||
## Returns stream's reader instance which can be used to read response's body.
|
||||
##
|
||||
## Streams which was obtained using this procedure must be closed to avoid
|
||||
@ -1205,18 +1344,24 @@ proc getBodyReader*(response: HttpClientResponseRef): HttpBodyReader =
|
||||
let reader =
|
||||
case response.bodyFlag
|
||||
of HttpClientBodyFlag.Sized:
|
||||
let bstream = newBoundedStreamReader(response.connection.reader,
|
||||
response.contentLength)
|
||||
newHttpBodyReader(bstream)
|
||||
newHttpBodyReader(
|
||||
newBoundedStreamReader(
|
||||
response.connection.reader, response.contentLength,
|
||||
bufferSize = response.session.connectionBufferSize))
|
||||
of HttpClientBodyFlag.Chunked:
|
||||
newHttpBodyReader(newChunkedStreamReader(response.connection.reader))
|
||||
newHttpBodyReader(
|
||||
newChunkedStreamReader(
|
||||
response.connection.reader,
|
||||
bufferSize = response.session.connectionBufferSize))
|
||||
of HttpClientBodyFlag.Custom:
|
||||
newHttpBodyReader(newAsyncStreamReader(response.connection.reader))
|
||||
newHttpBodyReader(
|
||||
newAsyncStreamReader(response.connection.reader))
|
||||
response.connection.state = HttpClientConnectionState.ResponseBodyReceiving
|
||||
response.reader = reader
|
||||
response.reader
|
||||
|
||||
proc finish*(response: HttpClientResponseRef) {.async.} =
|
||||
proc finish*(response: HttpClientResponseRef) {.
|
||||
async: (raises: [HttpUseClosedError]).} =
|
||||
## Finish receiving response.
|
||||
##
|
||||
## Because ``finish()`` returns nothing, this operation become NOP for
|
||||
@ -1235,7 +1380,7 @@ proc finish*(response: HttpClientResponseRef) {.async.} =
|
||||
response.setDuration()
|
||||
|
||||
proc getBodyBytes*(response: HttpClientResponseRef): Future[seq[byte]] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError, HttpError]).} =
|
||||
## Read all bytes from response ``response``.
|
||||
##
|
||||
## Note: This procedure performs automatic finishing for ``response``.
|
||||
@ -1245,21 +1390,22 @@ proc getBodyBytes*(response: HttpClientResponseRef): Future[seq[byte]] {.
|
||||
await reader.closeWait()
|
||||
reader = nil
|
||||
await response.finish()
|
||||
return data
|
||||
data
|
||||
except CancelledError as exc:
|
||||
if not(isNil(reader)):
|
||||
await reader.closeWait()
|
||||
response.setError(newHttpInterruptError())
|
||||
raise exc
|
||||
except AsyncStreamError:
|
||||
except AsyncStreamError as exc:
|
||||
let error = newHttpReadError("Could not read response, reason: " & $exc.msg)
|
||||
if not(isNil(reader)):
|
||||
await reader.closeWait()
|
||||
let error = newHttpReadError("Could not read response")
|
||||
response.setError(error)
|
||||
raise error
|
||||
|
||||
proc getBodyBytes*(response: HttpClientResponseRef,
|
||||
nbytes: int): Future[seq[byte]] {.async.} =
|
||||
nbytes: int): Future[seq[byte]] {.
|
||||
async: (raises: [CancelledError, HttpError]).} =
|
||||
## Read all bytes (nbytes <= 0) or exactly `nbytes` bytes from response
|
||||
## ``response``.
|
||||
##
|
||||
@ -1270,20 +1416,21 @@ proc getBodyBytes*(response: HttpClientResponseRef,
|
||||
await reader.closeWait()
|
||||
reader = nil
|
||||
await response.finish()
|
||||
return data
|
||||
data
|
||||
except CancelledError as exc:
|
||||
if not(isNil(reader)):
|
||||
await reader.closeWait()
|
||||
response.setError(newHttpInterruptError())
|
||||
raise exc
|
||||
except AsyncStreamError:
|
||||
except AsyncStreamError as exc:
|
||||
let error = newHttpReadError("Could not read response, reason: " & $exc.msg)
|
||||
if not(isNil(reader)):
|
||||
await reader.closeWait()
|
||||
let error = newHttpReadError("Could not read response")
|
||||
response.setError(error)
|
||||
raise error
|
||||
|
||||
proc consumeBody*(response: HttpClientResponseRef): Future[int] {.async.} =
|
||||
proc consumeBody*(response: HttpClientResponseRef): Future[int] {.
|
||||
async: (raises: [CancelledError, HttpError]).} =
|
||||
## Consume/discard response and return number of bytes consumed.
|
||||
##
|
||||
## Note: This procedure performs automatic finishing for ``response``.
|
||||
@ -1293,16 +1440,17 @@ proc consumeBody*(response: HttpClientResponseRef): Future[int] {.async.} =
|
||||
await reader.closeWait()
|
||||
reader = nil
|
||||
await response.finish()
|
||||
return res
|
||||
res
|
||||
except CancelledError as exc:
|
||||
if not(isNil(reader)):
|
||||
await reader.closeWait()
|
||||
response.setError(newHttpInterruptError())
|
||||
raise exc
|
||||
except AsyncStreamError:
|
||||
except AsyncStreamError as exc:
|
||||
let error = newHttpReadError(
|
||||
"Could not consume response, reason: " & $exc.msg)
|
||||
if not(isNil(reader)):
|
||||
await reader.closeWait()
|
||||
let error = newHttpReadError("Could not read response")
|
||||
response.setError(error)
|
||||
raise error
|
||||
|
||||
@ -1317,8 +1465,15 @@ proc redirect*(request: HttpClientRequestRef,
|
||||
if redirectCount > request.session.maxRedirections:
|
||||
err("Maximum number of redirects exceeded")
|
||||
else:
|
||||
var res = HttpClientRequestRef.new(request.session, ha, request.meth,
|
||||
request.version, request.flags, request.headers.toList(), request.buffer)
|
||||
let headers =
|
||||
block:
|
||||
var res = request.headers
|
||||
res.set(HostHeader, ha.hostname)
|
||||
res
|
||||
var res =
|
||||
HttpClientRequestRef.new(request.session, ha, request.meth,
|
||||
request.version, request.flags, headers = headers.toList(),
|
||||
body = request.buffer)
|
||||
res.redirectCount = redirectCount
|
||||
ok(res)
|
||||
|
||||
@ -1335,13 +1490,21 @@ proc redirect*(request: HttpClientRequestRef,
|
||||
err("Maximum number of redirects exceeded")
|
||||
else:
|
||||
let address = ? request.session.redirect(request.address, uri)
|
||||
var res = HttpClientRequestRef.new(request.session, address, request.meth,
|
||||
request.version, request.flags, request.headers.toList(), request.buffer)
|
||||
# Update Host header to redirected URL hostname
|
||||
let headers =
|
||||
block:
|
||||
var res = request.headers
|
||||
res.set(HostHeader, address.hostname)
|
||||
res
|
||||
var res =
|
||||
HttpClientRequestRef.new(request.session, address, request.meth,
|
||||
request.version, request.flags, headers = headers.toList(),
|
||||
body = request.buffer)
|
||||
res.redirectCount = redirectCount
|
||||
ok(res)
|
||||
|
||||
proc fetch*(request: HttpClientRequestRef): Future[HttpResponseTuple] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError, HttpError]).} =
|
||||
var response: HttpClientResponseRef
|
||||
try:
|
||||
response = await request.send()
|
||||
@ -1349,7 +1512,7 @@ proc fetch*(request: HttpClientRequestRef): Future[HttpResponseTuple] {.
|
||||
let status = response.status
|
||||
await response.closeWait()
|
||||
response = nil
|
||||
return (status, buffer)
|
||||
(status, buffer)
|
||||
except HttpError as exc:
|
||||
if not(isNil(response)): await response.closeWait()
|
||||
raise exc
|
||||
@ -1358,7 +1521,7 @@ proc fetch*(request: HttpClientRequestRef): Future[HttpResponseTuple] {.
|
||||
raise exc
|
||||
|
||||
proc fetch*(session: HttpSessionRef, url: Uri): Future[HttpResponseTuple] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError, HttpError]).} =
|
||||
## Fetch resource pointed by ``url`` using HTTP GET method and ``session``
|
||||
## parameters.
|
||||
##
|
||||
@ -1400,28 +1563,34 @@ proc fetch*(session: HttpSessionRef, url: Uri): Future[HttpResponseTuple] {.
|
||||
request = redirect
|
||||
redirect = nil
|
||||
else:
|
||||
let data = await response.getBodyBytes()
|
||||
let code = response.status
|
||||
let
|
||||
data = await response.getBodyBytes()
|
||||
code = response.status
|
||||
await response.closeWait()
|
||||
response = nil
|
||||
await request.closeWait()
|
||||
request = nil
|
||||
return (code, data)
|
||||
except CancelledError as exc:
|
||||
if not(isNil(response)): await closeWait(response)
|
||||
if not(isNil(request)): await closeWait(request)
|
||||
if not(isNil(redirect)): await closeWait(redirect)
|
||||
var pending: seq[Future[void]]
|
||||
if not(isNil(response)): pending.add(closeWait(response))
|
||||
if not(isNil(request)): pending.add(closeWait(request))
|
||||
if not(isNil(redirect)): pending.add(closeWait(redirect))
|
||||
await noCancel(allFutures(pending))
|
||||
raise exc
|
||||
except HttpError as exc:
|
||||
if not(isNil(response)): await closeWait(response)
|
||||
if not(isNil(request)): await closeWait(request)
|
||||
if not(isNil(redirect)): await closeWait(redirect)
|
||||
var pending: seq[Future[void]]
|
||||
if not(isNil(response)): pending.add(closeWait(response))
|
||||
if not(isNil(request)): pending.add(closeWait(request))
|
||||
if not(isNil(redirect)): pending.add(closeWait(redirect))
|
||||
await noCancel(allFutures(pending))
|
||||
raise exc
|
||||
|
||||
proc getServerSentEvents*(
|
||||
response: HttpClientResponseRef,
|
||||
maxEventSize: int = -1
|
||||
): Future[seq[ServerSentEvent]] {.async.} =
|
||||
): Future[seq[ServerSentEvent]] {.
|
||||
async: (raises: [CancelledError, HttpError]).} =
|
||||
## Read number of server-sent events (SSE) from HTTP response ``response``.
|
||||
##
|
||||
## ``maxEventSize`` - maximum size of events chunk in one message, use
|
||||
@ -1509,8 +1678,14 @@ proc getServerSentEvents*(
|
||||
|
||||
(i, false)
|
||||
|
||||
await reader.readMessage(predicate)
|
||||
try:
|
||||
await reader.readMessage(predicate)
|
||||
except CancelledError as exc:
|
||||
raise exc
|
||||
except AsyncStreamError as exc:
|
||||
raiseHttpReadError($exc.msg)
|
||||
|
||||
if not isNil(error):
|
||||
raise error
|
||||
else:
|
||||
return res
|
||||
|
||||
res
|
||||
|
||||
@ -6,8 +6,11 @@
|
||||
# Licensed under either of
|
||||
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
||||
# MIT license (LICENSE-MIT)
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[strutils, uri]
|
||||
import stew/results, httputils
|
||||
import results, httputils
|
||||
import ../../asyncloop, ../../asyncsync
|
||||
import ../../streams/[asyncstream, boundstream]
|
||||
export asyncloop, asyncsync, results, httputils, strutils
|
||||
@ -40,30 +43,48 @@ const
|
||||
ServerHeader* = "server"
|
||||
LocationHeader* = "location"
|
||||
AuthorizationHeader* = "authorization"
|
||||
ContentDispositionHeader* = "content-disposition"
|
||||
|
||||
UrlEncodedContentType* = MediaType.init("application/x-www-form-urlencoded")
|
||||
MultipartContentType* = MediaType.init("multipart/form-data")
|
||||
|
||||
type
|
||||
HttpMessage* = object
|
||||
code*: HttpCode
|
||||
contentType*: MediaType
|
||||
message*: string
|
||||
|
||||
HttpResult*[T] = Result[T, string]
|
||||
HttpResultCode*[T] = Result[T, HttpCode]
|
||||
HttpResultMessage*[T] = Result[T, HttpMessage]
|
||||
|
||||
HttpDefect* = object of Defect
|
||||
HttpError* = object of CatchableError
|
||||
HttpCriticalError* = object of HttpError
|
||||
code*: HttpCode
|
||||
HttpRecoverableError* = object of HttpError
|
||||
code*: HttpCode
|
||||
HttpDisconnectError* = object of HttpError
|
||||
HttpConnectionError* = object of HttpError
|
||||
HttpError* = object of AsyncError
|
||||
HttpInterruptError* = object of HttpError
|
||||
HttpReadError* = object of HttpError
|
||||
HttpWriteError* = object of HttpError
|
||||
HttpProtocolError* = object of HttpError
|
||||
HttpRedirectError* = object of HttpError
|
||||
HttpAddressError* = object of HttpError
|
||||
HttpUseClosedError* = object of HttpError
|
||||
|
||||
HttpTransportError* = object of HttpError
|
||||
HttpAddressError* = object of HttpTransportError
|
||||
HttpRedirectError* = object of HttpTransportError
|
||||
HttpConnectionError* = object of HttpTransportError
|
||||
HttpReadError* = object of HttpTransportError
|
||||
HttpReadLimitError* = object of HttpReadError
|
||||
HttpDisconnectError* = object of HttpReadError
|
||||
HttpWriteError* = object of HttpTransportError
|
||||
|
||||
HttpProtocolError* = object of HttpError
|
||||
code*: HttpCode
|
||||
|
||||
HttpCriticalError* = object of HttpProtocolError # deprecated
|
||||
HttpRecoverableError* = object of HttpProtocolError # deprecated
|
||||
|
||||
HttpRequestError* = object of HttpProtocolError
|
||||
HttpRequestHeadersError* = object of HttpRequestError
|
||||
HttpRequestBodyError* = object of HttpRequestError
|
||||
HttpRequestHeadersTooLargeError* = object of HttpRequestHeadersError
|
||||
HttpRequestBodyTooLargeError* = object of HttpRequestBodyError
|
||||
HttpResponseError* = object of HttpProtocolError
|
||||
|
||||
HttpInvalidUsageError* = object of HttpError
|
||||
HttpUseClosedError* = object of HttpInvalidUsageError
|
||||
|
||||
KeyValueTuple* = tuple
|
||||
key: string
|
||||
@ -82,35 +103,95 @@ type
|
||||
HttpState* {.pure.} = enum
|
||||
Alive, Closing, Closed
|
||||
|
||||
proc raiseHttpCriticalError*(msg: string,
|
||||
code = Http400) {.noinline, noreturn.} =
|
||||
HttpAddressErrorType* {.pure.} = enum
|
||||
InvalidUrlScheme,
|
||||
InvalidPortNumber,
|
||||
MissingHostname,
|
||||
InvalidIpHostname,
|
||||
NameLookupFailed,
|
||||
NoAddressResolved
|
||||
|
||||
const
|
||||
CriticalHttpAddressError* = {
|
||||
HttpAddressErrorType.InvalidUrlScheme,
|
||||
HttpAddressErrorType.InvalidPortNumber,
|
||||
HttpAddressErrorType.MissingHostname,
|
||||
HttpAddressErrorType.InvalidIpHostname
|
||||
}
|
||||
|
||||
RecoverableHttpAddressError* = {
|
||||
HttpAddressErrorType.NameLookupFailed,
|
||||
HttpAddressErrorType.NoAddressResolved
|
||||
}
|
||||
|
||||
func isCriticalError*(error: HttpAddressErrorType): bool =
|
||||
error in CriticalHttpAddressError
|
||||
|
||||
func isRecoverableError*(error: HttpAddressErrorType): bool =
|
||||
error in RecoverableHttpAddressError
|
||||
|
||||
func toString*(error: HttpAddressErrorType): string =
|
||||
case error
|
||||
of HttpAddressErrorType.InvalidUrlScheme:
|
||||
"URL scheme not supported"
|
||||
of HttpAddressErrorType.InvalidPortNumber:
|
||||
"Invalid URL port number"
|
||||
of HttpAddressErrorType.MissingHostname:
|
||||
"Missing URL hostname"
|
||||
of HttpAddressErrorType.InvalidIpHostname:
|
||||
"Invalid IPv4/IPv6 address in hostname"
|
||||
of HttpAddressErrorType.NameLookupFailed:
|
||||
"Could not resolve remote address"
|
||||
of HttpAddressErrorType.NoAddressResolved:
|
||||
"No address has been resolved"
|
||||
|
||||
proc raiseHttpRequestBodyTooLargeError*() {.
|
||||
noinline, noreturn, raises: [HttpRequestBodyTooLargeError].} =
|
||||
raise (ref HttpRequestBodyTooLargeError)(
|
||||
code: Http413, msg: MaximumBodySizeError)
|
||||
|
||||
proc raiseHttpCriticalError*(msg: string, code = Http400) {.
|
||||
noinline, noreturn, raises: [HttpCriticalError].} =
|
||||
raise (ref HttpCriticalError)(code: code, msg: msg)
|
||||
|
||||
proc raiseHttpDisconnectError*() {.noinline, noreturn.} =
|
||||
proc raiseHttpDisconnectError*() {.
|
||||
noinline, noreturn, raises: [HttpDisconnectError].} =
|
||||
raise (ref HttpDisconnectError)(msg: "Remote peer disconnected")
|
||||
|
||||
proc raiseHttpDefect*(msg: string) {.noinline, noreturn.} =
|
||||
raise (ref HttpDefect)(msg: msg)
|
||||
|
||||
proc raiseHttpConnectionError*(msg: string) {.noinline, noreturn.} =
|
||||
proc raiseHttpConnectionError*(msg: string) {.
|
||||
noinline, noreturn, raises: [HttpConnectionError].} =
|
||||
raise (ref HttpConnectionError)(msg: msg)
|
||||
|
||||
proc raiseHttpInterruptError*() {.noinline, noreturn.} =
|
||||
proc raiseHttpInterruptError*() {.
|
||||
noinline, noreturn, raises: [HttpInterruptError].} =
|
||||
raise (ref HttpInterruptError)(msg: "Connection was interrupted")
|
||||
|
||||
proc raiseHttpReadError*(msg: string) {.noinline, noreturn.} =
|
||||
proc raiseHttpReadError*(msg: string) {.
|
||||
noinline, noreturn, raises: [HttpReadError].} =
|
||||
raise (ref HttpReadError)(msg: msg)
|
||||
|
||||
proc raiseHttpProtocolError*(msg: string) {.noinline, noreturn.} =
|
||||
raise (ref HttpProtocolError)(msg: msg)
|
||||
proc raiseHttpProtocolError*(msg: string) {.
|
||||
noinline, noreturn, raises: [HttpProtocolError].} =
|
||||
raise (ref HttpProtocolError)(code: Http400, msg: msg)
|
||||
|
||||
proc raiseHttpWriteError*(msg: string) {.noinline, noreturn.} =
|
||||
proc raiseHttpProtocolError*(code: HttpCode, msg: string) {.
|
||||
noinline, noreturn, raises: [HttpProtocolError].} =
|
||||
raise (ref HttpProtocolError)(code: code, msg: msg)
|
||||
|
||||
proc raiseHttpProtocolError*(msg: HttpMessage) {.
|
||||
noinline, noreturn, raises: [HttpProtocolError].} =
|
||||
raise (ref HttpProtocolError)(code: msg.code, msg: msg.message)
|
||||
|
||||
proc raiseHttpWriteError*(msg: string) {.
|
||||
noinline, noreturn, raises: [HttpWriteError].} =
|
||||
raise (ref HttpWriteError)(msg: msg)
|
||||
|
||||
proc raiseHttpRedirectError*(msg: string) {.noinline, noreturn.} =
|
||||
proc raiseHttpRedirectError*(msg: string) {.
|
||||
noinline, noreturn, raises: [HttpRedirectError].} =
|
||||
raise (ref HttpRedirectError)(msg: msg)
|
||||
|
||||
proc raiseHttpAddressError*(msg: string) {.noinline, noreturn.} =
|
||||
proc raiseHttpAddressError*(msg: string) {.
|
||||
noinline, noreturn, raises: [HttpAddressError].} =
|
||||
raise (ref HttpAddressError)(msg: msg)
|
||||
|
||||
template newHttpInterruptError*(): ref HttpInterruptError =
|
||||
@ -125,9 +206,25 @@ template newHttpWriteError*(message: string): ref HttpWriteError =
|
||||
template newHttpUseClosedError*(): ref HttpUseClosedError =
|
||||
newException(HttpUseClosedError, "Connection was already closed")
|
||||
|
||||
func init*(t: typedesc[HttpMessage], code: HttpCode, message: string,
|
||||
contentType: MediaType): HttpMessage =
|
||||
HttpMessage(code: code, message: message, contentType: contentType)
|
||||
|
||||
func init*(t: typedesc[HttpMessage], code: HttpCode, message: string,
|
||||
contentType: string): HttpMessage =
|
||||
HttpMessage(code: code, message: message,
|
||||
contentType: MediaType.init(contentType))
|
||||
|
||||
func init*(t: typedesc[HttpMessage], code: HttpCode,
|
||||
message: string): HttpMessage =
|
||||
HttpMessage(code: code, message: message,
|
||||
contentType: MediaType.init("text/plain"))
|
||||
|
||||
func init*(t: typedesc[HttpMessage], code: HttpCode): HttpMessage =
|
||||
HttpMessage(code: code)
|
||||
|
||||
iterator queryParams*(query: string,
|
||||
flags: set[QueryParamsFlag] = {}): KeyValueTuple {.
|
||||
raises: [].} =
|
||||
flags: set[QueryParamsFlag] = {}): KeyValueTuple =
|
||||
## Iterate over url-encoded query string.
|
||||
for pair in query.split('&'):
|
||||
let items = pair.split('=', maxsplit = 1)
|
||||
@ -140,9 +237,9 @@ iterator queryParams*(query: string,
|
||||
else:
|
||||
yield (decodeUrl(k), decodeUrl(v))
|
||||
|
||||
func getTransferEncoding*(ch: openArray[string]): HttpResult[
|
||||
set[TransferEncodingFlags]] {.
|
||||
raises: [].} =
|
||||
func getTransferEncoding*(
|
||||
ch: openArray[string]
|
||||
): HttpResult[set[TransferEncodingFlags]] =
|
||||
## Parse value of multiple HTTP headers ``Transfer-Encoding`` and return
|
||||
## it as set of ``TransferEncodingFlags``.
|
||||
var res: set[TransferEncodingFlags] = {}
|
||||
@ -171,9 +268,9 @@ func getTransferEncoding*(ch: openArray[string]): HttpResult[
|
||||
return err("Incorrect Transfer-Encoding value")
|
||||
ok(res)
|
||||
|
||||
func getContentEncoding*(ch: openArray[string]): HttpResult[
|
||||
set[ContentEncodingFlags]] {.
|
||||
raises: [].} =
|
||||
func getContentEncoding*(
|
||||
ch: openArray[string]
|
||||
): HttpResult[set[ContentEncodingFlags]] =
|
||||
## Parse value of multiple HTTP headers ``Content-Encoding`` and return
|
||||
## it as set of ``ContentEncodingFlags``.
|
||||
var res: set[ContentEncodingFlags] = {}
|
||||
@ -202,8 +299,7 @@ func getContentEncoding*(ch: openArray[string]): HttpResult[
|
||||
return err("Incorrect Content-Encoding value")
|
||||
ok(res)
|
||||
|
||||
func getContentType*(ch: openArray[string]): HttpResult[ContentTypeData] {.
|
||||
raises: [].} =
|
||||
func getContentType*(ch: openArray[string]): HttpResult[ContentTypeData] =
|
||||
## Check and prepare value of ``Content-Type`` header.
|
||||
if len(ch) == 0:
|
||||
err("No Content-Type values found")
|
||||
|
||||
@ -6,8 +6,11 @@
|
||||
# Licensed under either of
|
||||
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
||||
# MIT license (LICENSE-MIT)
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/tables
|
||||
import stew/results
|
||||
import results
|
||||
import ../../timer
|
||||
import httpserver, shttpserver
|
||||
from httpclient import HttpClientScheme
|
||||
@ -16,8 +19,6 @@ from ../../osdefs import SocketHandle
|
||||
from ../../transports/common import TransportAddress, ServerFlags
|
||||
export HttpClientScheme, SocketHandle, TransportAddress, ServerFlags, HttpState
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
type
|
||||
ConnectionType* {.pure.} = enum
|
||||
NonSecure, Secure
|
||||
@ -29,6 +30,7 @@ type
|
||||
handle*: SocketHandle
|
||||
connectionType*: ConnectionType
|
||||
connectionState*: ConnectionState
|
||||
query*: Opt[string]
|
||||
remoteAddress*: Opt[TransportAddress]
|
||||
localAddress*: Opt[TransportAddress]
|
||||
acceptMoment*: Moment
|
||||
@ -85,6 +87,12 @@ proc getConnectionState*(holder: HttpConnectionHolderRef): ConnectionState =
|
||||
else:
|
||||
ConnectionState.Accepted
|
||||
|
||||
proc getQueryString*(holder: HttpConnectionHolderRef): Opt[string] =
|
||||
if not(isNil(holder.connection)):
|
||||
holder.connection.currentRawQuery
|
||||
else:
|
||||
Opt.none(string)
|
||||
|
||||
proc init*(t: typedesc[ServerConnectionInfo],
|
||||
holder: HttpConnectionHolderRef): ServerConnectionInfo =
|
||||
let
|
||||
@ -98,6 +106,7 @@ proc init*(t: typedesc[ServerConnectionInfo],
|
||||
Opt.some(holder.transp.remoteAddress())
|
||||
except CatchableError:
|
||||
Opt.none(TransportAddress)
|
||||
queryString = holder.getQueryString()
|
||||
|
||||
ServerConnectionInfo(
|
||||
handle: SocketHandle(holder.transp.fd),
|
||||
@ -106,6 +115,7 @@ proc init*(t: typedesc[ServerConnectionInfo],
|
||||
remoteAddress: remoteAddress,
|
||||
localAddress: localAddress,
|
||||
acceptMoment: holder.acceptMoment,
|
||||
query: queryString,
|
||||
createMoment:
|
||||
if not(isNil(holder.connection)):
|
||||
Opt.some(holder.connection.createMoment)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -197,3 +197,7 @@ proc toList*(ht: HttpTables, normKey = false): auto =
|
||||
for key, value in ht.stringItems(normKey):
|
||||
res.add((key, value))
|
||||
res
|
||||
|
||||
proc clear*(ht: var HttpTables) =
|
||||
## Resets the HtppTable so that it is empty.
|
||||
ht.table.clear()
|
||||
|
||||
@ -7,15 +7,20 @@
|
||||
# Licensed under either of
|
||||
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
||||
# MIT license (LICENSE-MIT)
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[monotimes, strutils]
|
||||
import stew/results, httputils
|
||||
import results, httputils
|
||||
import ../../asyncloop
|
||||
import ../../streams/[asyncstream, boundstream, chunkstream]
|
||||
import httptable, httpcommon, httpbodyrw
|
||||
import "."/[httptable, httpcommon, httpbodyrw]
|
||||
export asyncloop, httptable, httpcommon, httpbodyrw, asyncstream, httputils
|
||||
|
||||
const
|
||||
UnableToReadMultipartBody = "Unable to read multipart message body"
|
||||
UnableToReadMultipartBody = "Unable to read multipart message body, reason: "
|
||||
UnableToSendMultipartMessage = "Unable to send multipart message, reason: "
|
||||
MaxMultipartHeaderSize = 4096
|
||||
|
||||
type
|
||||
MultiPartSource* {.pure.} = enum
|
||||
@ -66,13 +71,12 @@ type
|
||||
name*: string
|
||||
filename*: string
|
||||
|
||||
MultipartError* = object of HttpCriticalError
|
||||
MultipartError* = object of HttpProtocolError
|
||||
MultipartEOMError* = object of MultipartError
|
||||
|
||||
BChar* = byte | char
|
||||
|
||||
proc startsWith(s, prefix: openArray[byte]): bool {.
|
||||
raises: [].} =
|
||||
proc startsWith(s, prefix: openArray[byte]): bool =
|
||||
# This procedure is copy of strutils.startsWith() procedure, however,
|
||||
# it is intended to work with arrays of bytes, but not with strings.
|
||||
var i = 0
|
||||
@ -81,8 +85,7 @@ proc startsWith(s, prefix: openArray[byte]): bool {.
|
||||
if i >= len(s) or s[i] != prefix[i]: return false
|
||||
inc(i)
|
||||
|
||||
proc parseUntil(s, until: openArray[byte]): int {.
|
||||
raises: [].} =
|
||||
proc parseUntil(s, until: openArray[byte]): int =
|
||||
# This procedure is copy of parseutils.parseUntil() procedure, however,
|
||||
# it is intended to work with arrays of bytes, but not with strings.
|
||||
var i = 0
|
||||
@ -95,8 +98,7 @@ proc parseUntil(s, until: openArray[byte]): int {.
|
||||
inc(i)
|
||||
-1
|
||||
|
||||
func setPartNames(part: var MultiPart): HttpResult[void] {.
|
||||
raises: [].} =
|
||||
func setPartNames(part: var MultiPart): HttpResult[void] =
|
||||
if part.headers.count("content-disposition") != 1:
|
||||
return err("Content-Disposition header is incorrect")
|
||||
var header = part.headers.getString("content-disposition")
|
||||
@ -105,7 +107,7 @@ func setPartNames(part: var MultiPart): HttpResult[void] {.
|
||||
return err("Content-Disposition header value is incorrect")
|
||||
let dtype = disp.dispositionType(header.toOpenArrayByte(0, len(header) - 1))
|
||||
if dtype.toLowerAscii() != "form-data":
|
||||
return err("Content-Disposition type is incorrect")
|
||||
return err("Content-Disposition header type is incorrect")
|
||||
for k, v in disp.fields(header.toOpenArrayByte(0, len(header) - 1)):
|
||||
case k.toLowerAscii()
|
||||
of "name":
|
||||
@ -120,8 +122,7 @@ func setPartNames(part: var MultiPart): HttpResult[void] {.
|
||||
|
||||
proc init*[A: BChar, B: BChar](mpt: typedesc[MultiPartReader],
|
||||
buffer: openArray[A],
|
||||
boundary: openArray[B]): MultiPartReader {.
|
||||
raises: [].} =
|
||||
boundary: openArray[B]): MultiPartReader =
|
||||
## Create new MultiPartReader instance with `buffer` interface.
|
||||
##
|
||||
## ``buffer`` - is buffer which will be used to read data.
|
||||
@ -142,11 +143,11 @@ proc init*[A: BChar, B: BChar](mpt: typedesc[MultiPartReader],
|
||||
MultiPartReader(kind: MultiPartSource.Buffer,
|
||||
buffer: buf, offset: 0, boundary: fboundary)
|
||||
|
||||
proc new*[B: BChar](mpt: typedesc[MultiPartReaderRef],
|
||||
stream: HttpBodyReader,
|
||||
boundary: openArray[B],
|
||||
partHeadersMaxSize = 4096): MultiPartReaderRef {.
|
||||
raises: [].} =
|
||||
proc new*[B: BChar](
|
||||
mpt: typedesc[MultiPartReaderRef],
|
||||
stream: HttpBodyReader,
|
||||
boundary: openArray[B],
|
||||
partHeadersMaxSize = MaxMultipartHeaderSize): MultiPartReaderRef =
|
||||
## Create new MultiPartReader instance with `stream` interface.
|
||||
##
|
||||
## ``stream`` is stream used to read data.
|
||||
@ -173,7 +174,17 @@ proc new*[B: BChar](mpt: typedesc[MultiPartReaderRef],
|
||||
stream: stream, offset: 0, boundary: fboundary,
|
||||
buffer: newSeq[byte](partHeadersMaxSize))
|
||||
|
||||
proc readPart*(mpr: MultiPartReaderRef): Future[MultiPart] {.async.} =
|
||||
template handleAsyncStreamReaderError(targ, excarg: untyped) =
|
||||
if targ.hasOverflow():
|
||||
raiseHttpRequestBodyTooLargeError()
|
||||
raiseHttpReadError(UnableToReadMultipartBody & $excarg.msg)
|
||||
|
||||
template handleAsyncStreamWriterError(targ, excarg: untyped) =
|
||||
targ.state = MultiPartWriterState.MessageFailure
|
||||
raiseHttpWriteError(UnableToSendMultipartMessage & $excarg.msg)
|
||||
|
||||
proc readPart*(mpr: MultiPartReaderRef): Future[MultiPart] {.
|
||||
async: (raises: [CancelledError, HttpReadError, HttpProtocolError]).} =
|
||||
doAssert(mpr.kind == MultiPartSource.Stream)
|
||||
if mpr.firstTime:
|
||||
try:
|
||||
@ -182,14 +193,11 @@ proc readPart*(mpr: MultiPartReaderRef): Future[MultiPart] {.async.} =
|
||||
mpr.firstTime = false
|
||||
if not(startsWith(mpr.buffer.toOpenArray(0, len(mpr.boundary) - 3),
|
||||
mpr.boundary.toOpenArray(2, len(mpr.boundary) - 1))):
|
||||
raiseHttpCriticalError("Unexpected boundary encountered")
|
||||
raiseHttpProtocolError(Http400, "Unexpected boundary encountered")
|
||||
except CancelledError as exc:
|
||||
raise exc
|
||||
except AsyncStreamError:
|
||||
if mpr.stream.hasOverflow():
|
||||
raiseHttpCriticalError(MaximumBodySizeError, Http413)
|
||||
else:
|
||||
raiseHttpCriticalError(UnableToReadMultipartBody)
|
||||
except AsyncStreamError as exc:
|
||||
handleAsyncStreamReaderError(mpr.stream, exc)
|
||||
|
||||
# Reading part's headers
|
||||
try:
|
||||
@ -203,9 +211,9 @@ proc readPart*(mpr: MultiPartReaderRef): Future[MultiPart] {.async.} =
|
||||
raise newException(MultipartEOMError,
|
||||
"End of multipart message")
|
||||
else:
|
||||
raiseHttpCriticalError("Incorrect multipart header found")
|
||||
raiseHttpProtocolError(Http400, "Incorrect multipart header found")
|
||||
if mpr.buffer[0] != 0x0D'u8 or mpr.buffer[1] != 0x0A'u8:
|
||||
raiseHttpCriticalError("Incorrect multipart boundary found")
|
||||
raiseHttpProtocolError(Http400, "Incorrect multipart boundary found")
|
||||
|
||||
# If two bytes are CRLF we are at the part beginning.
|
||||
# Reading part's headers
|
||||
@ -213,7 +221,7 @@ proc readPart*(mpr: MultiPartReaderRef): Future[MultiPart] {.async.} =
|
||||
HeadersMark)
|
||||
var headersList = parseHeaders(mpr.buffer.toOpenArray(0, res - 1), false)
|
||||
if headersList.failed():
|
||||
raiseHttpCriticalError("Incorrect multipart's headers found")
|
||||
raiseHttpProtocolError(Http400, "Incorrect multipart's headers found")
|
||||
inc(mpr.counter)
|
||||
|
||||
var part = MultiPart(
|
||||
@ -229,48 +237,39 @@ proc readPart*(mpr: MultiPartReaderRef): Future[MultiPart] {.async.} =
|
||||
|
||||
let sres = part.setPartNames()
|
||||
if sres.isErr():
|
||||
raiseHttpCriticalError($sres.error)
|
||||
raiseHttpProtocolError(Http400, $sres.error)
|
||||
return part
|
||||
|
||||
except CancelledError as exc:
|
||||
raise exc
|
||||
except AsyncStreamError:
|
||||
if mpr.stream.hasOverflow():
|
||||
raiseHttpCriticalError(MaximumBodySizeError, Http413)
|
||||
else:
|
||||
raiseHttpCriticalError(UnableToReadMultipartBody)
|
||||
except AsyncStreamError as exc:
|
||||
handleAsyncStreamReaderError(mpr.stream, exc)
|
||||
|
||||
proc getBody*(mp: MultiPart): Future[seq[byte]] {.async.} =
|
||||
proc getBody*(mp: MultiPart): Future[seq[byte]] {.
|
||||
async: (raises: [CancelledError, HttpReadError, HttpProtocolError]).} =
|
||||
## Get multipart's ``mp`` value as sequence of bytes.
|
||||
case mp.kind
|
||||
of MultiPartSource.Stream:
|
||||
try:
|
||||
let res = await mp.stream.read()
|
||||
return res
|
||||
except AsyncStreamError:
|
||||
if mp.breader.hasOverflow():
|
||||
raiseHttpCriticalError(MaximumBodySizeError, Http413)
|
||||
else:
|
||||
raiseHttpCriticalError(UnableToReadMultipartBody)
|
||||
await mp.stream.read()
|
||||
except AsyncStreamError as exc:
|
||||
handleAsyncStreamReaderError(mp.breader, exc)
|
||||
of MultiPartSource.Buffer:
|
||||
return mp.buffer
|
||||
mp.buffer
|
||||
|
||||
proc consumeBody*(mp: MultiPart) {.async.} =
|
||||
proc consumeBody*(mp: MultiPart) {.
|
||||
async: (raises: [CancelledError, HttpReadError, HttpProtocolError]).} =
|
||||
## Discard multipart's ``mp`` value.
|
||||
case mp.kind
|
||||
of MultiPartSource.Stream:
|
||||
try:
|
||||
discard await mp.stream.consume()
|
||||
except AsyncStreamError:
|
||||
if mp.breader.hasOverflow():
|
||||
raiseHttpCriticalError(MaximumBodySizeError, Http413)
|
||||
else:
|
||||
raiseHttpCriticalError(UnableToReadMultipartBody)
|
||||
except AsyncStreamError as exc:
|
||||
handleAsyncStreamReaderError(mp.breader, exc)
|
||||
of MultiPartSource.Buffer:
|
||||
discard
|
||||
|
||||
proc getBodyStream*(mp: MultiPart): HttpResult[AsyncStreamReader] {.
|
||||
raises: [].} =
|
||||
proc getBodyStream*(mp: MultiPart): HttpResult[AsyncStreamReader] =
|
||||
## Get multipart's ``mp`` stream, which can be used to obtain value of the
|
||||
## part.
|
||||
case mp.kind
|
||||
@ -279,7 +278,7 @@ proc getBodyStream*(mp: MultiPart): HttpResult[AsyncStreamReader] {.
|
||||
else:
|
||||
err("Could not obtain stream from buffer-like part")
|
||||
|
||||
proc closeWait*(mp: MultiPart) {.async.} =
|
||||
proc closeWait*(mp: MultiPart) {.async: (raises: []).} =
|
||||
## Close and release MultiPart's ``mp`` stream and resources.
|
||||
case mp.kind
|
||||
of MultiPartSource.Stream:
|
||||
@ -287,7 +286,7 @@ proc closeWait*(mp: MultiPart) {.async.} =
|
||||
else:
|
||||
discard
|
||||
|
||||
proc closeWait*(mpr: MultiPartReaderRef) {.async.} =
|
||||
proc closeWait*(mpr: MultiPartReaderRef) {.async: (raises: []).} =
|
||||
## Close and release MultiPartReader's ``mpr`` stream and resources.
|
||||
case mpr.kind
|
||||
of MultiPartSource.Stream:
|
||||
@ -295,7 +294,7 @@ proc closeWait*(mpr: MultiPartReaderRef) {.async.} =
|
||||
else:
|
||||
discard
|
||||
|
||||
proc getBytes*(mp: MultiPart): seq[byte] {.raises: [].} =
|
||||
proc getBytes*(mp: MultiPart): seq[byte] =
|
||||
## Returns value for MultiPart ``mp`` as sequence of bytes.
|
||||
case mp.kind
|
||||
of MultiPartSource.Buffer:
|
||||
@ -304,7 +303,7 @@ proc getBytes*(mp: MultiPart): seq[byte] {.raises: [].} =
|
||||
doAssert(not(mp.stream.atEof()), "Value is not obtained yet")
|
||||
mp.buffer
|
||||
|
||||
proc getString*(mp: MultiPart): string {.raises: [].} =
|
||||
proc getString*(mp: MultiPart): string =
|
||||
## Returns value for MultiPart ``mp`` as string.
|
||||
case mp.kind
|
||||
of MultiPartSource.Buffer:
|
||||
@ -313,7 +312,7 @@ proc getString*(mp: MultiPart): string {.raises: [].} =
|
||||
doAssert(not(mp.stream.atEof()), "Value is not obtained yet")
|
||||
bytesToString(mp.buffer)
|
||||
|
||||
proc atEoM*(mpr: var MultiPartReader): bool {.raises: [].} =
|
||||
proc atEoM*(mpr: var MultiPartReader): bool =
|
||||
## Procedure returns ``true`` if MultiPartReader has reached the end of
|
||||
## multipart message.
|
||||
case mpr.kind
|
||||
@ -322,7 +321,7 @@ proc atEoM*(mpr: var MultiPartReader): bool {.raises: [].} =
|
||||
of MultiPartSource.Stream:
|
||||
mpr.stream.atEof()
|
||||
|
||||
proc atEoM*(mpr: MultiPartReaderRef): bool {.raises: [].} =
|
||||
proc atEoM*(mpr: MultiPartReaderRef): bool =
|
||||
## Procedure returns ``true`` if MultiPartReader has reached the end of
|
||||
## multipart message.
|
||||
case mpr.kind
|
||||
@ -331,8 +330,7 @@ proc atEoM*(mpr: MultiPartReaderRef): bool {.raises: [].} =
|
||||
of MultiPartSource.Stream:
|
||||
mpr.stream.atEof()
|
||||
|
||||
proc getPart*(mpr: var MultiPartReader): Result[MultiPart, string] {.
|
||||
raises: [].} =
|
||||
proc getPart*(mpr: var MultiPartReader): Result[MultiPart, string] =
|
||||
## Get multipart part from MultiPartReader instance.
|
||||
##
|
||||
## This procedure will work only for MultiPartReader with buffer source.
|
||||
@ -422,8 +420,7 @@ proc getPart*(mpr: var MultiPartReader): Result[MultiPart, string] {.
|
||||
else:
|
||||
err("Incorrect multipart form")
|
||||
|
||||
func isEmpty*(mp: MultiPart): bool {.
|
||||
raises: [].} =
|
||||
func isEmpty*(mp: MultiPart): bool =
|
||||
## Returns ``true`` is multipart ``mp`` is not initialized/filled yet.
|
||||
mp.counter == 0
|
||||
|
||||
@ -439,8 +436,7 @@ func validateBoundary[B: BChar](boundary: openArray[B]): HttpResult[void] =
|
||||
return err("Content-Type boundary alphabet incorrect")
|
||||
ok()
|
||||
|
||||
func getMultipartBoundary*(contentData: ContentTypeData): HttpResult[string] {.
|
||||
raises: [].} =
|
||||
func getMultipartBoundary*(contentData: ContentTypeData): HttpResult[string] =
|
||||
## Returns ``multipart/form-data`` boundary value from ``Content-Type``
|
||||
## header.
|
||||
##
|
||||
@ -480,8 +476,7 @@ proc quoteCheck(name: string): HttpResult[string] =
|
||||
ok(name)
|
||||
|
||||
proc init*[B: BChar](mpt: typedesc[MultiPartWriter],
|
||||
boundary: openArray[B]): MultiPartWriter {.
|
||||
raises: [].} =
|
||||
boundary: openArray[B]): MultiPartWriter =
|
||||
## Create new MultiPartWriter instance with `buffer` interface.
|
||||
##
|
||||
## ``boundary`` - is multipart boundary, this value must not be empty.
|
||||
@ -510,8 +505,7 @@ proc init*[B: BChar](mpt: typedesc[MultiPartWriter],
|
||||
|
||||
proc new*[B: BChar](mpt: typedesc[MultiPartWriterRef],
|
||||
stream: HttpBodyWriter,
|
||||
boundary: openArray[B]): MultiPartWriterRef {.
|
||||
raises: [].} =
|
||||
boundary: openArray[B]): MultiPartWriterRef =
|
||||
doAssert(validateBoundary(boundary).isOk())
|
||||
doAssert(not(isNil(stream)))
|
||||
|
||||
@ -538,7 +532,7 @@ proc new*[B: BChar](mpt: typedesc[MultiPartWriterRef],
|
||||
|
||||
proc prepareHeaders(partMark: openArray[byte], name: string, filename: string,
|
||||
headers: HttpTable): string =
|
||||
const ContentDisposition = "Content-Disposition"
|
||||
const ContentDispositionHeader = "Content-Disposition"
|
||||
let qname =
|
||||
block:
|
||||
let res = quoteCheck(name)
|
||||
@ -551,10 +545,10 @@ proc prepareHeaders(partMark: openArray[byte], name: string, filename: string,
|
||||
res.get()
|
||||
var buffer = newString(len(partMark))
|
||||
copyMem(addr buffer[0], unsafeAddr partMark[0], len(partMark))
|
||||
buffer.add(ContentDisposition)
|
||||
buffer.add(ContentDispositionHeader)
|
||||
buffer.add(": ")
|
||||
if ContentDisposition in headers:
|
||||
buffer.add(headers.getString(ContentDisposition))
|
||||
if ContentDispositionHeader in headers:
|
||||
buffer.add(headers.getString(ContentDispositionHeader))
|
||||
buffer.add("\r\n")
|
||||
else:
|
||||
buffer.add("form-data; name=\"")
|
||||
@ -567,7 +561,7 @@ proc prepareHeaders(partMark: openArray[byte], name: string, filename: string,
|
||||
buffer.add("\r\n")
|
||||
|
||||
for k, v in headers.stringItems():
|
||||
if k != toLowerAscii(ContentDisposition):
|
||||
if k != ContentDispositionHeader:
|
||||
if len(v) > 0:
|
||||
buffer.add(k)
|
||||
buffer.add(": ")
|
||||
@ -576,7 +570,8 @@ proc prepareHeaders(partMark: openArray[byte], name: string, filename: string,
|
||||
buffer.add("\r\n")
|
||||
buffer
|
||||
|
||||
proc begin*(mpw: MultiPartWriterRef) {.async.} =
|
||||
proc begin*(mpw: MultiPartWriterRef) {.
|
||||
async: (raises: [CancelledError, HttpWriteError]).} =
|
||||
## Starts multipart message form and write approprate markers to output
|
||||
## stream.
|
||||
doAssert(mpw.kind == MultiPartSource.Stream)
|
||||
@ -584,10 +579,9 @@ proc begin*(mpw: MultiPartWriterRef) {.async.} =
|
||||
# write "--"
|
||||
try:
|
||||
await mpw.stream.write(mpw.beginMark)
|
||||
except AsyncStreamError:
|
||||
mpw.state = MultiPartWriterState.MessageFailure
|
||||
raiseHttpCriticalError("Unable to start multipart message")
|
||||
mpw.state = MultiPartWriterState.MessageStarted
|
||||
mpw.state = MultiPartWriterState.MessageStarted
|
||||
except AsyncStreamError as exc:
|
||||
handleAsyncStreamWriterError(mpw, exc)
|
||||
|
||||
proc begin*(mpw: var MultiPartWriter) =
|
||||
## Starts multipart message form and write approprate markers to output
|
||||
@ -599,7 +593,8 @@ proc begin*(mpw: var MultiPartWriter) =
|
||||
mpw.state = MultiPartWriterState.MessageStarted
|
||||
|
||||
proc beginPart*(mpw: MultiPartWriterRef, name: string,
|
||||
filename: string, headers: HttpTable) {.async.} =
|
||||
filename: string, headers: HttpTable) {.
|
||||
async: (raises: [CancelledError, HttpWriteError]).} =
|
||||
## Starts part of multipart message and write appropriate ``headers`` to the
|
||||
## output stream.
|
||||
##
|
||||
@ -614,9 +609,8 @@ proc beginPart*(mpw: MultiPartWriterRef, name: string,
|
||||
try:
|
||||
await mpw.stream.write(buffer)
|
||||
mpw.state = MultiPartWriterState.PartStarted
|
||||
except AsyncStreamError:
|
||||
mpw.state = MultiPartWriterState.MessageFailure
|
||||
raiseHttpCriticalError("Unable to start multipart part")
|
||||
except AsyncStreamError as exc:
|
||||
handleAsyncStreamWriterError(mpw, exc)
|
||||
|
||||
proc beginPart*(mpw: var MultiPartWriter, name: string,
|
||||
filename: string, headers: HttpTable) =
|
||||
@ -634,38 +628,38 @@ proc beginPart*(mpw: var MultiPartWriter, name: string,
|
||||
mpw.buffer.add(buffer.toOpenArrayByte(0, len(buffer) - 1))
|
||||
mpw.state = MultiPartWriterState.PartStarted
|
||||
|
||||
proc write*(mpw: MultiPartWriterRef, pbytes: pointer, nbytes: int) {.async.} =
|
||||
proc write*(mpw: MultiPartWriterRef, pbytes: pointer, nbytes: int) {.
|
||||
async: (raises: [CancelledError, HttpWriteError]).} =
|
||||
## Write part's data ``data`` to the output stream.
|
||||
doAssert(mpw.kind == MultiPartSource.Stream)
|
||||
doAssert(mpw.state == MultiPartWriterState.PartStarted)
|
||||
try:
|
||||
# write <chunk> of data
|
||||
await mpw.stream.write(pbytes, nbytes)
|
||||
except AsyncStreamError:
|
||||
mpw.state = MultiPartWriterState.MessageFailure
|
||||
raiseHttpCriticalError("Unable to write multipart data")
|
||||
except AsyncStreamError as exc:
|
||||
handleAsyncStreamWriterError(mpw, exc)
|
||||
|
||||
proc write*(mpw: MultiPartWriterRef, data: seq[byte]) {.async.} =
|
||||
proc write*(mpw: MultiPartWriterRef, data: seq[byte]) {.
|
||||
async: (raises: [CancelledError, HttpWriteError]).} =
|
||||
## Write part's data ``data`` to the output stream.
|
||||
doAssert(mpw.kind == MultiPartSource.Stream)
|
||||
doAssert(mpw.state == MultiPartWriterState.PartStarted)
|
||||
try:
|
||||
# write <chunk> of data
|
||||
await mpw.stream.write(data)
|
||||
except AsyncStreamError:
|
||||
mpw.state = MultiPartWriterState.MessageFailure
|
||||
raiseHttpCriticalError("Unable to write multipart data")
|
||||
except AsyncStreamError as exc:
|
||||
handleAsyncStreamWriterError(mpw, exc)
|
||||
|
||||
proc write*(mpw: MultiPartWriterRef, data: string) {.async.} =
|
||||
proc write*(mpw: MultiPartWriterRef, data: string) {.
|
||||
async: (raises: [CancelledError, HttpWriteError]).} =
|
||||
## Write part's data ``data`` to the output stream.
|
||||
doAssert(mpw.kind == MultiPartSource.Stream)
|
||||
doAssert(mpw.state == MultiPartWriterState.PartStarted)
|
||||
try:
|
||||
# write <chunk> of data
|
||||
await mpw.stream.write(data)
|
||||
except AsyncStreamError:
|
||||
mpw.state = MultiPartWriterState.MessageFailure
|
||||
raiseHttpCriticalError("Unable to write multipart data")
|
||||
except AsyncStreamError as exc:
|
||||
handleAsyncStreamWriterError(mpw, exc)
|
||||
|
||||
proc write*(mpw: var MultiPartWriter, pbytes: pointer, nbytes: int) =
|
||||
## Write part's data ``data`` to the output stream.
|
||||
@ -688,16 +682,16 @@ proc write*(mpw: var MultiPartWriter, data: openArray[char]) =
|
||||
doAssert(mpw.state == MultiPartWriterState.PartStarted)
|
||||
mpw.buffer.add(data.toOpenArrayByte(0, len(data) - 1))
|
||||
|
||||
proc finishPart*(mpw: MultiPartWriterRef) {.async.} =
|
||||
proc finishPart*(mpw: MultiPartWriterRef) {.
|
||||
async: (raises: [CancelledError, HttpWriteError]).} =
|
||||
## Finish multipart's message part and send proper markers to output stream.
|
||||
doAssert(mpw.state == MultiPartWriterState.PartStarted)
|
||||
try:
|
||||
# write "<CR><LF>--"
|
||||
await mpw.stream.write(mpw.finishPartMark)
|
||||
mpw.state = MultiPartWriterState.PartFinished
|
||||
except AsyncStreamError:
|
||||
mpw.state = MultiPartWriterState.MessageFailure
|
||||
raiseHttpCriticalError("Unable to finish multipart message part")
|
||||
except AsyncStreamError as exc:
|
||||
handleAsyncStreamWriterError(mpw, exc)
|
||||
|
||||
proc finishPart*(mpw: var MultiPartWriter) =
|
||||
## Finish multipart's message part and send proper markers to output stream.
|
||||
@ -707,7 +701,8 @@ proc finishPart*(mpw: var MultiPartWriter) =
|
||||
mpw.buffer.add(mpw.finishPartMark)
|
||||
mpw.state = MultiPartWriterState.PartFinished
|
||||
|
||||
proc finish*(mpw: MultiPartWriterRef) {.async.} =
|
||||
proc finish*(mpw: MultiPartWriterRef) {.
|
||||
async: (raises: [CancelledError, HttpWriteError]).} =
|
||||
## Finish multipart's message form and send finishing markers to the output
|
||||
## stream.
|
||||
doAssert(mpw.kind == MultiPartSource.Stream)
|
||||
@ -716,9 +711,8 @@ proc finish*(mpw: MultiPartWriterRef) {.async.} =
|
||||
# write "<boundary>--"
|
||||
await mpw.stream.write(mpw.finishMark)
|
||||
mpw.state = MultiPartWriterState.MessageFinished
|
||||
except AsyncStreamError:
|
||||
mpw.state = MultiPartWriterState.MessageFailure
|
||||
raiseHttpCriticalError("Unable to finish multipart message")
|
||||
except AsyncStreamError as exc:
|
||||
handleAsyncStreamWriterError(mpw, exc)
|
||||
|
||||
proc finish*(mpw: var MultiPartWriter): seq[byte] =
|
||||
## Finish multipart's message form and send finishing markers to the output
|
||||
|
||||
@ -6,8 +6,11 @@
|
||||
# Licensed under either of
|
||||
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
||||
# MIT license (LICENSE-MIT)
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import httpserver
|
||||
import ../../asyncloop, ../../asyncsync
|
||||
import ../../[asyncloop, asyncsync, config]
|
||||
import ../../streams/[asyncstream, tlsstream]
|
||||
export asyncloop, asyncsync, httpserver, asyncstream, tlsstream
|
||||
|
||||
@ -24,59 +27,119 @@ type
|
||||
|
||||
SecureHttpConnectionRef* = ref SecureHttpConnection
|
||||
|
||||
proc closeSecConnection(conn: HttpConnectionRef) {.async.} =
|
||||
proc closeSecConnection(conn: HttpConnectionRef) {.async: (raises: []).} =
|
||||
if conn.state == HttpState.Alive:
|
||||
conn.state = HttpState.Closing
|
||||
var pending: seq[Future[void]]
|
||||
pending.add(conn.writer.closeWait())
|
||||
pending.add(conn.reader.closeWait())
|
||||
try:
|
||||
await allFutures(pending)
|
||||
except CancelledError:
|
||||
await allFutures(pending)
|
||||
# After we going to close everything else.
|
||||
pending.setLen(3)
|
||||
pending[0] = conn.mainReader.closeWait()
|
||||
pending[1] = conn.mainWriter.closeWait()
|
||||
pending[2] = conn.transp.closeWait()
|
||||
try:
|
||||
await allFutures(pending)
|
||||
except CancelledError:
|
||||
await allFutures(pending)
|
||||
pending.add(conn.mainReader.closeWait())
|
||||
pending.add(conn.mainWriter.closeWait())
|
||||
pending.add(conn.transp.closeWait())
|
||||
await noCancel(allFutures(pending))
|
||||
reset(cast[SecureHttpConnectionRef](conn)[])
|
||||
untrackCounter(HttpServerSecureConnectionTrackerName)
|
||||
conn.state = HttpState.Closed
|
||||
|
||||
proc new*(ht: typedesc[SecureHttpConnectionRef], server: SecureHttpServerRef,
|
||||
transp: StreamTransport): SecureHttpConnectionRef =
|
||||
proc new(ht: typedesc[SecureHttpConnectionRef], server: SecureHttpServerRef,
|
||||
transp: StreamTransport): Result[SecureHttpConnectionRef, string] =
|
||||
var res = SecureHttpConnectionRef()
|
||||
HttpConnection(res[]).init(HttpServerRef(server), transp)
|
||||
let tlsStream =
|
||||
newTLSServerAsyncStream(res.mainReader, res.mainWriter,
|
||||
server.tlsPrivateKey,
|
||||
server.tlsCertificate,
|
||||
minVersion = TLSVersion.TLS12,
|
||||
flags = server.secureFlags)
|
||||
try:
|
||||
newTLSServerAsyncStream(res.mainReader, res.mainWriter,
|
||||
server.tlsPrivateKey,
|
||||
server.tlsCertificate,
|
||||
minVersion = TLSVersion.TLS12,
|
||||
flags = server.secureFlags)
|
||||
except TLSStreamError as exc:
|
||||
return err(exc.msg)
|
||||
res.tlsStream = tlsStream
|
||||
res.reader = AsyncStreamReader(tlsStream.reader)
|
||||
res.writer = AsyncStreamWriter(tlsStream.writer)
|
||||
res.closeCb = closeSecConnection
|
||||
trackCounter(HttpServerSecureConnectionTrackerName)
|
||||
res
|
||||
ok(res)
|
||||
|
||||
proc createSecConnection(server: HttpServerRef,
|
||||
transp: StreamTransport): Future[HttpConnectionRef] {.
|
||||
async.} =
|
||||
let secureServ = cast[SecureHttpServerRef](server)
|
||||
var sconn = SecureHttpConnectionRef.new(secureServ, transp)
|
||||
async: (raises: [CancelledError, HttpConnectionError]).} =
|
||||
let
|
||||
secureServ = cast[SecureHttpServerRef](server)
|
||||
sconn = SecureHttpConnectionRef.new(secureServ, transp).valueOr:
|
||||
raiseHttpConnectionError(error)
|
||||
|
||||
try:
|
||||
await handshake(sconn.tlsStream)
|
||||
return HttpConnectionRef(sconn)
|
||||
HttpConnectionRef(sconn)
|
||||
except CancelledError as exc:
|
||||
await HttpConnectionRef(sconn).closeWait()
|
||||
raise exc
|
||||
except TLSStreamError:
|
||||
except AsyncStreamError as exc:
|
||||
await HttpConnectionRef(sconn).closeWait()
|
||||
raiseHttpCriticalError("Unable to establish secure connection")
|
||||
let msg = "Unable to establish secure connection, reason: " & $exc.msg
|
||||
raiseHttpConnectionError(msg)
|
||||
|
||||
proc new*(htype: typedesc[SecureHttpServerRef],
|
||||
address: TransportAddress,
|
||||
processCallback: HttpProcessCallback2,
|
||||
tlsPrivateKey: TLSPrivateKey,
|
||||
tlsCertificate: TLSCertificate,
|
||||
serverFlags: set[HttpServerFlags] = {},
|
||||
socketFlags: set[ServerFlags] = {ReuseAddr},
|
||||
serverUri = Uri(),
|
||||
serverIdent = "",
|
||||
secureFlags: set[TLSFlags] = {},
|
||||
maxConnections: int = -1,
|
||||
bufferSize: int = chronosTransportDefaultBufferSize,
|
||||
backlogSize: int = DefaultBacklogSize,
|
||||
httpHeadersTimeout = 10.seconds,
|
||||
maxHeadersSize: int = 8192,
|
||||
maxRequestBodySize: int = 1_048_576,
|
||||
dualstack = DualStackType.Auto
|
||||
): HttpResult[SecureHttpServerRef] =
|
||||
doAssert(not(isNil(tlsPrivateKey)), "TLS private key must not be nil!")
|
||||
doAssert(not(isNil(tlsCertificate)), "TLS certificate must not be nil!")
|
||||
let
|
||||
serverInstance =
|
||||
try:
|
||||
createStreamServer(address, flags = socketFlags,
|
||||
bufferSize = bufferSize,
|
||||
backlog = backlogSize, dualstack = dualstack)
|
||||
except TransportOsError as exc:
|
||||
return err(exc.msg)
|
||||
serverUri =
|
||||
if len(serverUri.hostname) > 0:
|
||||
serverUri
|
||||
else:
|
||||
parseUri("https://" & $serverInstance.localAddress() & "/")
|
||||
res = SecureHttpServerRef(
|
||||
address: serverInstance.localAddress(),
|
||||
instance: serverInstance,
|
||||
processCallback: processCallback,
|
||||
createConnCallback: createSecConnection,
|
||||
baseUri: serverUri,
|
||||
serverIdent: serverIdent,
|
||||
flags: serverFlags + {HttpServerFlags.Secure},
|
||||
socketFlags: socketFlags,
|
||||
maxConnections: maxConnections,
|
||||
bufferSize: bufferSize,
|
||||
backlogSize: backlogSize,
|
||||
headersTimeout: httpHeadersTimeout,
|
||||
maxHeadersSize: maxHeadersSize,
|
||||
maxRequestBodySize: maxRequestBodySize,
|
||||
# semaphore:
|
||||
# if maxConnections > 0:
|
||||
# newAsyncSemaphore(maxConnections)
|
||||
# else:
|
||||
# nil
|
||||
lifetime: newFuture[void]("http.server.lifetime"),
|
||||
connections: initOrderedTable[string, HttpConnectionHolderRef](),
|
||||
tlsCertificate: tlsCertificate,
|
||||
tlsPrivateKey: tlsPrivateKey,
|
||||
secureFlags: secureFlags
|
||||
)
|
||||
ok(res)
|
||||
|
||||
proc new*(htype: typedesc[SecureHttpServerRef],
|
||||
address: TransportAddress,
|
||||
@ -89,58 +152,40 @@ proc new*(htype: typedesc[SecureHttpServerRef],
|
||||
serverIdent = "",
|
||||
secureFlags: set[TLSFlags] = {},
|
||||
maxConnections: int = -1,
|
||||
bufferSize: int = 4096,
|
||||
backlogSize: int = 100,
|
||||
bufferSize: int = chronosTransportDefaultBufferSize,
|
||||
backlogSize: int = DefaultBacklogSize,
|
||||
httpHeadersTimeout = 10.seconds,
|
||||
maxHeadersSize: int = 8192,
|
||||
maxRequestBodySize: int = 1_048_576
|
||||
): HttpResult[SecureHttpServerRef] {.raises: [].} =
|
||||
maxRequestBodySize: int = 1_048_576,
|
||||
dualstack = DualStackType.Auto
|
||||
): HttpResult[SecureHttpServerRef] {.
|
||||
deprecated: "Callback could raise only CancelledError, annotate with " &
|
||||
"{.async: (raises: [CancelledError]).}".} =
|
||||
|
||||
doAssert(not(isNil(tlsPrivateKey)), "TLS private key must not be nil!")
|
||||
doAssert(not(isNil(tlsCertificate)), "TLS certificate must not be nil!")
|
||||
|
||||
let serverUri =
|
||||
if len(serverUri.hostname) > 0:
|
||||
serverUri
|
||||
else:
|
||||
try:
|
||||
parseUri("https://" & $address & "/")
|
||||
except TransportAddressError as exc:
|
||||
return err(exc.msg)
|
||||
|
||||
let serverInstance =
|
||||
proc wrap(req: RequestFence): Future[HttpResponseRef] {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
try:
|
||||
createStreamServer(address, flags = socketFlags, bufferSize = bufferSize,
|
||||
backlog = backlogSize)
|
||||
except TransportOsError as exc:
|
||||
return err(exc.msg)
|
||||
await processCallback(req)
|
||||
except CancelledError as exc:
|
||||
raise exc
|
||||
except CatchableError as exc:
|
||||
return err(exc.msg)
|
||||
defaultResponse(exc)
|
||||
|
||||
let res = SecureHttpServerRef(
|
||||
address: address,
|
||||
instance: serverInstance,
|
||||
processCallback: processCallback,
|
||||
createConnCallback: createSecConnection,
|
||||
baseUri: serverUri,
|
||||
serverIdent: serverIdent,
|
||||
flags: serverFlags + {HttpServerFlags.Secure},
|
||||
socketFlags: socketFlags,
|
||||
maxConnections: maxConnections,
|
||||
bufferSize: bufferSize,
|
||||
backlogSize: backlogSize,
|
||||
headersTimeout: httpHeadersTimeout,
|
||||
maxHeadersSize: maxHeadersSize,
|
||||
maxRequestBodySize: maxRequestBodySize,
|
||||
# semaphore:
|
||||
# if maxConnections > 0:
|
||||
# newAsyncSemaphore(maxConnections)
|
||||
# else:
|
||||
# nil
|
||||
lifetime: newFuture[void]("http.server.lifetime"),
|
||||
connections: initOrderedTable[string, HttpConnectionHolderRef](),
|
||||
tlsCertificate: tlsCertificate,
|
||||
tlsPrivateKey: tlsPrivateKey,
|
||||
secureFlags: secureFlags
|
||||
SecureHttpServerRef.new(
|
||||
address = address,
|
||||
processCallback = wrap,
|
||||
tlsPrivateKey = tlsPrivateKey,
|
||||
tlsCertificate = tlsCertificate,
|
||||
serverFlags = serverFlags,
|
||||
socketFlags = socketFlags,
|
||||
serverUri = serverUri,
|
||||
serverIdent = serverIdent,
|
||||
secureFlags = secureFlags,
|
||||
maxConnections = maxConnections,
|
||||
bufferSize = bufferSize,
|
||||
backlogSize = backlogSize,
|
||||
httpHeadersTimeout = httpHeadersTimeout,
|
||||
maxHeadersSize = maxHeadersSize,
|
||||
maxRequestBodySize = maxRequestBodySize,
|
||||
dualstack = dualstack
|
||||
)
|
||||
ok(res)
|
||||
|
||||
@ -1,983 +0,0 @@
|
||||
#
|
||||
# Chronos
|
||||
#
|
||||
# (c) Copyright 2015 Dominik Picheta
|
||||
# (c) Copyright 2018-2023 Status Research & Development GmbH
|
||||
#
|
||||
# Licensed under either of
|
||||
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
||||
# MIT license (LICENSE-MIT)
|
||||
|
||||
import std/sequtils
|
||||
import stew/base10
|
||||
|
||||
when chronosStackTrace:
|
||||
when defined(nimHasStacktracesModule):
|
||||
import system/stacktraces
|
||||
else:
|
||||
const
|
||||
reraisedFromBegin = -10
|
||||
reraisedFromEnd = -100
|
||||
|
||||
template LocCreateIndex*: auto {.deprecated: "LocationKind.Create".} =
|
||||
LocationKind.Create
|
||||
template LocFinishIndex*: auto {.deprecated: "LocationKind.Finish".} =
|
||||
LocationKind.Finish
|
||||
template LocCompleteIndex*: untyped {.deprecated: "LocationKind.Finish".} =
|
||||
LocationKind.Finish
|
||||
|
||||
func `[]`*(loc: array[LocationKind, ptr SrcLoc], v: int): ptr SrcLoc {.deprecated: "use LocationKind".} =
|
||||
case v
|
||||
of 0: loc[LocationKind.Create]
|
||||
of 1: loc[LocationKind.Finish]
|
||||
else: raiseAssert("Unknown source location " & $v)
|
||||
|
||||
type
|
||||
FutureStr*[T] = ref object of Future[T]
|
||||
## Future to hold GC strings
|
||||
gcholder*: string
|
||||
|
||||
FutureSeq*[A, B] = ref object of Future[A]
|
||||
## Future to hold GC seqs
|
||||
gcholder*: seq[B]
|
||||
|
||||
# Backwards compatibility for old FutureState name
|
||||
template Finished* {.deprecated: "Use Completed instead".} = Completed
|
||||
template Finished*(T: type FutureState): FutureState {.deprecated: "Use FutureState.Completed instead".} = FutureState.Completed
|
||||
|
||||
proc newFutureImpl[T](loc: ptr SrcLoc): Future[T] =
|
||||
let fut = Future[T]()
|
||||
internalInitFutureBase(fut, loc, FutureState.Pending)
|
||||
fut
|
||||
|
||||
proc newFutureSeqImpl[A, B](loc: ptr SrcLoc): FutureSeq[A, B] =
|
||||
let fut = FutureSeq[A, B]()
|
||||
internalInitFutureBase(fut, loc, FutureState.Pending)
|
||||
fut
|
||||
|
||||
proc newFutureStrImpl[T](loc: ptr SrcLoc): FutureStr[T] =
|
||||
let fut = FutureStr[T]()
|
||||
internalInitFutureBase(fut, loc, FutureState.Pending)
|
||||
fut
|
||||
|
||||
template newFuture*[T](fromProc: static[string] = ""): Future[T] =
|
||||
## Creates a new future.
|
||||
##
|
||||
## Specifying ``fromProc``, which is a string specifying the name of the proc
|
||||
## that this future belongs to, is a good habit as it helps with debugging.
|
||||
newFutureImpl[T](getSrcLocation(fromProc))
|
||||
|
||||
template newFutureSeq*[A, B](fromProc: static[string] = ""): FutureSeq[A, B] =
|
||||
## Create a new future which can hold/preserve GC sequence until future will
|
||||
## not be completed.
|
||||
##
|
||||
## Specifying ``fromProc``, which is a string specifying the name of the proc
|
||||
## that this future belongs to, is a good habit as it helps with debugging.
|
||||
newFutureSeqImpl[A, B](getSrcLocation(fromProc))
|
||||
|
||||
template newFutureStr*[T](fromProc: static[string] = ""): FutureStr[T] =
|
||||
## Create a new future which can hold/preserve GC string until future will
|
||||
## not be completed.
|
||||
##
|
||||
## Specifying ``fromProc``, which is a string specifying the name of the proc
|
||||
## that this future belongs to, is a good habit as it helps with debugging.
|
||||
newFutureStrImpl[T](getSrcLocation(fromProc))
|
||||
|
||||
proc done*(future: FutureBase): bool {.deprecated: "Use `completed` instead".} =
|
||||
## This is an alias for ``completed(future)`` procedure.
|
||||
completed(future)
|
||||
|
||||
when chronosFutureTracking:
|
||||
proc futureDestructor(udata: pointer) =
|
||||
## This procedure will be called when Future[T] got completed, cancelled or
|
||||
## failed and all Future[T].callbacks are already scheduled and processed.
|
||||
let future = cast[FutureBase](udata)
|
||||
if future == futureList.tail: futureList.tail = future.prev
|
||||
if future == futureList.head: futureList.head = future.next
|
||||
if not(isNil(future.next)): future.next.internalPrev = future.prev
|
||||
if not(isNil(future.prev)): future.prev.internalNext = future.next
|
||||
futureList.count.dec()
|
||||
|
||||
proc scheduleDestructor(future: FutureBase) {.inline.} =
|
||||
callSoon(futureDestructor, cast[pointer](future))
|
||||
|
||||
proc checkFinished(future: FutureBase, loc: ptr SrcLoc) =
|
||||
## Checks whether `future` is finished. If it is then raises a
|
||||
## ``FutureDefect``.
|
||||
if future.finished():
|
||||
var msg = ""
|
||||
msg.add("An attempt was made to complete a Future more than once. ")
|
||||
msg.add("Details:")
|
||||
msg.add("\n Future ID: " & Base10.toString(future.id))
|
||||
msg.add("\n Creation location:")
|
||||
msg.add("\n " & $future.location[LocationKind.Create])
|
||||
msg.add("\n First completion location:")
|
||||
msg.add("\n " & $future.location[LocationKind.Finish])
|
||||
msg.add("\n Second completion location:")
|
||||
msg.add("\n " & $loc)
|
||||
when chronosStackTrace:
|
||||
msg.add("\n Stack trace to moment of creation:")
|
||||
msg.add("\n" & indent(future.stackTrace.strip(), 4))
|
||||
msg.add("\n Stack trace to moment of secondary completion:")
|
||||
msg.add("\n" & indent(getStackTrace().strip(), 4))
|
||||
msg.add("\n\n")
|
||||
var err = newException(FutureDefect, msg)
|
||||
err.cause = future
|
||||
raise err
|
||||
else:
|
||||
future.internalLocation[LocationKind.Finish] = loc
|
||||
|
||||
proc finish(fut: FutureBase, state: FutureState) =
|
||||
# We do not perform any checks here, because:
|
||||
# 1. `finish()` is a private procedure and `state` is under our control.
|
||||
# 2. `fut.state` is checked by `checkFinished()`.
|
||||
fut.internalState = state
|
||||
when chronosStrictFutureAccess:
|
||||
doAssert fut.internalCancelcb == nil or state != FutureState.Cancelled
|
||||
fut.internalCancelcb = nil # release cancellation callback memory
|
||||
for item in fut.internalCallbacks.mitems():
|
||||
if not(isNil(item.function)):
|
||||
callSoon(item)
|
||||
item = default(AsyncCallback) # release memory as early as possible
|
||||
fut.internalCallbacks = default(seq[AsyncCallback]) # release seq as well
|
||||
|
||||
when chronosFutureTracking:
|
||||
scheduleDestructor(fut)
|
||||
|
||||
proc complete[T](future: Future[T], val: T, loc: ptr SrcLoc) =
|
||||
if not(future.cancelled()):
|
||||
checkFinished(future, loc)
|
||||
doAssert(isNil(future.internalError))
|
||||
future.internalValue = val
|
||||
future.finish(FutureState.Completed)
|
||||
|
||||
template complete*[T](future: Future[T], val: T) =
|
||||
## Completes ``future`` with value ``val``.
|
||||
complete(future, val, getSrcLocation())
|
||||
|
||||
proc complete(future: Future[void], loc: ptr SrcLoc) =
|
||||
if not(future.cancelled()):
|
||||
checkFinished(future, loc)
|
||||
doAssert(isNil(future.internalError))
|
||||
future.finish(FutureState.Completed)
|
||||
|
||||
template complete*(future: Future[void]) =
|
||||
## Completes a void ``future``.
|
||||
complete(future, getSrcLocation())
|
||||
|
||||
proc fail(future: FutureBase, error: ref CatchableError, loc: ptr SrcLoc) =
|
||||
if not(future.cancelled()):
|
||||
checkFinished(future, loc)
|
||||
future.internalError = error
|
||||
when chronosStackTrace:
|
||||
future.internalErrorStackTrace = if getStackTrace(error) == "":
|
||||
getStackTrace()
|
||||
else:
|
||||
getStackTrace(error)
|
||||
future.finish(FutureState.Failed)
|
||||
|
||||
template fail*(future: FutureBase, error: ref CatchableError) =
|
||||
## Completes ``future`` with ``error``.
|
||||
fail(future, error, getSrcLocation())
|
||||
|
||||
template newCancelledError(): ref CancelledError =
|
||||
(ref CancelledError)(msg: "Future operation cancelled!")
|
||||
|
||||
proc cancelAndSchedule(future: FutureBase, loc: ptr SrcLoc) =
|
||||
if not(future.finished()):
|
||||
checkFinished(future, loc)
|
||||
future.internalError = newCancelledError()
|
||||
when chronosStackTrace:
|
||||
future.internalErrorStackTrace = getStackTrace()
|
||||
future.finish(FutureState.Cancelled)
|
||||
|
||||
template cancelAndSchedule*(future: FutureBase) =
|
||||
cancelAndSchedule(future, getSrcLocation())
|
||||
|
||||
proc cancel(future: FutureBase, loc: ptr SrcLoc): bool =
|
||||
## Request that Future ``future`` cancel itself.
|
||||
##
|
||||
## This arranges for a `CancelledError` to be thrown into procedure which
|
||||
## waits for ``future`` on the next cycle through the event loop.
|
||||
## The procedure then has a chance to clean up or even deny the request
|
||||
## using `try/except/finally`.
|
||||
##
|
||||
## This call do not guarantee that the ``future`` will be cancelled: the
|
||||
## exception might be caught and acted upon, delaying cancellation of the
|
||||
## ``future`` or preventing cancellation completely. The ``future`` may also
|
||||
## return value or raise different exception.
|
||||
##
|
||||
## Immediately after this procedure is called, ``future.cancelled()`` will
|
||||
## not return ``true`` (unless the Future was already cancelled).
|
||||
if future.finished():
|
||||
return false
|
||||
|
||||
if not(isNil(future.internalChild)):
|
||||
# If you hit this assertion, you should have used the `CancelledError`
|
||||
# mechanism and/or use a regular `addCallback`
|
||||
when chronosStrictFutureAccess:
|
||||
doAssert future.internalCancelcb.isNil,
|
||||
"futures returned from `{.async.}` functions must not use `cancelCallback`"
|
||||
|
||||
if cancel(future.internalChild, getSrcLocation()):
|
||||
return true
|
||||
|
||||
else:
|
||||
if not(isNil(future.internalCancelcb)):
|
||||
future.internalCancelcb(cast[pointer](future))
|
||||
future.internalCancelcb = nil
|
||||
cancelAndSchedule(future, getSrcLocation())
|
||||
|
||||
future.internalMustCancel = true
|
||||
return true
|
||||
|
||||
template cancel*(future: FutureBase) =
|
||||
## Cancel ``future``.
|
||||
discard cancel(future, getSrcLocation())
|
||||
|
||||
proc clearCallbacks(future: FutureBase) =
|
||||
future.internalCallbacks = default(seq[AsyncCallback])
|
||||
|
||||
proc addCallback*(future: FutureBase, cb: CallbackFunc, udata: pointer) =
|
||||
## Adds the callbacks proc to be called when the future completes.
|
||||
##
|
||||
## If future has already completed then ``cb`` will be called immediately.
|
||||
doAssert(not isNil(cb))
|
||||
if future.finished():
|
||||
callSoon(cb, udata)
|
||||
else:
|
||||
future.internalCallbacks.add AsyncCallback(function: cb, udata: udata)
|
||||
|
||||
proc addCallback*(future: FutureBase, cb: CallbackFunc) =
|
||||
## Adds the callbacks proc to be called when the future completes.
|
||||
##
|
||||
## If future has already completed then ``cb`` will be called immediately.
|
||||
future.addCallback(cb, cast[pointer](future))
|
||||
|
||||
proc removeCallback*(future: FutureBase, cb: CallbackFunc,
|
||||
udata: pointer) =
|
||||
## Remove future from list of callbacks - this operation may be slow if there
|
||||
## are many registered callbacks!
|
||||
doAssert(not isNil(cb))
|
||||
# Make sure to release memory associated with callback, or reference chains
|
||||
# may be created!
|
||||
future.internalCallbacks.keepItIf:
|
||||
it.function != cb or it.udata != udata
|
||||
|
||||
proc removeCallback*(future: FutureBase, cb: CallbackFunc) =
|
||||
future.removeCallback(cb, cast[pointer](future))
|
||||
|
||||
proc `callback=`*(future: FutureBase, cb: CallbackFunc, udata: pointer) =
|
||||
## Clears the list of callbacks and sets the callback proc to be called when
|
||||
## the future completes.
|
||||
##
|
||||
## If future has already completed then ``cb`` will be called immediately.
|
||||
##
|
||||
## It's recommended to use ``addCallback`` or ``then`` instead.
|
||||
# ZAH: how about `setLen(1); callbacks[0] = cb`
|
||||
future.clearCallbacks
|
||||
future.addCallback(cb, udata)
|
||||
|
||||
proc `callback=`*(future: FutureBase, cb: CallbackFunc) =
|
||||
## Sets the callback proc to be called when the future completes.
|
||||
##
|
||||
## If future has already completed then ``cb`` will be called immediately.
|
||||
`callback=`(future, cb, cast[pointer](future))
|
||||
|
||||
proc `cancelCallback=`*(future: FutureBase, cb: CallbackFunc) =
|
||||
## Sets the callback procedure to be called when the future is cancelled.
|
||||
##
|
||||
## This callback will be called immediately as ``future.cancel()`` invoked and
|
||||
## must be set before future is finished.
|
||||
|
||||
when chronosStrictFutureAccess:
|
||||
doAssert not future.finished(),
|
||||
"cancellation callback must be set before finishing the future"
|
||||
future.internalCancelcb = cb
|
||||
|
||||
{.push stackTrace: off.}
|
||||
proc futureContinue*(fut: FutureBase) {.raises: [], gcsafe.}
|
||||
|
||||
proc internalContinue(fut: pointer) {.raises: [], gcsafe.} =
|
||||
let asFut = cast[FutureBase](fut)
|
||||
GC_unref(asFut)
|
||||
futureContinue(asFut)
|
||||
|
||||
proc futureContinue*(fut: FutureBase) {.raises: [], gcsafe.} =
|
||||
# This function is responsible for calling the closure iterator generated by
|
||||
# the `{.async.}` transformation either until it has completed its iteration
|
||||
# or raised and error / been cancelled.
|
||||
#
|
||||
# Every call to an `{.async.}` proc is redirected to call this function
|
||||
# instead with its original body captured in `fut.closure`.
|
||||
var next: FutureBase
|
||||
template iterate =
|
||||
while true:
|
||||
# Call closure to make progress on `fut` until it reaches `yield` (inside
|
||||
# `await` typically) or completes / fails / is cancelled
|
||||
next = fut.internalClosure(fut)
|
||||
if fut.internalClosure.finished(): # Reached the end of the transformed proc
|
||||
break
|
||||
|
||||
if next == nil:
|
||||
raiseAssert "Async procedure (" & ($fut.location[LocationKind.Create]) &
|
||||
") yielded `nil`, are you await'ing a `nil` Future?"
|
||||
|
||||
if not next.finished():
|
||||
# We cannot make progress on `fut` until `next` has finished - schedule
|
||||
# `fut` to continue running when that happens
|
||||
GC_ref(fut)
|
||||
next.addCallback(CallbackFunc(internalContinue), cast[pointer](fut))
|
||||
|
||||
# return here so that we don't remove the closure below
|
||||
return
|
||||
|
||||
# Continue while the yielded future is already finished.
|
||||
|
||||
when chronosStrictException:
|
||||
try:
|
||||
iterate
|
||||
except CancelledError:
|
||||
fut.cancelAndSchedule()
|
||||
except CatchableError as exc:
|
||||
fut.fail(exc)
|
||||
finally:
|
||||
next = nil # GC hygiene
|
||||
else:
|
||||
try:
|
||||
iterate
|
||||
except CancelledError:
|
||||
fut.cancelAndSchedule()
|
||||
except CatchableError as exc:
|
||||
fut.fail(exc)
|
||||
except Exception as exc:
|
||||
if exc of Defect:
|
||||
raise (ref Defect)(exc)
|
||||
|
||||
fut.fail((ref ValueError)(msg: exc.msg, parent: exc))
|
||||
finally:
|
||||
next = nil # GC hygiene
|
||||
|
||||
# `futureContinue` will not be called any more for this future so we can
|
||||
# clean it up
|
||||
fut.internalClosure = nil
|
||||
fut.internalChild = nil
|
||||
|
||||
{.pop.}
|
||||
|
||||
when chronosStackTrace:
|
||||
import std/strutils
|
||||
|
||||
template getFilenameProcname(entry: StackTraceEntry): (string, string) =
|
||||
when compiles(entry.filenameStr) and compiles(entry.procnameStr):
|
||||
# We can't rely on "entry.filename" and "entry.procname" still being valid
|
||||
# cstring pointers, because the "string.data" buffers they pointed to might
|
||||
# be already garbage collected (this entry being a non-shallow copy,
|
||||
# "entry.filename" no longer points to "entry.filenameStr.data", but to the
|
||||
# buffer of the original object).
|
||||
(entry.filenameStr, entry.procnameStr)
|
||||
else:
|
||||
($entry.filename, $entry.procname)
|
||||
|
||||
proc `$`(stackTraceEntries: seq[StackTraceEntry]): string =
|
||||
try:
|
||||
when defined(nimStackTraceOverride) and declared(addDebuggingInfo):
|
||||
let entries = addDebuggingInfo(stackTraceEntries)
|
||||
else:
|
||||
let entries = stackTraceEntries
|
||||
|
||||
# Find longest filename & line number combo for alignment purposes.
|
||||
var longestLeft = 0
|
||||
for entry in entries:
|
||||
let (filename, procname) = getFilenameProcname(entry)
|
||||
|
||||
if procname == "": continue
|
||||
|
||||
let leftLen = filename.len + len($entry.line)
|
||||
if leftLen > longestLeft:
|
||||
longestLeft = leftLen
|
||||
|
||||
var indent = 2
|
||||
# Format the entries.
|
||||
for entry in entries:
|
||||
let (filename, procname) = getFilenameProcname(entry)
|
||||
|
||||
if procname == "":
|
||||
if entry.line == reraisedFromBegin:
|
||||
result.add(spaces(indent) & "#[\n")
|
||||
indent.inc(2)
|
||||
elif entry.line == reraisedFromEnd:
|
||||
indent.dec(2)
|
||||
result.add(spaces(indent) & "]#\n")
|
||||
continue
|
||||
|
||||
let left = "$#($#)" % [filename, $entry.line]
|
||||
result.add((spaces(indent) & "$#$# $#\n") % [
|
||||
left,
|
||||
spaces(longestLeft - left.len + 2),
|
||||
procname
|
||||
])
|
||||
except ValueError as exc:
|
||||
return exc.msg # Shouldn't actually happen since we set the formatting
|
||||
# string
|
||||
|
||||
proc injectStacktrace(error: ref Exception) =
|
||||
const header = "\nAsync traceback:\n"
|
||||
|
||||
var exceptionMsg = error.msg
|
||||
if header in exceptionMsg:
|
||||
# This is messy: extract the original exception message from the msg
|
||||
# containing the async traceback.
|
||||
let start = exceptionMsg.find(header)
|
||||
exceptionMsg = exceptionMsg[0..<start]
|
||||
|
||||
var newMsg = exceptionMsg & header
|
||||
|
||||
let entries = getStackTraceEntries(error)
|
||||
newMsg.add($entries)
|
||||
|
||||
newMsg.add("Exception message: " & exceptionMsg & "\n")
|
||||
|
||||
# # For debugging purposes
|
||||
# newMsg.add("Exception type:")
|
||||
# for entry in getStackTraceEntries(future.error):
|
||||
# newMsg.add "\n" & $entry
|
||||
error.msg = newMsg
|
||||
|
||||
proc internalCheckComplete*(fut: FutureBase) {.raises: [CatchableError].} =
|
||||
# For internal use only. Used in asyncmacro
|
||||
if not(isNil(fut.internalError)):
|
||||
when chronosStackTrace:
|
||||
injectStacktrace(fut.internalError)
|
||||
raise fut.internalError
|
||||
|
||||
proc internalRead*[T](fut: Future[T]): T {.inline.} =
|
||||
# For internal use only. Used in asyncmacro
|
||||
when T isnot void:
|
||||
return fut.internalValue
|
||||
|
||||
proc read*[T](future: Future[T] ): T {.raises: [CatchableError].} =
|
||||
## Retrieves the value of ``future``. Future must be finished otherwise
|
||||
## this function will fail with a ``ValueError`` exception.
|
||||
##
|
||||
## If the result of the future is an error then that error will be raised.
|
||||
if future.finished():
|
||||
internalCheckComplete(future)
|
||||
internalRead(future)
|
||||
else:
|
||||
# TODO: Make a custom exception type for this?
|
||||
raise newException(ValueError, "Future still in progress.")
|
||||
|
||||
proc readError*(future: FutureBase): ref CatchableError {.raises: [ValueError].} =
|
||||
## Retrieves the exception stored in ``future``.
|
||||
##
|
||||
## An ``ValueError`` exception will be thrown if no exception exists
|
||||
## in the specified Future.
|
||||
if not(isNil(future.error)):
|
||||
return future.error
|
||||
else:
|
||||
# TODO: Make a custom exception type for this?
|
||||
raise newException(ValueError, "No error in future.")
|
||||
|
||||
template taskFutureLocation(future: FutureBase): string =
|
||||
let loc = future.location[LocationKind.Create]
|
||||
"[" & (
|
||||
if len(loc.procedure) == 0: "[unspecified]" else: $loc.procedure & "()"
|
||||
) & " at " & $loc.file & ":" & $(loc.line) & "]"
|
||||
|
||||
template taskErrorMessage(future: FutureBase): string =
|
||||
"Asynchronous task " & taskFutureLocation(future) &
|
||||
" finished with an exception \"" & $future.error.name &
|
||||
"\"!\nMessage: " & future.error.msg &
|
||||
"\nStack trace: " & future.error.getStackTrace()
|
||||
template taskCancelMessage(future: FutureBase): string =
|
||||
"Asynchronous task " & taskFutureLocation(future) & " was cancelled!"
|
||||
|
||||
proc asyncSpawn*(future: Future[void]) =
|
||||
## Spawns a new concurrent async task.
|
||||
##
|
||||
## Tasks may not raise exceptions or be cancelled - a ``Defect`` will be
|
||||
## raised when this happens.
|
||||
##
|
||||
## This should be used instead of ``discard`` and ``asyncCheck`` when calling
|
||||
## an ``async`` procedure without ``await``, to ensure exceptions in the
|
||||
## returned future are not silently discarded.
|
||||
##
|
||||
## Note, that if passed ``future`` is already finished, it will be checked
|
||||
## and processed immediately.
|
||||
doAssert(not isNil(future), "Future is nil")
|
||||
|
||||
proc cb(data: pointer) =
|
||||
if future.failed():
|
||||
raise newException(FutureDefect, taskErrorMessage(future))
|
||||
elif future.cancelled():
|
||||
raise newException(FutureDefect, taskCancelMessage(future))
|
||||
|
||||
if not(future.finished()):
|
||||
# We adding completion callback only if ``future`` is not finished yet.
|
||||
future.addCallback(cb)
|
||||
else:
|
||||
cb(nil)
|
||||
|
||||
proc asyncCheck*[T](future: Future[T]) {.
|
||||
deprecated: "Raises Defect on future failure, fix your code and use" &
|
||||
" asyncSpawn!".} =
|
||||
## This function used to raise an exception through the `poll` call if
|
||||
## the given future failed - there's no way to handle such exceptions so this
|
||||
## function is now an alias for `asyncSpawn`
|
||||
##
|
||||
when T is void:
|
||||
asyncSpawn(future)
|
||||
else:
|
||||
proc cb(data: pointer) =
|
||||
if future.failed():
|
||||
raise newException(FutureDefect, taskErrorMessage(future))
|
||||
elif future.cancelled():
|
||||
raise newException(FutureDefect, taskCancelMessage(future))
|
||||
|
||||
if not(future.finished()):
|
||||
# We adding completion callback only if ``future`` is not finished yet.
|
||||
future.addCallback(cb)
|
||||
else:
|
||||
cb(nil)
|
||||
|
||||
proc asyncDiscard*[T](future: Future[T]) {.
|
||||
deprecated: "Use asyncSpawn or `discard await`".} = discard
|
||||
## `asyncDiscard` will discard the outcome of the operation - unlike `discard`
|
||||
## it also throws away exceptions! Use `asyncSpawn` if you're sure your
|
||||
## code doesn't raise exceptions, or `discard await` to ignore successful
|
||||
## outcomes
|
||||
|
||||
proc `and`*[T, Y](fut1: Future[T], fut2: Future[Y]): Future[void] {.
|
||||
deprecated: "Use allFutures[T](varargs[Future[T]])".} =
|
||||
## Returns a future which will complete once both ``fut1`` and ``fut2``
|
||||
## finish.
|
||||
##
|
||||
## If cancelled, ``fut1`` and ``fut2`` futures WILL NOT BE cancelled.
|
||||
var retFuture = newFuture[void]("chronos.`and`")
|
||||
proc cb(data: pointer) =
|
||||
if not(retFuture.finished()):
|
||||
if fut1.finished() and fut2.finished():
|
||||
if cast[pointer](fut1) == data:
|
||||
if fut1.failed():
|
||||
retFuture.fail(fut1.error)
|
||||
else:
|
||||
retFuture.complete()
|
||||
else:
|
||||
if fut2.failed():
|
||||
retFuture.fail(fut2.error)
|
||||
else:
|
||||
retFuture.complete()
|
||||
fut1.callback = cb
|
||||
fut2.callback = cb
|
||||
|
||||
proc cancellation(udata: pointer) =
|
||||
# On cancel we remove all our callbacks only.
|
||||
if not(fut1.finished()):
|
||||
fut1.removeCallback(cb)
|
||||
if not(fut2.finished()):
|
||||
fut2.removeCallback(cb)
|
||||
|
||||
retFuture.cancelCallback = cancellation
|
||||
return retFuture
|
||||
|
||||
proc `or`*[T, Y](fut1: Future[T], fut2: Future[Y]): Future[void] =
|
||||
## Returns a future which will complete once either ``fut1`` or ``fut2``
|
||||
## finish.
|
||||
##
|
||||
## If ``fut1`` or ``fut2`` future is failed, the result future will also be
|
||||
## failed with an error stored in ``fut1`` or ``fut2`` respectively.
|
||||
##
|
||||
## If both ``fut1`` and ``fut2`` future are completed or failed, the result
|
||||
## future will depend on the state of ``fut1`` future. So if ``fut1`` future
|
||||
## is failed, the result future will also be failed, if ``fut1`` future is
|
||||
## completed, the result future will also be completed.
|
||||
##
|
||||
## If cancelled, ``fut1`` and ``fut2`` futures WILL NOT BE cancelled.
|
||||
var retFuture = newFuture[void]("chronos.or")
|
||||
var cb: proc(udata: pointer) {.gcsafe, raises: [].}
|
||||
cb = proc(udata: pointer) {.gcsafe, raises: [].} =
|
||||
if not(retFuture.finished()):
|
||||
var fut = cast[FutureBase](udata)
|
||||
if cast[pointer](fut1) == udata:
|
||||
fut2.removeCallback(cb)
|
||||
else:
|
||||
fut1.removeCallback(cb)
|
||||
if fut.failed():
|
||||
retFuture.fail(fut.error)
|
||||
else:
|
||||
retFuture.complete()
|
||||
|
||||
proc cancellation(udata: pointer) =
|
||||
# On cancel we remove all our callbacks only.
|
||||
if not(fut1.finished()):
|
||||
fut1.removeCallback(cb)
|
||||
if not(fut2.finished()):
|
||||
fut2.removeCallback(cb)
|
||||
|
||||
if fut1.finished():
|
||||
if fut1.failed():
|
||||
retFuture.fail(fut1.error)
|
||||
else:
|
||||
retFuture.complete()
|
||||
return retFuture
|
||||
|
||||
if fut2.finished():
|
||||
if fut2.failed():
|
||||
retFuture.fail(fut2.error)
|
||||
else:
|
||||
retFuture.complete()
|
||||
return retFuture
|
||||
|
||||
fut1.addCallback(cb)
|
||||
fut2.addCallback(cb)
|
||||
|
||||
retFuture.cancelCallback = cancellation
|
||||
return retFuture
|
||||
|
||||
proc all*[T](futs: varargs[Future[T]]): auto {.
|
||||
deprecated: "Use allFutures(varargs[Future[T]])".} =
|
||||
## Returns a future which will complete once all futures in ``futs`` finish.
|
||||
## If the argument is empty, the returned future completes immediately.
|
||||
##
|
||||
## If the awaited futures are not ``Future[void]``, the returned future
|
||||
## will hold the values of all awaited futures in a sequence.
|
||||
##
|
||||
## If the awaited futures *are* ``Future[void]``, this proc returns
|
||||
## ``Future[void]``.
|
||||
##
|
||||
## Note, that if one of the futures in ``futs`` will fail, result of ``all()``
|
||||
## will also be failed with error from failed future.
|
||||
##
|
||||
## TODO: This procedure has bug on handling cancelled futures from ``futs``.
|
||||
## So if future from ``futs`` list become cancelled, what must be returned?
|
||||
## You can't cancel result ``retFuture`` because in such way infinite
|
||||
## recursion will happen.
|
||||
let totalFutures = len(futs)
|
||||
var completedFutures = 0
|
||||
|
||||
# Because we can't capture varargs[T] in closures we need to create copy.
|
||||
var nfuts = @futs
|
||||
|
||||
when T is void:
|
||||
var retFuture = newFuture[void]("chronos.all(void)")
|
||||
proc cb(udata: pointer) =
|
||||
if not(retFuture.finished()):
|
||||
inc(completedFutures)
|
||||
if completedFutures == totalFutures:
|
||||
for nfut in nfuts:
|
||||
if nfut.failed():
|
||||
retFuture.fail(nfut.error)
|
||||
break
|
||||
if not(retFuture.failed()):
|
||||
retFuture.complete()
|
||||
|
||||
for fut in nfuts:
|
||||
fut.addCallback(cb)
|
||||
|
||||
if len(nfuts) == 0:
|
||||
retFuture.complete()
|
||||
|
||||
return retFuture
|
||||
else:
|
||||
var retFuture = newFuture[seq[T]]("chronos.all(T)")
|
||||
var retValues = newSeq[T](totalFutures)
|
||||
|
||||
proc cb(udata: pointer) =
|
||||
if not(retFuture.finished()):
|
||||
inc(completedFutures)
|
||||
if completedFutures == totalFutures:
|
||||
for k, nfut in nfuts:
|
||||
if nfut.failed():
|
||||
retFuture.fail(nfut.error)
|
||||
break
|
||||
else:
|
||||
retValues[k] = nfut.value
|
||||
if not(retFuture.failed()):
|
||||
retFuture.complete(retValues)
|
||||
|
||||
for fut in nfuts:
|
||||
fut.addCallback(cb)
|
||||
|
||||
if len(nfuts) == 0:
|
||||
retFuture.complete(retValues)
|
||||
|
||||
return retFuture
|
||||
|
||||
proc oneIndex*[T](futs: varargs[Future[T]]): Future[int] {.
|
||||
deprecated: "Use one[T](varargs[Future[T]])".} =
|
||||
## Returns a future which will complete once one of the futures in ``futs``
|
||||
## complete.
|
||||
##
|
||||
## If the argument is empty, the returned future FAILS immediately.
|
||||
##
|
||||
## Returned future will hold index of completed/failed future in ``futs``
|
||||
## argument.
|
||||
var nfuts = @futs
|
||||
var retFuture = newFuture[int]("chronos.oneIndex(T)")
|
||||
|
||||
proc cb(udata: pointer) =
|
||||
var res = -1
|
||||
if not(retFuture.finished()):
|
||||
var rfut = cast[FutureBase](udata)
|
||||
for i in 0..<len(nfuts):
|
||||
if cast[FutureBase](nfuts[i]) != rfut:
|
||||
nfuts[i].removeCallback(cb)
|
||||
else:
|
||||
res = i
|
||||
retFuture.complete(res)
|
||||
|
||||
for fut in nfuts:
|
||||
fut.addCallback(cb)
|
||||
|
||||
if len(nfuts) == 0:
|
||||
retFuture.fail(newException(ValueError, "Empty Future[T] list"))
|
||||
|
||||
return retFuture
|
||||
|
||||
proc oneValue*[T](futs: varargs[Future[T]]): Future[T] {.
|
||||
deprecated: "Use one[T](varargs[Future[T]])".} =
|
||||
## Returns a future which will finish once one of the futures in ``futs``
|
||||
## finish.
|
||||
##
|
||||
## If the argument is empty, returned future FAILS immediately.
|
||||
##
|
||||
## Returned future will hold value of completed ``futs`` future, or error
|
||||
## if future was failed.
|
||||
var nfuts = @futs
|
||||
var retFuture = newFuture[T]("chronos.oneValue(T)")
|
||||
|
||||
proc cb(udata: pointer) =
|
||||
var resFut: Future[T]
|
||||
if not(retFuture.finished()):
|
||||
var rfut = cast[FutureBase](udata)
|
||||
for i in 0..<len(nfuts):
|
||||
if cast[FutureBase](nfuts[i]) != rfut:
|
||||
nfuts[i].removeCallback(cb)
|
||||
else:
|
||||
resFut = nfuts[i]
|
||||
if resFut.failed():
|
||||
retFuture.fail(resFut.error)
|
||||
else:
|
||||
when T is void:
|
||||
retFuture.complete()
|
||||
else:
|
||||
retFuture.complete(resFut.read())
|
||||
|
||||
for fut in nfuts:
|
||||
fut.addCallback(cb)
|
||||
|
||||
if len(nfuts) == 0:
|
||||
retFuture.fail(newException(ValueError, "Empty Future[T] list"))
|
||||
|
||||
return retFuture
|
||||
|
||||
proc cancelAndWait*(fut: FutureBase): Future[void] =
|
||||
## Initiate cancellation process for Future ``fut`` and wait until ``fut`` is
|
||||
## done e.g. changes its state (become completed, failed or cancelled).
|
||||
##
|
||||
## If ``fut`` is already finished (completed, failed or cancelled) result
|
||||
## Future[void] object will be returned complete.
|
||||
var retFuture = newFuture[void]("chronos.cancelAndWait(T)")
|
||||
proc continuation(udata: pointer) =
|
||||
if not(retFuture.finished()):
|
||||
retFuture.complete()
|
||||
proc cancellation(udata: pointer) =
|
||||
if not(fut.finished()):
|
||||
fut.removeCallback(continuation)
|
||||
if fut.finished():
|
||||
retFuture.complete()
|
||||
else:
|
||||
fut.addCallback(continuation)
|
||||
retFuture.cancelCallback = cancellation
|
||||
# Initiate cancellation process.
|
||||
fut.cancel()
|
||||
return retFuture
|
||||
|
||||
proc allFutures*(futs: varargs[FutureBase]): Future[void] =
|
||||
## Returns a future which will complete only when all futures in ``futs``
|
||||
## will be completed, failed or canceled.
|
||||
##
|
||||
## If the argument is empty, the returned future COMPLETES immediately.
|
||||
##
|
||||
## On cancel all the awaited futures ``futs`` WILL NOT BE cancelled.
|
||||
var retFuture = newFuture[void]("chronos.allFutures()")
|
||||
let totalFutures = len(futs)
|
||||
var finishedFutures = 0
|
||||
|
||||
# Because we can't capture varargs[T] in closures we need to create copy.
|
||||
var nfuts = @futs
|
||||
|
||||
proc cb(udata: pointer) =
|
||||
if not(retFuture.finished()):
|
||||
inc(finishedFutures)
|
||||
if finishedFutures == totalFutures:
|
||||
retFuture.complete()
|
||||
|
||||
proc cancellation(udata: pointer) =
|
||||
# On cancel we remove all our callbacks only.
|
||||
for i in 0..<len(nfuts):
|
||||
if not(nfuts[i].finished()):
|
||||
nfuts[i].removeCallback(cb)
|
||||
|
||||
for fut in nfuts:
|
||||
if not(fut.finished()):
|
||||
fut.addCallback(cb)
|
||||
else:
|
||||
inc(finishedFutures)
|
||||
|
||||
retFuture.cancelCallback = cancellation
|
||||
if len(nfuts) == 0 or len(nfuts) == finishedFutures:
|
||||
retFuture.complete()
|
||||
|
||||
return retFuture
|
||||
|
||||
proc allFutures*[T](futs: varargs[Future[T]]): Future[void] =
|
||||
## Returns a future which will complete only when all futures in ``futs``
|
||||
## will be completed, failed or canceled.
|
||||
##
|
||||
## If the argument is empty, the returned future COMPLETES immediately.
|
||||
##
|
||||
## On cancel all the awaited futures ``futs`` WILL NOT BE cancelled.
|
||||
# Because we can't capture varargs[T] in closures we need to create copy.
|
||||
var nfuts: seq[FutureBase]
|
||||
for future in futs:
|
||||
nfuts.add(future)
|
||||
allFutures(nfuts)
|
||||
|
||||
proc allFinished*[T](futs: varargs[Future[T]]): Future[seq[Future[T]]] =
|
||||
## Returns a future which will complete only when all futures in ``futs``
|
||||
## will be completed, failed or canceled.
|
||||
##
|
||||
## Returned sequence will hold all the Future[T] objects passed to
|
||||
## ``allFinished`` with the order preserved.
|
||||
##
|
||||
## If the argument is empty, the returned future COMPLETES immediately.
|
||||
##
|
||||
## On cancel all the awaited futures ``futs`` WILL NOT BE cancelled.
|
||||
var retFuture = newFuture[seq[Future[T]]]("chronos.allFinished()")
|
||||
let totalFutures = len(futs)
|
||||
var finishedFutures = 0
|
||||
|
||||
var nfuts = @futs
|
||||
|
||||
proc cb(udata: pointer) =
|
||||
if not(retFuture.finished()):
|
||||
inc(finishedFutures)
|
||||
if finishedFutures == totalFutures:
|
||||
retFuture.complete(nfuts)
|
||||
|
||||
proc cancellation(udata: pointer) =
|
||||
# On cancel we remove all our callbacks only.
|
||||
for fut in nfuts.mitems():
|
||||
if not(fut.finished()):
|
||||
fut.removeCallback(cb)
|
||||
|
||||
for fut in nfuts:
|
||||
if not(fut.finished()):
|
||||
fut.addCallback(cb)
|
||||
else:
|
||||
inc(finishedFutures)
|
||||
|
||||
retFuture.cancelCallback = cancellation
|
||||
if len(nfuts) == 0 or len(nfuts) == finishedFutures:
|
||||
retFuture.complete(nfuts)
|
||||
|
||||
return retFuture
|
||||
|
||||
proc one*[T](futs: varargs[Future[T]]): Future[Future[T]] =
|
||||
## Returns a future which will complete and return completed Future[T] inside,
|
||||
## when one of the futures in ``futs`` will be completed, failed or canceled.
|
||||
##
|
||||
## If the argument is empty, the returned future FAILS immediately.
|
||||
##
|
||||
## On success returned Future will hold finished Future[T].
|
||||
##
|
||||
## On cancel futures in ``futs`` WILL NOT BE cancelled.
|
||||
var retFuture = newFuture[Future[T]]("chronos.one()")
|
||||
|
||||
if len(futs) == 0:
|
||||
retFuture.fail(newException(ValueError, "Empty Future[T] list"))
|
||||
return retFuture
|
||||
|
||||
# If one of the Future[T] already finished we return it as result
|
||||
for fut in futs:
|
||||
if fut.finished():
|
||||
retFuture.complete(fut)
|
||||
return retFuture
|
||||
|
||||
# Because we can't capture varargs[T] in closures we need to create copy.
|
||||
var nfuts = @futs
|
||||
|
||||
var cb: proc(udata: pointer) {.gcsafe, raises: [].}
|
||||
cb = proc(udata: pointer) {.gcsafe, raises: [].} =
|
||||
if not(retFuture.finished()):
|
||||
var res: Future[T]
|
||||
var rfut = cast[FutureBase](udata)
|
||||
for i in 0..<len(nfuts):
|
||||
if cast[FutureBase](nfuts[i]) != rfut:
|
||||
nfuts[i].removeCallback(cb)
|
||||
else:
|
||||
res = nfuts[i]
|
||||
retFuture.complete(res)
|
||||
|
||||
proc cancellation(udata: pointer) =
|
||||
# On cancel we remove all our callbacks only.
|
||||
for i in 0..<len(nfuts):
|
||||
if not(nfuts[i].finished()):
|
||||
nfuts[i].removeCallback(cb)
|
||||
|
||||
for fut in nfuts:
|
||||
fut.addCallback(cb)
|
||||
|
||||
retFuture.cancelCallback = cancellation
|
||||
return retFuture
|
||||
|
||||
proc race*(futs: varargs[FutureBase]): Future[FutureBase] =
|
||||
## Returns a future which will complete and return completed FutureBase,
|
||||
## when one of the futures in ``futs`` will be completed, failed or canceled.
|
||||
##
|
||||
## If the argument is empty, the returned future FAILS immediately.
|
||||
##
|
||||
## On success returned Future will hold finished FutureBase.
|
||||
##
|
||||
## On cancel futures in ``futs`` WILL NOT BE cancelled.
|
||||
let retFuture = newFuture[FutureBase]("chronos.race()")
|
||||
|
||||
if len(futs) == 0:
|
||||
retFuture.fail(newException(ValueError, "Empty Future[T] list"))
|
||||
return retFuture
|
||||
|
||||
# If one of the Future[T] already finished we return it as result
|
||||
for fut in futs:
|
||||
if fut.finished():
|
||||
retFuture.complete(fut)
|
||||
return retFuture
|
||||
|
||||
# Because we can't capture varargs[T] in closures we need to create copy.
|
||||
var nfuts = @futs
|
||||
|
||||
var cb: proc(udata: pointer) {.gcsafe, raises: [].}
|
||||
cb = proc(udata: pointer) {.gcsafe, raises: [].} =
|
||||
if not(retFuture.finished()):
|
||||
var res: FutureBase
|
||||
var rfut = cast[FutureBase](udata)
|
||||
for i in 0..<len(nfuts):
|
||||
if nfuts[i] != rfut:
|
||||
nfuts[i].removeCallback(cb)
|
||||
else:
|
||||
res = nfuts[i]
|
||||
retFuture.complete(res)
|
||||
|
||||
proc cancellation(udata: pointer) =
|
||||
# On cancel we remove all our callbacks only.
|
||||
for i in 0..<len(nfuts):
|
||||
if not(nfuts[i].finished()):
|
||||
nfuts[i].removeCallback(cb)
|
||||
|
||||
for fut in nfuts:
|
||||
fut.addCallback(cb, cast[pointer](fut))
|
||||
|
||||
retFuture.cancelCallback = cancellation
|
||||
|
||||
return retFuture
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,336 +0,0 @@
|
||||
#
|
||||
#
|
||||
# Nim's Runtime Library
|
||||
# (c) Copyright 2015 Dominik Picheta
|
||||
#
|
||||
# See the file "copying.txt", included in this
|
||||
# distribution, for details about the copyright.
|
||||
#
|
||||
|
||||
import std/[macros]
|
||||
|
||||
# `quote do` will ruin line numbers so we avoid it using these helpers
|
||||
proc completeWithResult(fut, baseType: NimNode): NimNode {.compileTime.} =
|
||||
# when `baseType` is void:
|
||||
# complete(`fut`)
|
||||
# else:
|
||||
# complete(`fut`, result)
|
||||
if baseType.eqIdent("void"):
|
||||
# Shortcut if we know baseType at macro expansion time
|
||||
newCall(ident "complete", fut)
|
||||
else:
|
||||
# `baseType` might be generic and resolve to `void`
|
||||
nnkWhenStmt.newTree(
|
||||
nnkElifExpr.newTree(
|
||||
nnkInfix.newTree(ident "is", baseType, ident "void"),
|
||||
newCall(ident "complete", fut)
|
||||
),
|
||||
nnkElseExpr.newTree(
|
||||
newCall(ident "complete", fut, ident "result")
|
||||
)
|
||||
)
|
||||
|
||||
proc completeWithNode(fut, baseType, node: NimNode): NimNode {.compileTime.} =
|
||||
# when typeof(`node`) is void:
|
||||
# `node` # statement / explicit return
|
||||
# -> completeWithResult(fut, baseType)
|
||||
# else: # expression / implicit return
|
||||
# complete(`fut`, `node`)
|
||||
if node.kind == nnkEmpty: # shortcut when known at macro expanstion time
|
||||
completeWithResult(fut, baseType)
|
||||
else:
|
||||
# Handle both expressions and statements - since the type is not know at
|
||||
# macro expansion time, we delegate this choice to a later compilation stage
|
||||
# with `when`.
|
||||
nnkWhenStmt.newTree(
|
||||
nnkElifExpr.newTree(
|
||||
nnkInfix.newTree(
|
||||
ident "is", nnkTypeOfExpr.newTree(node), ident "void"),
|
||||
newStmtList(
|
||||
node,
|
||||
completeWithResult(fut, baseType)
|
||||
)
|
||||
),
|
||||
nnkElseExpr.newTree(
|
||||
newCall(ident "complete", fut, node)
|
||||
)
|
||||
)
|
||||
|
||||
proc processBody(node, fut, baseType: NimNode): NimNode {.compileTime.} =
|
||||
#echo(node.treeRepr)
|
||||
case node.kind
|
||||
of nnkReturnStmt:
|
||||
let
|
||||
res = newNimNode(nnkStmtList, node)
|
||||
res.add completeWithNode(fut, baseType, processBody(node[0], fut, baseType))
|
||||
res.add newNimNode(nnkReturnStmt, node).add(newNilLit())
|
||||
|
||||
res
|
||||
of RoutineNodes-{nnkTemplateDef}:
|
||||
# skip all the nested procedure definitions
|
||||
node
|
||||
else:
|
||||
for i in 0 ..< node.len:
|
||||
# We must not transform nested procedures of any form, otherwise
|
||||
# `fut` will be used for all nested procedures as their own
|
||||
# `retFuture`.
|
||||
node[i] = processBody(node[i], fut, baseType)
|
||||
node
|
||||
|
||||
proc getName(node: NimNode): string {.compileTime.} =
|
||||
case node.kind
|
||||
of nnkSym:
|
||||
return node.strVal
|
||||
of nnkPostfix:
|
||||
return node[1].strVal
|
||||
of nnkIdent:
|
||||
return node.strVal
|
||||
of nnkEmpty:
|
||||
return "anonymous"
|
||||
else:
|
||||
error("Unknown name.")
|
||||
|
||||
macro unsupported(s: static[string]): untyped =
|
||||
error s
|
||||
|
||||
proc params2(someProc: NimNode): NimNode =
|
||||
# until https://github.com/nim-lang/Nim/pull/19563 is available
|
||||
if someProc.kind == nnkProcTy:
|
||||
someProc[0]
|
||||
else:
|
||||
params(someProc)
|
||||
|
||||
proc cleanupOpenSymChoice(node: NimNode): NimNode {.compileTime.} =
|
||||
# Replace every Call -> OpenSymChoice by a Bracket expr
|
||||
# ref https://github.com/nim-lang/Nim/issues/11091
|
||||
if node.kind in nnkCallKinds and
|
||||
node[0].kind == nnkOpenSymChoice and node[0].eqIdent("[]"):
|
||||
result = newNimNode(nnkBracketExpr)
|
||||
for child in node[1..^1]:
|
||||
result.add(cleanupOpenSymChoice(child))
|
||||
else:
|
||||
result = node.copyNimNode()
|
||||
for child in node:
|
||||
result.add(cleanupOpenSymChoice(child))
|
||||
|
||||
proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
|
||||
## This macro transforms a single procedure into a closure iterator.
|
||||
## The ``async`` macro supports a stmtList holding multiple async procedures.
|
||||
if prc.kind notin {nnkProcTy, nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}:
|
||||
error("Cannot transform " & $prc.kind & " into an async proc." &
|
||||
" proc/method definition or lambda node expected.", prc)
|
||||
|
||||
let returnType = cleanupOpenSymChoice(prc.params2[0])
|
||||
|
||||
# Verify that the return type is a Future[T]
|
||||
let baseType =
|
||||
if returnType.kind == nnkEmpty:
|
||||
ident "void"
|
||||
elif not (
|
||||
returnType.kind == nnkBracketExpr and eqIdent(returnType[0], "Future")):
|
||||
error(
|
||||
"Expected return type of 'Future' got '" & repr(returnType) & "'", prc)
|
||||
return
|
||||
else:
|
||||
returnType[1]
|
||||
|
||||
let
|
||||
baseTypeIsVoid = baseType.eqIdent("void")
|
||||
futureVoidType = nnkBracketExpr.newTree(ident "Future", ident "void")
|
||||
|
||||
if prc.kind in {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}:
|
||||
let
|
||||
prcName = prc.name.getName
|
||||
outerProcBody = newNimNode(nnkStmtList, prc.body)
|
||||
|
||||
# Copy comment for nimdoc
|
||||
if prc.body.len > 0 and prc.body[0].kind == nnkCommentStmt:
|
||||
outerProcBody.add(prc.body[0])
|
||||
|
||||
let
|
||||
internalFutureSym = ident "chronosInternalRetFuture"
|
||||
internalFutureType =
|
||||
if baseTypeIsVoid: futureVoidType
|
||||
else: returnType
|
||||
castFutureSym = nnkCast.newTree(internalFutureType, internalFutureSym)
|
||||
|
||||
procBody = prc.body.processBody(castFutureSym, baseType)
|
||||
|
||||
# don't do anything with forward bodies (empty)
|
||||
if procBody.kind != nnkEmpty:
|
||||
let
|
||||
# fix #13899, `defer` should not escape its original scope
|
||||
procBodyBlck = nnkBlockStmt.newTree(newEmptyNode(), procBody)
|
||||
|
||||
resultDecl = nnkWhenStmt.newTree(
|
||||
# when `baseType` is void:
|
||||
nnkElifExpr.newTree(
|
||||
nnkInfix.newTree(ident "is", baseType, ident "void"),
|
||||
quote do:
|
||||
template result: auto {.used.} =
|
||||
{.fatal: "You should not reference the `result` variable inside" &
|
||||
" a void async proc".}
|
||||
),
|
||||
# else:
|
||||
nnkElseExpr.newTree(
|
||||
newStmtList(
|
||||
quote do: {.push warning[resultshadowed]: off.},
|
||||
# var result {.used.}: `baseType`
|
||||
# In the proc body, result may or may not end up being used
|
||||
# depending on how the body is written - with implicit returns /
|
||||
# expressions in particular, it is likely but not guaranteed that
|
||||
# it is not used. Ideally, we would avoid emitting it in this
|
||||
# case to avoid the default initializaiton. {.used.} typically
|
||||
# works better than {.push.} which has a tendency to leak out of
|
||||
# scope.
|
||||
# TODO figure out if there's a way to detect `result` usage in
|
||||
# the proc body _after_ template exapnsion, and therefore
|
||||
# avoid creating this variable - one option is to create an
|
||||
# addtional when branch witha fake `result` and check
|
||||
# `compiles(procBody)` - this is not without cost though
|
||||
nnkVarSection.newTree(nnkIdentDefs.newTree(
|
||||
nnkPragmaExpr.newTree(
|
||||
ident "result",
|
||||
nnkPragma.newTree(ident "used")),
|
||||
baseType, newEmptyNode())
|
||||
),
|
||||
quote do: {.pop.},
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
completeDecl = completeWithNode(castFutureSym, baseType, procBodyBlck)
|
||||
|
||||
closureBody = newStmtList(resultDecl, completeDecl)
|
||||
|
||||
internalFutureParameter = nnkIdentDefs.newTree(
|
||||
internalFutureSym, newIdentNode("FutureBase"), newEmptyNode())
|
||||
iteratorNameSym = genSym(nskIterator, $prcName)
|
||||
closureIterator = newProc(
|
||||
iteratorNameSym,
|
||||
[newIdentNode("FutureBase"), internalFutureParameter],
|
||||
closureBody, nnkIteratorDef)
|
||||
|
||||
iteratorNameSym.copyLineInfo(prc)
|
||||
|
||||
closureIterator.pragma = newNimNode(nnkPragma, lineInfoFrom=prc.body)
|
||||
closureIterator.addPragma(newIdentNode("closure"))
|
||||
|
||||
# `async` code must be gcsafe
|
||||
closureIterator.addPragma(newIdentNode("gcsafe"))
|
||||
|
||||
# TODO when push raises is active in a module, the iterator here inherits
|
||||
# that annotation - here we explicitly disable it again which goes
|
||||
# against the spirit of the raises annotation - one should investigate
|
||||
# here the possibility of transporting more specific error types here
|
||||
# for example by casting exceptions coming out of `await`..
|
||||
let raises = nnkBracket.newTree()
|
||||
when chronosStrictException:
|
||||
raises.add(newIdentNode("CatchableError"))
|
||||
else:
|
||||
raises.add(newIdentNode("Exception"))
|
||||
|
||||
closureIterator.addPragma(nnkExprColonExpr.newTree(
|
||||
newIdentNode("raises"),
|
||||
raises
|
||||
))
|
||||
|
||||
# If proc has an explicit gcsafe pragma, we add it to iterator as well.
|
||||
# TODO if these lines are not here, srcloc tests fail (!)
|
||||
if prc.pragma.findChild(it.kind in {nnkSym, nnkIdent} and
|
||||
it.strVal == "gcsafe") != nil:
|
||||
closureIterator.addPragma(newIdentNode("gcsafe"))
|
||||
|
||||
outerProcBody.add(closureIterator)
|
||||
|
||||
# -> let resultFuture = newFuture[T]()
|
||||
# declared at the end to be sure that the closure
|
||||
# doesn't reference it, avoid cyclic ref (#203)
|
||||
let
|
||||
retFutureSym = ident "resultFuture"
|
||||
retFutureSym.copyLineInfo(prc)
|
||||
# Do not change this code to `quote do` version because `instantiationInfo`
|
||||
# will be broken for `newFuture()` call.
|
||||
outerProcBody.add(
|
||||
newLetStmt(
|
||||
retFutureSym,
|
||||
newCall(newTree(nnkBracketExpr, ident "newFuture", baseType),
|
||||
newLit(prcName))
|
||||
)
|
||||
)
|
||||
# -> resultFuture.internalClosure = iterator
|
||||
outerProcBody.add(
|
||||
newAssignment(
|
||||
newDotExpr(retFutureSym, newIdentNode("internalClosure")),
|
||||
iteratorNameSym)
|
||||
)
|
||||
|
||||
# -> futureContinue(resultFuture))
|
||||
outerProcBody.add(
|
||||
newCall(newIdentNode("futureContinue"), retFutureSym)
|
||||
)
|
||||
|
||||
# -> return resultFuture
|
||||
outerProcBody.add newNimNode(nnkReturnStmt, prc.body[^1]).add(retFutureSym)
|
||||
|
||||
prc.body = outerProcBody
|
||||
|
||||
if prc.kind notin {nnkProcTy, nnkLambda}: # TODO: Nim bug?
|
||||
prc.addPragma(newColonExpr(ident "stackTrace", ident "off"))
|
||||
|
||||
# See **Remark 435** in this file.
|
||||
# https://github.com/nim-lang/RFCs/issues/435
|
||||
prc.addPragma(newIdentNode("gcsafe"))
|
||||
|
||||
prc.addPragma(nnkExprColonExpr.newTree(
|
||||
newIdentNode("raises"),
|
||||
nnkBracket.newTree()
|
||||
))
|
||||
|
||||
if baseTypeIsVoid:
|
||||
if returnType.kind == nnkEmpty:
|
||||
# Add Future[void]
|
||||
prc.params2[0] = futureVoidType
|
||||
|
||||
prc
|
||||
|
||||
template await*[T](f: Future[T]): untyped =
|
||||
when declared(chronosInternalRetFuture):
|
||||
chronosInternalRetFuture.internalChild = f
|
||||
# `futureContinue` calls the iterator generated by the `async`
|
||||
# transformation - `yield` gives control back to `futureContinue` which is
|
||||
# responsible for resuming execution once the yielded future is finished
|
||||
yield chronosInternalRetFuture.internalChild
|
||||
|
||||
# `child` is guaranteed to have been `finished` after the yield
|
||||
if chronosInternalRetFuture.internalMustCancel:
|
||||
raise newCancelledError()
|
||||
|
||||
# `child` released by `futureContinue`
|
||||
chronosInternalRetFuture.internalChild.internalCheckComplete()
|
||||
when T isnot void:
|
||||
cast[type(f)](chronosInternalRetFuture.internalChild).internalRead()
|
||||
else:
|
||||
unsupported "await is only available within {.async.}"
|
||||
|
||||
template awaitne*[T](f: Future[T]): Future[T] =
|
||||
when declared(chronosInternalRetFuture):
|
||||
chronosInternalRetFuture.internalChild = f
|
||||
yield chronosInternalRetFuture.internalChild
|
||||
if chronosInternalRetFuture.internalMustCancel:
|
||||
raise newCancelledError()
|
||||
cast[type(f)](chronosInternalRetFuture.internalChild)
|
||||
else:
|
||||
unsupported "awaitne is only available within {.async.}"
|
||||
|
||||
macro async*(prc: untyped): untyped =
|
||||
## Macro which processes async procedures into the appropriate
|
||||
## iterators and yield statements.
|
||||
if prc.kind == nnkStmtList:
|
||||
result = newStmtList()
|
||||
for oneProc in prc:
|
||||
result.add asyncSingleProc(oneProc)
|
||||
else:
|
||||
result = asyncSingleProc(prc)
|
||||
when chronosDumpAsync:
|
||||
echo repr result
|
||||
@ -13,7 +13,7 @@
|
||||
import std/strtabs
|
||||
import "."/[config, asyncloop, handles, osdefs, osutils, oserrno],
|
||||
streams/asyncstream
|
||||
import stew/[results, byteutils]
|
||||
import stew/[byteutils], results
|
||||
from std/os import quoteShell, quoteShellWindows, quoteShellPosix, envPairs
|
||||
|
||||
export strtabs, results
|
||||
@ -24,7 +24,8 @@ const
|
||||
## AsyncProcess leaks tracker name
|
||||
|
||||
type
|
||||
AsyncProcessError* = object of CatchableError
|
||||
AsyncProcessError* = object of AsyncError
|
||||
AsyncProcessTimeoutError* = object of AsyncProcessError
|
||||
|
||||
AsyncProcessResult*[T] = Result[T, OSErrorCode]
|
||||
|
||||
@ -107,6 +108,9 @@ type
|
||||
stdError*: string
|
||||
status*: int
|
||||
|
||||
WaitOperation {.pure.} = enum
|
||||
Kill, Terminate
|
||||
|
||||
template Pipe*(t: typedesc[AsyncProcess]): ProcessStreamHandle =
|
||||
ProcessStreamHandle(kind: ProcessStreamHandleKind.Auto)
|
||||
|
||||
@ -227,8 +231,9 @@ proc closeProcessHandles(pipes: var AsyncProcessPipes,
|
||||
lastError: OSErrorCode): OSErrorCode {.apforward.}
|
||||
proc closeProcessStreams(pipes: AsyncProcessPipes,
|
||||
options: set[AsyncProcessOption]): Future[void] {.
|
||||
apforward.}
|
||||
proc closeWait(holder: AsyncStreamHolder): Future[void] {.apforward.}
|
||||
async: (raises: []).}
|
||||
proc closeWait(holder: AsyncStreamHolder): Future[void] {.
|
||||
async: (raises: []).}
|
||||
|
||||
template isOk(code: OSErrorCode): bool =
|
||||
when defined(windows):
|
||||
@ -294,6 +299,11 @@ proc raiseAsyncProcessError(msg: string, exc: ref CatchableError = nil) {.
|
||||
msg & " ([" & $exc.name & "]: " & $exc.msg & ")"
|
||||
raise newException(AsyncProcessError, message)
|
||||
|
||||
proc raiseAsyncProcessTimeoutError() {.
|
||||
noreturn, noinit, noinline, raises: [AsyncProcessTimeoutError].} =
|
||||
let message = "Operation timed out"
|
||||
raise newException(AsyncProcessTimeoutError, message)
|
||||
|
||||
proc raiseAsyncProcessError(msg: string, error: OSErrorCode|cint) {.
|
||||
noreturn, noinit, noinline, raises: [AsyncProcessError].} =
|
||||
when error is OSErrorCode:
|
||||
@ -382,7 +392,8 @@ when defined(windows):
|
||||
stdinHandle = ProcessStreamHandle(),
|
||||
stdoutHandle = ProcessStreamHandle(),
|
||||
stderrHandle = ProcessStreamHandle(),
|
||||
): Future[AsyncProcessRef] {.async.} =
|
||||
): Future[AsyncProcessRef] {.
|
||||
async: (raises: [AsyncProcessError, CancelledError]).} =
|
||||
var
|
||||
pipes = preparePipes(options, stdinHandle, stdoutHandle,
|
||||
stderrHandle).valueOr:
|
||||
@ -508,14 +519,16 @@ when defined(windows):
|
||||
ok(false)
|
||||
|
||||
proc waitForExit*(p: AsyncProcessRef,
|
||||
timeout = InfiniteDuration): Future[int] {.async.} =
|
||||
timeout = InfiniteDuration): Future[int] {.
|
||||
async: (raises: [AsyncProcessError, AsyncProcessTimeoutError,
|
||||
CancelledError]).} =
|
||||
if p.exitStatus.isSome():
|
||||
return p.exitStatus.get()
|
||||
|
||||
let wres =
|
||||
try:
|
||||
await waitForSingleObject(p.processHandle, timeout)
|
||||
except ValueError as exc:
|
||||
except AsyncError as exc:
|
||||
raiseAsyncProcessError("Unable to wait for process handle", exc)
|
||||
|
||||
if wres == WaitableResult.Timeout:
|
||||
@ -528,7 +541,8 @@ when defined(windows):
|
||||
|
||||
if exitCode >= 0:
|
||||
p.exitStatus = Opt.some(exitCode)
|
||||
return exitCode
|
||||
|
||||
exitCode
|
||||
|
||||
proc peekExitCode(p: AsyncProcessRef): AsyncProcessResult[int] =
|
||||
if p.exitStatus.isSome():
|
||||
@ -778,7 +792,8 @@ else:
|
||||
stdinHandle = ProcessStreamHandle(),
|
||||
stdoutHandle = ProcessStreamHandle(),
|
||||
stderrHandle = ProcessStreamHandle(),
|
||||
): Future[AsyncProcessRef] {.async.} =
|
||||
): Future[AsyncProcessRef] {.
|
||||
async: (raises: [AsyncProcessError, CancelledError]).} =
|
||||
var
|
||||
pid: Pid
|
||||
pipes = preparePipes(options, stdinHandle, stdoutHandle,
|
||||
@ -878,7 +893,7 @@ else:
|
||||
)
|
||||
|
||||
trackCounter(AsyncProcessTrackerName)
|
||||
return process
|
||||
process
|
||||
|
||||
proc peekProcessExitCode(p: AsyncProcessRef,
|
||||
reap = false): AsyncProcessResult[int] =
|
||||
@ -939,7 +954,9 @@ else:
|
||||
ok(false)
|
||||
|
||||
proc waitForExit*(p: AsyncProcessRef,
|
||||
timeout = InfiniteDuration): Future[int] =
|
||||
timeout = InfiniteDuration): Future[int] {.
|
||||
async: (raw: true, raises: [
|
||||
AsyncProcessError, AsyncProcessTimeoutError, CancelledError]).} =
|
||||
var
|
||||
retFuture = newFuture[int]("chronos.waitForExit()")
|
||||
processHandle: ProcessHandle
|
||||
@ -979,7 +996,7 @@ else:
|
||||
return
|
||||
if not(isNil(timer)):
|
||||
clearTimer(timer)
|
||||
let exitCode = p.peekProcessExitCode().valueOr:
|
||||
let exitCode = p.peekProcessExitCode(reap = true).valueOr:
|
||||
retFuture.fail(newException(AsyncProcessError, osErrorMsg(error)))
|
||||
return
|
||||
if exitCode == -1:
|
||||
@ -993,12 +1010,14 @@ else:
|
||||
retFuture.fail(newException(AsyncProcessError,
|
||||
osErrorMsg(res.error())))
|
||||
|
||||
timer = nil
|
||||
|
||||
proc cancellation(udata: pointer) {.gcsafe.} =
|
||||
if not(retFuture.finished()):
|
||||
if not(isNil(timer)):
|
||||
clearTimer(timer)
|
||||
# Ignore any errors because of cancellation.
|
||||
discard removeProcess2(processHandle)
|
||||
if not(isNil(timer)):
|
||||
clearTimer(timer)
|
||||
timer = nil
|
||||
# Ignore any errors because of cancellation.
|
||||
discard removeProcess2(processHandle)
|
||||
|
||||
if timeout != InfiniteDuration:
|
||||
timer = setTimer(Moment.fromNow(timeout), continuation, cast[pointer](2))
|
||||
@ -1041,7 +1060,7 @@ else:
|
||||
|
||||
# Process is still running, so we going to wait for SIGCHLD.
|
||||
retFuture.cancelCallback = cancellation
|
||||
return retFuture
|
||||
retFuture
|
||||
|
||||
proc peekExitCode(p: AsyncProcessRef): AsyncProcessResult[int] =
|
||||
let res = ? p.peekProcessExitCode()
|
||||
@ -1146,7 +1165,7 @@ proc preparePipes(options: set[AsyncProcessOption],
|
||||
stderrHandle: remoteStderr
|
||||
))
|
||||
|
||||
proc closeWait(holder: AsyncStreamHolder) {.async.} =
|
||||
proc closeWait(holder: AsyncStreamHolder) {.async: (raises: []).} =
|
||||
let (future, transp) =
|
||||
case holder.kind
|
||||
of StreamKind.None:
|
||||
@ -1173,10 +1192,11 @@ proc closeWait(holder: AsyncStreamHolder) {.async.} =
|
||||
res
|
||||
|
||||
if len(pending) > 0:
|
||||
await allFutures(pending)
|
||||
await noCancel allFutures(pending)
|
||||
|
||||
proc closeProcessStreams(pipes: AsyncProcessPipes,
|
||||
options: set[AsyncProcessOption]): Future[void] =
|
||||
options: set[AsyncProcessOption]): Future[void] {.
|
||||
async: (raw: true, raises: []).} =
|
||||
let pending =
|
||||
block:
|
||||
var res: seq[Future[void]]
|
||||
@ -1187,9 +1207,53 @@ proc closeProcessStreams(pipes: AsyncProcessPipes,
|
||||
if ProcessFlag.AutoStderr in pipes.flags:
|
||||
res.add(pipes.stderrHolder.closeWait())
|
||||
res
|
||||
allFutures(pending)
|
||||
noCancel allFutures(pending)
|
||||
|
||||
proc closeWait*(p: AsyncProcessRef) {.async.} =
|
||||
proc opAndWaitForExit(p: AsyncProcessRef, op: WaitOperation,
|
||||
timeout = InfiniteDuration): Future[int] {.
|
||||
async: (raises: [
|
||||
AsyncProcessError, AsyncProcessTimeoutError, CancelledError]).} =
|
||||
let timerFut =
|
||||
if timeout == InfiniteDuration:
|
||||
newFuture[void]("chronos.killAndwaitForExit")
|
||||
else:
|
||||
sleepAsync(timeout)
|
||||
|
||||
while true:
|
||||
if p.running().get(true):
|
||||
# We ignore operation errors because we going to repeat calling
|
||||
# operation until process will not exit.
|
||||
case op
|
||||
of WaitOperation.Kill:
|
||||
discard p.kill()
|
||||
of WaitOperation.Terminate:
|
||||
discard p.terminate()
|
||||
else:
|
||||
let exitCode = p.peekExitCode().valueOr:
|
||||
raiseAsyncProcessError("Unable to peek process exit code", error)
|
||||
if not(timerFut.finished()):
|
||||
await cancelAndWait(timerFut)
|
||||
return exitCode
|
||||
|
||||
let waitFut = p.waitForExit().wait(100.milliseconds)
|
||||
try:
|
||||
discard await race(FutureBase(waitFut), FutureBase(timerFut))
|
||||
except ValueError:
|
||||
raiseAssert "This should not be happened!"
|
||||
|
||||
if waitFut.finished() and not(waitFut.failed()):
|
||||
let res = p.peekExitCode()
|
||||
if res.isOk():
|
||||
if not(timerFut.finished()):
|
||||
await cancelAndWait(timerFut)
|
||||
return res.get()
|
||||
|
||||
if timerFut.finished():
|
||||
if not(waitFut.finished()):
|
||||
await waitFut.cancelAndWait()
|
||||
raiseAsyncProcessTimeoutError()
|
||||
|
||||
proc closeWait*(p: AsyncProcessRef) {.async: (raises: []).} =
|
||||
# Here we ignore all possible errrors, because we do not want to raise
|
||||
# exceptions.
|
||||
discard closeProcessHandles(p.pipes, p.options, OSErrorCode(0))
|
||||
@ -1198,16 +1262,19 @@ proc closeWait*(p: AsyncProcessRef) {.async.} =
|
||||
untrackCounter(AsyncProcessTrackerName)
|
||||
|
||||
proc stdinStream*(p: AsyncProcessRef): AsyncStreamWriter =
|
||||
## Returns STDIN async stream associated with process `p`.
|
||||
doAssert(p.pipes.stdinHolder.kind == StreamKind.Writer,
|
||||
"StdinStreamWriter is not available")
|
||||
p.pipes.stdinHolder.writer
|
||||
|
||||
proc stdoutStream*(p: AsyncProcessRef): AsyncStreamReader =
|
||||
## Returns STDOUT async stream associated with process `p`.
|
||||
doAssert(p.pipes.stdoutHolder.kind == StreamKind.Reader,
|
||||
"StdoutStreamReader is not available")
|
||||
p.pipes.stdoutHolder.reader
|
||||
|
||||
proc stderrStream*(p: AsyncProcessRef): AsyncStreamReader =
|
||||
## Returns STDERR async stream associated with process `p`.
|
||||
doAssert(p.pipes.stderrHolder.kind == StreamKind.Reader,
|
||||
"StderrStreamReader is not available")
|
||||
p.pipes.stderrHolder.reader
|
||||
@ -1215,20 +1282,25 @@ proc stderrStream*(p: AsyncProcessRef): AsyncStreamReader =
|
||||
proc execCommand*(command: string,
|
||||
options = {AsyncProcessOption.EvalCommand},
|
||||
timeout = InfiniteDuration
|
||||
): Future[int] {.async.} =
|
||||
let poptions = options + {AsyncProcessOption.EvalCommand}
|
||||
let process = await startProcess(command, options = poptions)
|
||||
let res =
|
||||
try:
|
||||
await process.waitForExit(timeout)
|
||||
finally:
|
||||
await process.closeWait()
|
||||
return res
|
||||
): Future[int] {.
|
||||
async: (raises: [
|
||||
AsyncProcessError, AsyncProcessTimeoutError, CancelledError]).} =
|
||||
let
|
||||
poptions = options + {AsyncProcessOption.EvalCommand}
|
||||
process = await startProcess(command, options = poptions)
|
||||
res =
|
||||
try:
|
||||
await process.waitForExit(timeout)
|
||||
finally:
|
||||
await process.closeWait()
|
||||
res
|
||||
|
||||
proc execCommandEx*(command: string,
|
||||
options = {AsyncProcessOption.EvalCommand},
|
||||
timeout = InfiniteDuration
|
||||
): Future[CommandExResponse] {.async.} =
|
||||
): Future[CommandExResponse] {.
|
||||
async: (raises: [
|
||||
AsyncProcessError, AsyncProcessTimeoutError, CancelledError]).} =
|
||||
let
|
||||
process = await startProcess(command, options = options,
|
||||
stdoutHandle = AsyncProcess.Pipe,
|
||||
@ -1242,13 +1314,13 @@ proc execCommandEx*(command: string,
|
||||
status = await process.waitForExit(timeout)
|
||||
output =
|
||||
try:
|
||||
string.fromBytes(outputReader.read())
|
||||
string.fromBytes(await outputReader)
|
||||
except AsyncStreamError as exc:
|
||||
raiseAsyncProcessError("Unable to read process' stdout channel",
|
||||
exc)
|
||||
error =
|
||||
try:
|
||||
string.fromBytes(errorReader.read())
|
||||
string.fromBytes(await errorReader)
|
||||
except AsyncStreamError as exc:
|
||||
raiseAsyncProcessError("Unable to read process' stderr channel",
|
||||
exc)
|
||||
@ -1256,10 +1328,47 @@ proc execCommandEx*(command: string,
|
||||
finally:
|
||||
await process.closeWait()
|
||||
|
||||
return res
|
||||
res
|
||||
|
||||
proc pid*(p: AsyncProcessRef): int =
|
||||
## Returns process ``p`` identifier.
|
||||
## Returns process ``p`` unique process identifier.
|
||||
int(p.processId)
|
||||
|
||||
template processId*(p: AsyncProcessRef): int = pid(p)
|
||||
|
||||
proc killAndWaitForExit*(p: AsyncProcessRef,
|
||||
timeout = InfiniteDuration): Future[int] {.
|
||||
async: (raw: true, raises: [
|
||||
AsyncProcessError, AsyncProcessTimeoutError, CancelledError]).} =
|
||||
## Perform continuous attempts to kill the ``p`` process for specified period
|
||||
## of time ``timeout``.
|
||||
##
|
||||
## On Posix systems, killing means sending ``SIGKILL`` to the process ``p``,
|
||||
## On Windows, it uses ``TerminateProcess`` to kill the process ``p``.
|
||||
##
|
||||
## If the process ``p`` fails to be killed within the ``timeout`` time, it
|
||||
## will raise ``AsyncProcessTimeoutError``.
|
||||
##
|
||||
## In case of error this it will raise ``AsyncProcessError``.
|
||||
##
|
||||
## Returns process ``p`` exit code.
|
||||
opAndWaitForExit(p, WaitOperation.Kill, timeout)
|
||||
|
||||
proc terminateAndWaitForExit*(p: AsyncProcessRef,
|
||||
timeout = InfiniteDuration): Future[int] {.
|
||||
async: (raw: true, raises: [
|
||||
AsyncProcessError, AsyncProcessTimeoutError, CancelledError]).} =
|
||||
## Perform continuous attempts to terminate the ``p`` process for specified
|
||||
## period of time ``timeout``.
|
||||
##
|
||||
## On Posix systems, terminating means sending ``SIGTERM`` to the process
|
||||
## ``p``, on Windows, it uses ``TerminateProcess`` to terminate the process
|
||||
## ``p``.
|
||||
##
|
||||
## If the process ``p`` fails to be terminated within the ``timeout`` time, it
|
||||
## will raise ``AsyncProcessTimeoutError``.
|
||||
##
|
||||
## In case of error this it will raise ``AsyncProcessError``.
|
||||
##
|
||||
## Returns process ``p`` exit code.
|
||||
opAndWaitForExit(p, WaitOperation.Terminate, timeout)
|
||||
|
||||
@ -28,7 +28,7 @@ type
|
||||
## is blocked in ``acquire()`` is being processed.
|
||||
locked: bool
|
||||
acquired: bool
|
||||
waiters: seq[Future[void]]
|
||||
waiters: seq[Future[void].Raising([CancelledError])]
|
||||
|
||||
AsyncEvent* = ref object of RootRef
|
||||
## A primitive event object.
|
||||
@ -41,7 +41,7 @@ type
|
||||
## state to be signaled, when event get fired, then all coroutines
|
||||
## continue proceeds in order, they have entered waiting state.
|
||||
flag: bool
|
||||
waiters: seq[Future[void]]
|
||||
waiters: seq[Future[void].Raising([CancelledError])]
|
||||
|
||||
AsyncQueue*[T] = ref object of RootRef
|
||||
## A queue, useful for coordinating producer and consumer coroutines.
|
||||
@ -50,8 +50,8 @@ type
|
||||
## infinite. If it is an integer greater than ``0``, then "await put()"
|
||||
## will block when the queue reaches ``maxsize``, until an item is
|
||||
## removed by "await get()".
|
||||
getters: seq[Future[void]]
|
||||
putters: seq[Future[void]]
|
||||
getters: seq[Future[void].Raising([CancelledError])]
|
||||
putters: seq[Future[void].Raising([CancelledError])]
|
||||
queue: Deque[T]
|
||||
maxsize: int
|
||||
|
||||
@ -62,50 +62,6 @@ type
|
||||
AsyncLockError* = object of AsyncError
|
||||
## ``AsyncLock`` is either locked or unlocked.
|
||||
|
||||
EventBusSubscription*[T] = proc(bus: AsyncEventBus,
|
||||
payload: EventPayload[T]): Future[void] {.
|
||||
gcsafe, raises: [].}
|
||||
## EventBus subscription callback type.
|
||||
|
||||
EventBusAllSubscription* = proc(bus: AsyncEventBus,
|
||||
event: AwaitableEvent): Future[void] {.
|
||||
gcsafe, raises: [].}
|
||||
## EventBus subscription callback type.
|
||||
|
||||
EventBusCallback = proc(bus: AsyncEventBus, event: string, key: EventBusKey,
|
||||
data: EventPayloadBase) {.
|
||||
gcsafe, raises: [].}
|
||||
|
||||
EventBusKey* = object
|
||||
## Unique subscription key.
|
||||
eventName: string
|
||||
typeName: string
|
||||
unique: uint64
|
||||
cb: EventBusCallback
|
||||
|
||||
EventItem = object
|
||||
waiters: seq[FutureBase]
|
||||
subscribers: seq[EventBusKey]
|
||||
|
||||
AsyncEventBus* = ref object of RootObj
|
||||
## An eventbus object.
|
||||
counter: uint64
|
||||
events: Table[string, EventItem]
|
||||
subscribers: seq[EventBusKey]
|
||||
waiters: seq[Future[AwaitableEvent]]
|
||||
|
||||
EventPayloadBase* = ref object of RootObj
|
||||
loc: ptr SrcLoc
|
||||
|
||||
EventPayload*[T] = ref object of EventPayloadBase
|
||||
## Eventbus' event payload object
|
||||
value: T
|
||||
|
||||
AwaitableEvent* = object
|
||||
## Eventbus' event payload object
|
||||
eventName: string
|
||||
payload: EventPayloadBase
|
||||
|
||||
AsyncEventQueueFullError* = object of AsyncError
|
||||
|
||||
EventQueueKey* = distinct uint64
|
||||
@ -113,7 +69,7 @@ type
|
||||
EventQueueReader* = object
|
||||
key: EventQueueKey
|
||||
offset: int
|
||||
waiter: Future[void]
|
||||
waiter: Future[void].Raising([CancelledError])
|
||||
overflow: bool
|
||||
|
||||
AsyncEventQueue*[T] = ref object of RootObj
|
||||
@ -134,17 +90,14 @@ proc newAsyncLock*(): AsyncLock =
|
||||
## The ``release()`` procedure changes the state to unlocked and returns
|
||||
## immediately.
|
||||
|
||||
# Workaround for callSoon() not worked correctly before
|
||||
# getThreadDispatcher() call.
|
||||
discard getThreadDispatcher()
|
||||
AsyncLock(waiters: newSeq[Future[void]](), locked: false, acquired: false)
|
||||
AsyncLock()
|
||||
|
||||
proc wakeUpFirst(lock: AsyncLock): bool {.inline.} =
|
||||
## Wake up the first waiter if it isn't done.
|
||||
var i = 0
|
||||
var res = false
|
||||
while i < len(lock.waiters):
|
||||
var waiter = lock.waiters[i]
|
||||
let waiter = lock.waiters[i]
|
||||
inc(i)
|
||||
if not(waiter.finished()):
|
||||
waiter.complete()
|
||||
@ -164,7 +117,7 @@ proc checkAll(lock: AsyncLock): bool {.inline.} =
|
||||
return false
|
||||
return true
|
||||
|
||||
proc acquire*(lock: AsyncLock) {.async.} =
|
||||
proc acquire*(lock: AsyncLock) {.async: (raises: [CancelledError]).} =
|
||||
## Acquire a lock ``lock``.
|
||||
##
|
||||
## This procedure blocks until the lock ``lock`` is unlocked, then sets it
|
||||
@ -173,7 +126,7 @@ proc acquire*(lock: AsyncLock) {.async.} =
|
||||
lock.acquired = true
|
||||
lock.locked = true
|
||||
else:
|
||||
var w = newFuture[void]("AsyncLock.acquire")
|
||||
let w = Future[void].Raising([CancelledError]).init("AsyncLock.acquire")
|
||||
lock.waiters.add(w)
|
||||
await w
|
||||
lock.acquired = true
|
||||
@ -209,13 +162,10 @@ proc newAsyncEvent*(): AsyncEvent =
|
||||
## procedure and reset to `false` with the `clear()` procedure.
|
||||
## The `wait()` procedure blocks until the flag is `true`. The flag is
|
||||
## initially `false`.
|
||||
AsyncEvent()
|
||||
|
||||
# Workaround for callSoon() not worked correctly before
|
||||
# getThreadDispatcher() call.
|
||||
discard getThreadDispatcher()
|
||||
AsyncEvent(waiters: newSeq[Future[void]](), flag: false)
|
||||
|
||||
proc wait*(event: AsyncEvent): Future[void] =
|
||||
proc wait*(event: AsyncEvent): Future[void] {.
|
||||
async: (raw: true, raises: [CancelledError]).} =
|
||||
## Block until the internal flag of ``event`` is `true`.
|
||||
## If the internal flag is `true` on entry, return immediately. Otherwise,
|
||||
## block until another task calls `fire()` to set the flag to `true`,
|
||||
@ -254,20 +204,15 @@ proc isSet*(event: AsyncEvent): bool =
|
||||
proc newAsyncQueue*[T](maxsize: int = 0): AsyncQueue[T] =
|
||||
## Creates a new asynchronous queue ``AsyncQueue``.
|
||||
|
||||
# Workaround for callSoon() not worked correctly before
|
||||
# getThreadDispatcher() call.
|
||||
discard getThreadDispatcher()
|
||||
AsyncQueue[T](
|
||||
getters: newSeq[Future[void]](),
|
||||
putters: newSeq[Future[void]](),
|
||||
queue: initDeque[T](),
|
||||
maxsize: maxsize
|
||||
)
|
||||
|
||||
proc wakeupNext(waiters: var seq[Future[void]]) {.inline.} =
|
||||
proc wakeupNext(waiters: var seq) {.inline.} =
|
||||
var i = 0
|
||||
while i < len(waiters):
|
||||
var waiter = waiters[i]
|
||||
let waiter = waiters[i]
|
||||
inc(i)
|
||||
|
||||
if not(waiter.finished()):
|
||||
@ -294,119 +239,141 @@ proc empty*[T](aq: AsyncQueue[T]): bool {.inline.} =
|
||||
## Return ``true`` if the queue is empty, ``false`` otherwise.
|
||||
(len(aq.queue) == 0)
|
||||
|
||||
proc addFirstImpl[T](aq: AsyncQueue[T], item: T) =
|
||||
aq.queue.addFirst(item)
|
||||
aq.getters.wakeupNext()
|
||||
|
||||
proc addLastImpl[T](aq: AsyncQueue[T], item: T) =
|
||||
aq.queue.addLast(item)
|
||||
aq.getters.wakeupNext()
|
||||
|
||||
proc popFirstImpl[T](aq: AsyncQueue[T]): T =
|
||||
let res = aq.queue.popFirst()
|
||||
aq.putters.wakeupNext()
|
||||
res
|
||||
|
||||
proc popLastImpl[T](aq: AsyncQueue[T]): T =
|
||||
let res = aq.queue.popLast()
|
||||
aq.putters.wakeupNext()
|
||||
res
|
||||
|
||||
proc addFirstNoWait*[T](aq: AsyncQueue[T], item: T) {.
|
||||
raises: [AsyncQueueFullError].}=
|
||||
raises: [AsyncQueueFullError].} =
|
||||
## Put an item ``item`` to the beginning of the queue ``aq`` immediately.
|
||||
##
|
||||
## If queue ``aq`` is full, then ``AsyncQueueFullError`` exception raised.
|
||||
if aq.full():
|
||||
raise newException(AsyncQueueFullError, "AsyncQueue is full!")
|
||||
aq.queue.addFirst(item)
|
||||
aq.getters.wakeupNext()
|
||||
aq.addFirstImpl(item)
|
||||
|
||||
proc addLastNoWait*[T](aq: AsyncQueue[T], item: T) {.
|
||||
raises: [AsyncQueueFullError].}=
|
||||
raises: [AsyncQueueFullError].} =
|
||||
## Put an item ``item`` at the end of the queue ``aq`` immediately.
|
||||
##
|
||||
## If queue ``aq`` is full, then ``AsyncQueueFullError`` exception raised.
|
||||
if aq.full():
|
||||
raise newException(AsyncQueueFullError, "AsyncQueue is full!")
|
||||
aq.queue.addLast(item)
|
||||
aq.getters.wakeupNext()
|
||||
aq.addLastImpl(item)
|
||||
|
||||
proc popFirstNoWait*[T](aq: AsyncQueue[T]): T {.
|
||||
raises: [AsyncQueueEmptyError].} =
|
||||
raises: [AsyncQueueEmptyError].} =
|
||||
## Get an item from the beginning of the queue ``aq`` immediately.
|
||||
##
|
||||
## If queue ``aq`` is empty, then ``AsyncQueueEmptyError`` exception raised.
|
||||
if aq.empty():
|
||||
raise newException(AsyncQueueEmptyError, "AsyncQueue is empty!")
|
||||
let res = aq.queue.popFirst()
|
||||
aq.putters.wakeupNext()
|
||||
res
|
||||
aq.popFirstImpl()
|
||||
|
||||
proc popLastNoWait*[T](aq: AsyncQueue[T]): T {.
|
||||
raises: [AsyncQueueEmptyError].} =
|
||||
raises: [AsyncQueueEmptyError].} =
|
||||
## Get an item from the end of the queue ``aq`` immediately.
|
||||
##
|
||||
## If queue ``aq`` is empty, then ``AsyncQueueEmptyError`` exception raised.
|
||||
if aq.empty():
|
||||
raise newException(AsyncQueueEmptyError, "AsyncQueue is empty!")
|
||||
let res = aq.queue.popLast()
|
||||
aq.putters.wakeupNext()
|
||||
res
|
||||
aq.popLastImpl()
|
||||
|
||||
proc addFirst*[T](aq: AsyncQueue[T], item: T) {.async.} =
|
||||
proc addFirst*[T](aq: AsyncQueue[T], item: T) {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
## Put an ``item`` to the beginning of the queue ``aq``. If the queue is full,
|
||||
## wait until a free slot is available before adding item.
|
||||
while aq.full():
|
||||
var putter = newFuture[void]("AsyncQueue.addFirst")
|
||||
let putter =
|
||||
Future[void].Raising([CancelledError]).init("AsyncQueue.addFirst")
|
||||
aq.putters.add(putter)
|
||||
try:
|
||||
await putter
|
||||
except CatchableError as exc:
|
||||
except CancelledError as exc:
|
||||
if not(aq.full()) and not(putter.cancelled()):
|
||||
aq.putters.wakeupNext()
|
||||
raise exc
|
||||
aq.addFirstNoWait(item)
|
||||
aq.addFirstImpl(item)
|
||||
|
||||
proc addLast*[T](aq: AsyncQueue[T], item: T) {.async.} =
|
||||
proc addLast*[T](aq: AsyncQueue[T], item: T) {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
## Put an ``item`` to the end of the queue ``aq``. If the queue is full,
|
||||
## wait until a free slot is available before adding item.
|
||||
while aq.full():
|
||||
var putter = newFuture[void]("AsyncQueue.addLast")
|
||||
let putter =
|
||||
Future[void].Raising([CancelledError]).init("AsyncQueue.addLast")
|
||||
aq.putters.add(putter)
|
||||
try:
|
||||
await putter
|
||||
except CatchableError as exc:
|
||||
except CancelledError as exc:
|
||||
if not(aq.full()) and not(putter.cancelled()):
|
||||
aq.putters.wakeupNext()
|
||||
raise exc
|
||||
aq.addLastNoWait(item)
|
||||
aq.addLastImpl(item)
|
||||
|
||||
proc popFirst*[T](aq: AsyncQueue[T]): Future[T] {.async.} =
|
||||
proc popFirst*[T](aq: AsyncQueue[T]): Future[T] {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
## Remove and return an ``item`` from the beginning of the queue ``aq``.
|
||||
## If the queue is empty, wait until an item is available.
|
||||
while aq.empty():
|
||||
var getter = newFuture[void]("AsyncQueue.popFirst")
|
||||
let getter =
|
||||
Future[void].Raising([CancelledError]).init("AsyncQueue.popFirst")
|
||||
aq.getters.add(getter)
|
||||
try:
|
||||
await getter
|
||||
except CatchableError as exc:
|
||||
except CancelledError as exc:
|
||||
if not(aq.empty()) and not(getter.cancelled()):
|
||||
aq.getters.wakeupNext()
|
||||
raise exc
|
||||
return aq.popFirstNoWait()
|
||||
aq.popFirstImpl()
|
||||
|
||||
proc popLast*[T](aq: AsyncQueue[T]): Future[T] {.async.} =
|
||||
proc popLast*[T](aq: AsyncQueue[T]): Future[T] {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
## Remove and return an ``item`` from the end of the queue ``aq``.
|
||||
## If the queue is empty, wait until an item is available.
|
||||
while aq.empty():
|
||||
var getter = newFuture[void]("AsyncQueue.popLast")
|
||||
let getter =
|
||||
Future[void].Raising([CancelledError]).init("AsyncQueue.popLast")
|
||||
aq.getters.add(getter)
|
||||
try:
|
||||
await getter
|
||||
except CatchableError as exc:
|
||||
except CancelledError as exc:
|
||||
if not(aq.empty()) and not(getter.cancelled()):
|
||||
aq.getters.wakeupNext()
|
||||
raise exc
|
||||
return aq.popLastNoWait()
|
||||
aq.popLastImpl()
|
||||
|
||||
proc putNoWait*[T](aq: AsyncQueue[T], item: T) {.
|
||||
raises: [AsyncQueueFullError].} =
|
||||
raises: [AsyncQueueFullError].} =
|
||||
## Alias of ``addLastNoWait()``.
|
||||
aq.addLastNoWait(item)
|
||||
|
||||
proc getNoWait*[T](aq: AsyncQueue[T]): T {.
|
||||
raises: [AsyncQueueEmptyError].} =
|
||||
raises: [AsyncQueueEmptyError].} =
|
||||
## Alias of ``popFirstNoWait()``.
|
||||
aq.popFirstNoWait()
|
||||
|
||||
proc put*[T](aq: AsyncQueue[T], item: T): Future[void] {.inline.} =
|
||||
proc put*[T](aq: AsyncQueue[T], item: T): Future[void] {.
|
||||
async: (raw: true, raises: [CancelledError]).} =
|
||||
## Alias of ``addLast()``.
|
||||
aq.addLast(item)
|
||||
|
||||
proc get*[T](aq: AsyncQueue[T]): Future[T] {.inline.} =
|
||||
proc get*[T](aq: AsyncQueue[T]): Future[T] {.
|
||||
async: (raw: true, raises: [CancelledError]).} =
|
||||
## Alias of ``popFirst()``.
|
||||
aq.popFirst()
|
||||
|
||||
@ -460,7 +427,7 @@ proc contains*[T](aq: AsyncQueue[T], item: T): bool {.inline.} =
|
||||
## via the ``in`` operator.
|
||||
for e in aq.queue.items():
|
||||
if e == item: return true
|
||||
return false
|
||||
false
|
||||
|
||||
proc `$`*[T](aq: AsyncQueue[T]): string =
|
||||
## Turn an async queue ``aq`` into its string representation.
|
||||
@ -471,190 +438,6 @@ proc `$`*[T](aq: AsyncQueue[T]): string =
|
||||
res.add("]")
|
||||
res
|
||||
|
||||
template generateKey(typeName, eventName: string): string =
|
||||
"type[" & typeName & "]-key[" & eventName & "]"
|
||||
|
||||
proc newAsyncEventBus*(): AsyncEventBus {.
|
||||
deprecated: "Implementation has unfixable flaws, please use" &
|
||||
"AsyncEventQueue[T] instead".} =
|
||||
## Creates new ``AsyncEventBus``.
|
||||
AsyncEventBus(counter: 0'u64, events: initTable[string, EventItem]())
|
||||
|
||||
template get*[T](payload: EventPayload[T]): T =
|
||||
## Returns event payload data.
|
||||
payload.value
|
||||
|
||||
template location*(payload: EventPayloadBase): SrcLoc =
|
||||
## Returns source location address of event emitter.
|
||||
payload.loc[]
|
||||
|
||||
proc get*(event: AwaitableEvent, T: typedesc): T {.
|
||||
deprecated: "Implementation has unfixable flaws, please use " &
|
||||
"AsyncEventQueue[T] instead".} =
|
||||
## Returns event's payload of type ``T`` from event ``event``.
|
||||
cast[EventPayload[T]](event.payload).value
|
||||
|
||||
template event*(event: AwaitableEvent): string =
|
||||
## Returns event's name from event ``event``.
|
||||
event.eventName
|
||||
|
||||
template location*(event: AwaitableEvent): SrcLoc =
|
||||
## Returns source location address of event emitter.
|
||||
event.payload.loc[]
|
||||
|
||||
proc waitEvent*(bus: AsyncEventBus, T: typedesc, event: string): Future[T] {.
|
||||
deprecated: "Implementation has unfixable flaws, please use " &
|
||||
"AsyncEventQueue[T] instead".} =
|
||||
## Wait for the event from AsyncEventBus ``bus`` with name ``event``.
|
||||
##
|
||||
## Returned ``Future[T]`` will hold event's payload of type ``T``.
|
||||
var default: EventItem
|
||||
var retFuture = newFuture[T]("AsyncEventBus.waitEvent")
|
||||
let eventKey = generateKey(T.name, event)
|
||||
proc cancellation(udata: pointer) {.gcsafe, raises: [].} =
|
||||
if not(retFuture.finished()):
|
||||
bus.events.withValue(eventKey, item):
|
||||
item.waiters.keepItIf(it != cast[FutureBase](retFuture))
|
||||
retFuture.cancelCallback = cancellation
|
||||
let baseFuture = cast[FutureBase](retFuture)
|
||||
bus.events.mgetOrPut(eventKey, default).waiters.add(baseFuture)
|
||||
retFuture
|
||||
|
||||
proc waitAllEvents*(bus: AsyncEventBus): Future[AwaitableEvent] {.
|
||||
deprecated: "Implementation has unfixable flaws, please use " &
|
||||
"AsyncEventQueue[T] instead".} =
|
||||
## Wait for any event from AsyncEventBus ``bus``.
|
||||
##
|
||||
## Returns ``Future`` which holds helper object. Using this object you can
|
||||
## retrieve event's name and payload.
|
||||
var retFuture = newFuture[AwaitableEvent]("AsyncEventBus.waitAllEvents")
|
||||
proc cancellation(udata: pointer) {.gcsafe, raises: [].} =
|
||||
if not(retFuture.finished()):
|
||||
bus.waiters.keepItIf(it != retFuture)
|
||||
retFuture.cancelCallback = cancellation
|
||||
bus.waiters.add(retFuture)
|
||||
retFuture
|
||||
|
||||
proc subscribe*[T](bus: AsyncEventBus, event: string,
|
||||
callback: EventBusSubscription[T]): EventBusKey {.
|
||||
deprecated: "Implementation has unfixable flaws, please use " &
|
||||
"AsyncEventQueue[T] instead".} =
|
||||
## Subscribe to the event ``event`` passed through eventbus ``bus`` with
|
||||
## callback ``callback``.
|
||||
##
|
||||
## Returns key that can be used to unsubscribe.
|
||||
proc trampoline(tbus: AsyncEventBus, event: string, key: EventBusKey,
|
||||
data: EventPayloadBase) {.gcsafe, raises: [].} =
|
||||
let payload = cast[EventPayload[T]](data)
|
||||
asyncSpawn callback(bus, payload)
|
||||
|
||||
let subkey =
|
||||
block:
|
||||
inc(bus.counter)
|
||||
EventBusKey(eventName: event, typeName: T.name, unique: bus.counter,
|
||||
cb: trampoline)
|
||||
|
||||
var default: EventItem
|
||||
let eventKey = generateKey(T.name, event)
|
||||
bus.events.mgetOrPut(eventKey, default).subscribers.add(subkey)
|
||||
subkey
|
||||
|
||||
proc subscribeAll*(bus: AsyncEventBus,
|
||||
callback: EventBusAllSubscription): EventBusKey {.
|
||||
deprecated: "Implementation has unfixable flaws, please use " &
|
||||
"AsyncEventQueue instead".} =
|
||||
## Subscribe to all events passed through eventbus ``bus`` with callback
|
||||
## ``callback``.
|
||||
##
|
||||
## Returns key that can be used to unsubscribe.
|
||||
proc trampoline(tbus: AsyncEventBus, event: string, key: EventBusKey,
|
||||
data: EventPayloadBase) {.gcsafe, raises: [].} =
|
||||
let event = AwaitableEvent(eventName: event, payload: data)
|
||||
asyncSpawn callback(bus, event)
|
||||
|
||||
let subkey =
|
||||
block:
|
||||
inc(bus.counter)
|
||||
EventBusKey(eventName: "", typeName: "", unique: bus.counter,
|
||||
cb: trampoline)
|
||||
bus.subscribers.add(subkey)
|
||||
subkey
|
||||
|
||||
proc unsubscribe*(bus: AsyncEventBus, key: EventBusKey) {.
|
||||
deprecated: "Implementation has unfixable flaws, please use " &
|
||||
"AsyncEventQueue instead".} =
|
||||
## Cancel subscription of subscriber with key ``key`` from eventbus ``bus``.
|
||||
let eventKey = generateKey(key.typeName, key.eventName)
|
||||
|
||||
# Clean event's subscribers.
|
||||
bus.events.withValue(eventKey, item):
|
||||
item.subscribers.keepItIf(it.unique != key.unique)
|
||||
|
||||
# Clean subscribers subscribed to all events.
|
||||
bus.subscribers.keepItIf(it.unique != key.unique)
|
||||
|
||||
proc emit[T](bus: AsyncEventBus, event: string, data: T, loc: ptr SrcLoc) =
|
||||
let
|
||||
eventKey = generateKey(T.name, event)
|
||||
payload =
|
||||
block:
|
||||
var data = EventPayload[T](value: data, loc: loc)
|
||||
cast[EventPayloadBase](data)
|
||||
|
||||
# Used to capture the "subscriber" variable in the loops
|
||||
# sugar.capture doesn't work in Nim <1.6
|
||||
proc triggerSubscriberCallback(subscriber: EventBusKey) =
|
||||
callSoon(proc(udata: pointer) =
|
||||
subscriber.cb(bus, event, subscriber, payload)
|
||||
)
|
||||
|
||||
bus.events.withValue(eventKey, item):
|
||||
# Schedule waiters which are waiting for the event ``event``.
|
||||
for waiter in item.waiters:
|
||||
var fut = cast[Future[T]](waiter)
|
||||
fut.complete(data)
|
||||
# Clear all the waiters.
|
||||
item.waiters.setLen(0)
|
||||
|
||||
# Schedule subscriber's callbacks, which are subscribed to the event.
|
||||
for subscriber in item.subscribers:
|
||||
triggerSubscriberCallback(subscriber)
|
||||
|
||||
# Schedule waiters which are waiting all events
|
||||
for waiter in bus.waiters:
|
||||
waiter.complete(AwaitableEvent(eventName: event, payload: payload))
|
||||
# Clear all the waiters.
|
||||
bus.waiters.setLen(0)
|
||||
|
||||
# Schedule subscriber's callbacks which are subscribed to all events.
|
||||
for subscriber in bus.subscribers:
|
||||
triggerSubscriberCallback(subscriber)
|
||||
|
||||
template emit*[T](bus: AsyncEventBus, event: string, data: T) {.
|
||||
deprecated: "Implementation has unfixable flaws, please use " &
|
||||
"AsyncEventQueue instead".} =
|
||||
## Emit new event ``event`` to the eventbus ``bus`` with payload ``data``.
|
||||
emit(bus, event, data, getSrcLocation())
|
||||
|
||||
proc emitWait[T](bus: AsyncEventBus, event: string, data: T,
|
||||
loc: ptr SrcLoc): Future[void] =
|
||||
var retFuture = newFuture[void]("AsyncEventBus.emitWait")
|
||||
proc continuation(udata: pointer) {.gcsafe.} =
|
||||
if not(retFuture.finished()):
|
||||
retFuture.complete()
|
||||
emit(bus, event, data, loc)
|
||||
callSoon(continuation)
|
||||
return retFuture
|
||||
|
||||
template emitWait*[T](bus: AsyncEventBus, event: string,
|
||||
data: T): Future[void] {.
|
||||
deprecated: "Implementation has unfixable flaws, please use " &
|
||||
"AsyncEventQueue instead".} =
|
||||
## Emit new event ``event`` to the eventbus ``bus`` with payload ``data`` and
|
||||
## wait until all the subscribers/waiters will receive notification about
|
||||
## event.
|
||||
emitWait(bus, event, data, getSrcLocation())
|
||||
|
||||
proc `==`(a, b: EventQueueKey): bool {.borrow.}
|
||||
|
||||
proc compact(ab: AsyncEventQueue) {.raises: [].} =
|
||||
@ -680,8 +463,7 @@ proc compact(ab: AsyncEventQueue) {.raises: [].} =
|
||||
else:
|
||||
ab.queue.clear()
|
||||
|
||||
proc getReaderIndex(ab: AsyncEventQueue, key: EventQueueKey): int {.
|
||||
raises: [].} =
|
||||
proc getReaderIndex(ab: AsyncEventQueue, key: EventQueueKey): int =
|
||||
for index, value in ab.readers.pairs():
|
||||
if value.key == key:
|
||||
return index
|
||||
@ -735,11 +517,16 @@ proc close*(ab: AsyncEventQueue) {.raises: [].} =
|
||||
ab.readers.reset()
|
||||
ab.queue.clear()
|
||||
|
||||
proc closeWait*(ab: AsyncEventQueue): Future[void] {.raises: [].} =
|
||||
var retFuture = newFuture[void]("AsyncEventQueue.closeWait()")
|
||||
proc closeWait*(ab: AsyncEventQueue): Future[void] {.
|
||||
async: (raw: true, raises: []).} =
|
||||
let retFuture = newFuture[void]("AsyncEventQueue.closeWait()",
|
||||
{FutureFlag.OwnCancelSchedule})
|
||||
proc continuation(udata: pointer) {.gcsafe.} =
|
||||
if not(retFuture.finished()):
|
||||
retFuture.complete()
|
||||
retFuture.complete()
|
||||
|
||||
# Ignore cancellation requests - we'll complete the future soon enough
|
||||
retFuture.cancelCallback = nil
|
||||
|
||||
ab.close()
|
||||
# Schedule `continuation` to be called only after all the `reader`
|
||||
# notifications will be scheduled and processed.
|
||||
@ -750,7 +537,7 @@ template readerOverflow*(ab: AsyncEventQueue,
|
||||
reader: EventQueueReader): bool =
|
||||
ab.limit + (reader.offset - ab.offset) <= len(ab.queue)
|
||||
|
||||
proc emit*[T](ab: AsyncEventQueue[T], data: T) {.raises: [].} =
|
||||
proc emit*[T](ab: AsyncEventQueue[T], data: T) =
|
||||
if len(ab.readers) > 0:
|
||||
# We enqueue `data` only if there active reader present.
|
||||
var changesPresent = false
|
||||
@ -787,7 +574,8 @@ proc emit*[T](ab: AsyncEventQueue[T], data: T) {.raises: [].} =
|
||||
|
||||
proc waitEvents*[T](ab: AsyncEventQueue[T],
|
||||
key: EventQueueKey,
|
||||
eventsCount = -1): Future[seq[T]] {.async.} =
|
||||
eventsCount = -1): Future[seq[T]] {.
|
||||
async: (raises: [AsyncEventQueueFullError, CancelledError]).} =
|
||||
## Wait for events
|
||||
var
|
||||
events: seq[T]
|
||||
@ -817,7 +605,8 @@ proc waitEvents*[T](ab: AsyncEventQueue[T],
|
||||
doAssert(length >= ab.readers[index].offset)
|
||||
if length == ab.readers[index].offset:
|
||||
# We are at the end of queue, it means that we should wait for new events.
|
||||
let waitFuture = newFuture[void]("AsyncEventQueue.waitEvents")
|
||||
let waitFuture = Future[void].Raising([CancelledError]).init(
|
||||
"AsyncEventQueue.waitEvents")
|
||||
ab.readers[index].waiter = waitFuture
|
||||
resetFuture = true
|
||||
await waitFuture
|
||||
@ -848,4 +637,4 @@ proc waitEvents*[T](ab: AsyncEventQueue[T],
|
||||
if (eventsCount <= 0) or (len(events) == eventsCount):
|
||||
break
|
||||
|
||||
return events
|
||||
events
|
||||
|
||||
140
chronos/bipbuffer.nim
Normal file
140
chronos/bipbuffer.nim
Normal file
@ -0,0 +1,140 @@
|
||||
#
|
||||
# Chronos
|
||||
#
|
||||
# (c) Copyright 2018-Present Status Research & Development GmbH
|
||||
#
|
||||
# Licensed under either of
|
||||
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
||||
# MIT license (LICENSE-MIT)
|
||||
|
||||
## This module implements Bip Buffer (bi-partite circular buffer) by Simone
|
||||
## Cooke.
|
||||
##
|
||||
## The Bip-Buffer is like a circular buffer, but slightly different. Instead of
|
||||
## keeping one head and tail pointer to the data in the buffer, it maintains two
|
||||
## revolving regions, allowing for fast data access without having to worry
|
||||
## about wrapping at the end of the buffer. Buffer allocations are always
|
||||
## maintained as contiguous blocks, allowing the buffer to be used in a highly
|
||||
## efficient manner with API calls, and also reducing the amount of copying
|
||||
## which needs to be performed to put data into the buffer. Finally, a two-phase
|
||||
## allocation system allows the user to pessimistically reserve an area of
|
||||
## buffer space, and then trim back the buffer to commit to only the space which
|
||||
## was used.
|
||||
##
|
||||
## https://www.codeproject.com/Articles/3479/The-Bip-Buffer-The-Circular-Buffer-with-a-Twist
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
type
|
||||
BipPos = object
|
||||
start: Natural
|
||||
finish: Natural
|
||||
|
||||
BipBuffer* = object
|
||||
a, b, r: BipPos
|
||||
data: seq[byte]
|
||||
|
||||
proc init*(t: typedesc[BipBuffer], size: int): BipBuffer =
|
||||
## Creates new Bip Buffer with size `size`.
|
||||
BipBuffer(data: newSeq[byte](size))
|
||||
|
||||
template len(pos: BipPos): Natural =
|
||||
pos.finish - pos.start
|
||||
|
||||
template reset(pos: var BipPos) =
|
||||
pos = BipPos()
|
||||
|
||||
func init(t: typedesc[BipPos], start, finish: Natural): BipPos =
|
||||
BipPos(start: start, finish: finish)
|
||||
|
||||
func calcReserve(bp: BipBuffer): tuple[space: Natural, start: Natural] =
|
||||
if len(bp.b) > 0:
|
||||
(Natural(bp.a.start - bp.b.finish), bp.b.finish)
|
||||
else:
|
||||
let spaceAfterA = Natural(len(bp.data) - bp.a.finish)
|
||||
if spaceAfterA >= bp.a.start:
|
||||
(spaceAfterA, bp.a.finish)
|
||||
else:
|
||||
(bp.a.start, Natural(0))
|
||||
|
||||
func availSpace*(bp: BipBuffer): Natural =
|
||||
## Returns amount of space available for reserve in buffer `bp`.
|
||||
let (res, _) = bp.calcReserve()
|
||||
res
|
||||
|
||||
func len*(bp: BipBuffer): Natural =
|
||||
## Returns amount of used space in buffer `bp`.
|
||||
len(bp.b) + len(bp.a)
|
||||
|
||||
proc reserve*(bp: var BipBuffer,
|
||||
size: Natural = 0): tuple[data: ptr byte, size: Natural] =
|
||||
## Reserve `size` bytes in buffer.
|
||||
##
|
||||
## If `size == 0` (default) reserve all available space from buffer.
|
||||
##
|
||||
## If there is not enough space in buffer for resevation - error will be
|
||||
## returned.
|
||||
##
|
||||
## Returns current reserved range as pointer of type `pt` and size of
|
||||
## type `st`.
|
||||
const ErrorMessage = "Not enough space available"
|
||||
doAssert(size <= len(bp.data))
|
||||
let (availableSpace, reserveStart) = bp.calcReserve()
|
||||
if availableSpace == 0:
|
||||
raiseAssert ErrorMessage
|
||||
let reserveLength =
|
||||
if size == 0:
|
||||
availableSpace
|
||||
else:
|
||||
if size < availableSpace:
|
||||
raiseAssert ErrorMessage
|
||||
size
|
||||
bp.r = BipPos.init(reserveStart, Natural(reserveStart + reserveLength))
|
||||
(addr bp.data[bp.r.start], len(bp.r))
|
||||
|
||||
proc commit*(bp: var BipBuffer, size: Natural) =
|
||||
## Updates structure's pointers when new data inserted into buffer.
|
||||
doAssert(len(bp.r) >= size,
|
||||
"Committed size could not be larger than the previously reserved one")
|
||||
if size == 0:
|
||||
bp.r.reset()
|
||||
return
|
||||
|
||||
let toCommit = min(size, len(bp.r))
|
||||
if len(bp.a) == 0 and len(bp.b) == 0:
|
||||
bp.a.start = bp.r.start
|
||||
bp.a.finish = bp.r.start + toCommit
|
||||
elif bp.r.start == bp.a.finish:
|
||||
bp.a.finish += toCommit
|
||||
else:
|
||||
bp.b.finish += toCommit
|
||||
bp.r.reset()
|
||||
|
||||
proc consume*(bp: var BipBuffer, size: Natural) =
|
||||
## The procedure removes/frees `size` bytes from the buffer ``bp``.
|
||||
var currentSize = size
|
||||
if currentSize >= len(bp.a):
|
||||
currentSize -= len(bp.a)
|
||||
bp.a = bp.b
|
||||
bp.b.reset()
|
||||
if currentSize >= len(bp.a):
|
||||
currentSize -= len(bp.a)
|
||||
bp.a.reset()
|
||||
else:
|
||||
bp.a.start += currentSize
|
||||
else:
|
||||
bp.a.start += currentSize
|
||||
|
||||
iterator items*(bp: BipBuffer): byte =
|
||||
## Iterates over all the bytes in the buffer.
|
||||
for index in bp.a.start ..< bp.a.finish:
|
||||
yield bp.data[index]
|
||||
for index in bp.b.start ..< bp.b.finish:
|
||||
yield bp.data[index]
|
||||
|
||||
iterator regions*(bp: var BipBuffer): tuple[data: ptr byte, size: Natural] =
|
||||
## Iterates over all the regions (`a` and `b`) in the buffer.
|
||||
if len(bp.a) > 0:
|
||||
yield (addr bp.data[bp.a.start], len(bp.a))
|
||||
if len(bp.b) > 0:
|
||||
yield (addr bp.data[bp.b.start], len(bp.b))
|
||||
@ -11,64 +11,100 @@
|
||||
## `chronosDebug` can be defined to enable several debugging helpers that come
|
||||
## with a runtime cost - it is recommeneded to not enable these in production
|
||||
## code.
|
||||
when (NimMajor, NimMinor) >= (1, 4):
|
||||
const
|
||||
chronosStrictException* {.booldefine.}: bool = defined(chronosPreviewV4)
|
||||
## Require that `async` code raises only derivatives of `CatchableError`
|
||||
## and not `Exception` - forward declarations, methods and `proc` types
|
||||
## used from within `async` code may need to be be explicitly annotated
|
||||
## with `raises: [CatchableError]` when this mode is enabled.
|
||||
##
|
||||
## In this file we also declare feature flags starting with `chronosHas...` -
|
||||
## these constants are declared when a feature exists in a particular release -
|
||||
## each flag is declared as an integer starting at 0 during experimental
|
||||
## development, 1 when feature complete and higher numbers when significant
|
||||
## functionality has been added. If a feature ends up being removed (or changed
|
||||
## in a backwards-incompatible way), the feature flag will be removed or renamed
|
||||
## also - you can use `when declared(chronosHasXxx): when chronosHasXxx >= N:`
|
||||
## to require a particular version.
|
||||
const
|
||||
chronosHandleException* {.booldefine.}: bool = false
|
||||
## Remap `Exception` to `AsyncExceptionError` for all `async` functions.
|
||||
##
|
||||
## This modes provides backwards compatibility when using functions with
|
||||
## inaccurate `{.raises.}` effects such as unannotated forward declarations,
|
||||
## methods and `proc` types - it is recommened to annotate such code
|
||||
## explicitly as the `Exception` handling mode may introduce surprising
|
||||
## behavior in exception handlers, should `Exception` actually be raised.
|
||||
##
|
||||
## The setting provides the default for the per-function-based
|
||||
## `handleException` parameter which has precedence over this global setting.
|
||||
##
|
||||
## `Exception` handling may be removed in future chronos versions.
|
||||
|
||||
chronosStrictFutureAccess* {.booldefine.}: bool = defined(chronosPreviewV4)
|
||||
chronosStrictFutureAccess* {.booldefine.}: bool = defined(chronosPreviewV4)
|
||||
|
||||
chronosStackTrace* {.booldefine.}: bool = defined(chronosDebug)
|
||||
## Include stack traces in futures for creation and completion points
|
||||
chronosStackTrace* {.booldefine.}: bool = defined(chronosDebug)
|
||||
## Include stack traces in futures for creation and completion points
|
||||
|
||||
chronosFutureId* {.booldefine.}: bool = defined(chronosDebug)
|
||||
## Generate a unique `id` for every future - when disabled, the address of
|
||||
## the future will be used instead
|
||||
chronosFutureId* {.booldefine.}: bool = defined(chronosDebug)
|
||||
## Generate a unique `id` for every future - when disabled, the address of
|
||||
## the future will be used instead
|
||||
|
||||
chronosFutureTracking* {.booldefine.}: bool = defined(chronosDebug)
|
||||
## Keep track of all pending futures and allow iterating over them -
|
||||
## useful for detecting hung tasks
|
||||
chronosFutureTracking* {.booldefine.}: bool = defined(chronosDebug)
|
||||
## Keep track of all pending futures and allow iterating over them -
|
||||
## useful for detecting hung tasks
|
||||
|
||||
chronosDumpAsync* {.booldefine.}: bool = defined(nimDumpAsync)
|
||||
## Print code generated by {.async.} transformation
|
||||
chronosDumpAsync* {.booldefine.}: bool = defined(nimDumpAsync)
|
||||
## Print code generated by {.async.} transformation
|
||||
|
||||
chronosProcShell* {.strdefine.}: string =
|
||||
when defined(windows):
|
||||
"cmd.exe"
|
||||
chronosProcShell* {.strdefine.}: string =
|
||||
when defined(windows):
|
||||
"cmd.exe"
|
||||
else:
|
||||
when defined(android):
|
||||
"/system/bin/sh"
|
||||
else:
|
||||
when defined(android):
|
||||
"/system/bin/sh"
|
||||
else:
|
||||
"/bin/sh"
|
||||
## Default shell binary path.
|
||||
##
|
||||
## The shell is used as command for command line when process started
|
||||
## using `AsyncProcessOption.EvalCommand` and API calls such as
|
||||
## ``execCommand(command)`` and ``execCommandEx(command)``.
|
||||
"/bin/sh"
|
||||
## Default shell binary path.
|
||||
##
|
||||
## The shell is used as command for command line when process started
|
||||
## using `AsyncProcessOption.EvalCommand` and API calls such as
|
||||
## ``execCommand(command)`` and ``execCommandEx(command)``.
|
||||
|
||||
else:
|
||||
# 1.2 doesn't support `booldefine` in `when` properly
|
||||
const
|
||||
chronosStrictException*: bool =
|
||||
defined(chronosPreviewV4) or defined(chronosStrictException)
|
||||
chronosStrictFutureAccess*: bool =
|
||||
defined(chronosPreviewV4) or defined(chronosStrictFutureAccess)
|
||||
chronosStackTrace*: bool = defined(chronosDebug) or defined(chronosStackTrace)
|
||||
chronosFutureId*: bool = defined(chronosDebug) or defined(chronosFutureId)
|
||||
chronosFutureTracking*: bool =
|
||||
defined(chronosDebug) or defined(chronosFutureTracking)
|
||||
chronosDumpAsync*: bool = defined(nimDumpAsync)
|
||||
chronosProcShell* {.strdefine.}: string =
|
||||
when defined(windows):
|
||||
"cmd.exe"
|
||||
else:
|
||||
when defined(android):
|
||||
"/system/bin/sh"
|
||||
else:
|
||||
"/bin/sh"
|
||||
chronosEventsCount* {.intdefine.} = 64
|
||||
## Number of OS poll events retrieved by syscall (epoll, kqueue, poll).
|
||||
|
||||
chronosInitialSize* {.intdefine.} = 64
|
||||
## Initial size of Selector[T]'s array of file descriptors.
|
||||
|
||||
chronosEventEngine* {.strdefine.}: string =
|
||||
when defined(nimdoc):
|
||||
""
|
||||
elif defined(linux) and not(defined(android) or defined(emscripten)):
|
||||
"epoll"
|
||||
elif defined(macosx) or defined(macos) or defined(ios) or
|
||||
defined(freebsd) or defined(netbsd) or defined(openbsd) or
|
||||
defined(dragonfly):
|
||||
"kqueue"
|
||||
elif defined(android) or defined(emscripten):
|
||||
"poll"
|
||||
elif defined(posix):
|
||||
"poll"
|
||||
else:
|
||||
""
|
||||
## OS polling engine type which is going to be used by chronos.
|
||||
|
||||
chronosHasRaises* = 0
|
||||
## raises effect support via `async: (raises: [])`
|
||||
|
||||
chronosTransportDefaultBufferSize* {.intdefine.} = 16384
|
||||
## Default size of chronos transport internal buffer.
|
||||
|
||||
chronosStreamDefaultBufferSize* {.intdefine.} = 16384
|
||||
## Default size of chronos async stream internal buffer.
|
||||
|
||||
chronosTLSSessionCacheBufferSize* {.intdefine.} = 4096
|
||||
## Default size of chronos TLS Session cache's internal buffer.
|
||||
|
||||
when defined(chronosStrictException):
|
||||
{.warning: "-d:chronosStrictException has been deprecated in favor of handleException".}
|
||||
# In chronos v3, this setting was used as the opposite of
|
||||
# `chronosHandleException` - the setting is deprecated to encourage
|
||||
# migration to the new mode.
|
||||
|
||||
when defined(debug) or defined(chronosConfig):
|
||||
import std/macros
|
||||
@ -77,9 +113,54 @@ when defined(debug) or defined(chronosConfig):
|
||||
hint("Chronos configuration:")
|
||||
template printOption(name: string, value: untyped) =
|
||||
hint(name & ": " & $value)
|
||||
printOption("chronosStrictException", chronosStrictException)
|
||||
printOption("chronosHandleException", chronosHandleException)
|
||||
printOption("chronosStackTrace", chronosStackTrace)
|
||||
printOption("chronosFutureId", chronosFutureId)
|
||||
printOption("chronosFutureTracking", chronosFutureTracking)
|
||||
printOption("chronosDumpAsync", chronosDumpAsync)
|
||||
printOption("chronosProcShell", chronosProcShell)
|
||||
printOption("chronosEventEngine", chronosEventEngine)
|
||||
printOption("chronosEventsCount", chronosEventsCount)
|
||||
printOption("chronosInitialSize", chronosInitialSize)
|
||||
printOption("chronosTransportDefaultBufferSize",
|
||||
chronosTransportDefaultBufferSize)
|
||||
printOption("chronosStreamDefaultBufferSize",
|
||||
chronosStreamDefaultBufferSize)
|
||||
printOption("chronosTLSSessionCacheBufferSize",
|
||||
chronosTLSSessionCacheBufferSize)
|
||||
|
||||
# In nim 1.6, `sink` + local variable + `move` generates the best code for
|
||||
# moving a proc parameter into a closure - this only works for closure
|
||||
# procedures however - in closure iterators, the parameter is always copied
|
||||
# into the closure (!) meaning that non-raw `{.async.}` functions always carry
|
||||
# this overhead, sink or no. See usages of chronosMoveSink for examples.
|
||||
# In addition, we need to work around https://github.com/nim-lang/Nim/issues/22175
|
||||
# which has not been backported to 1.6.
|
||||
# Long story short, the workaround is not needed in non-raw {.async.} because
|
||||
# a copy of the literal is always made.
|
||||
# TODO review the above for 2.0 / 2.0+refc
|
||||
type
|
||||
SeqHeader = object
|
||||
length, reserved: int
|
||||
|
||||
proc isLiteral(s: string): bool {.inline.} =
|
||||
when defined(gcOrc) or defined(gcArc):
|
||||
false
|
||||
else:
|
||||
s.len > 0 and (cast[ptr SeqHeader](s).reserved and (1 shl (sizeof(int) * 8 - 2))) != 0
|
||||
|
||||
proc isLiteral[T](s: seq[T]): bool {.inline.} =
|
||||
when defined(gcOrc) or defined(gcArc):
|
||||
false
|
||||
else:
|
||||
s.len > 0 and (cast[ptr SeqHeader](s).reserved and (1 shl (sizeof(int) * 8 - 2))) != 0
|
||||
|
||||
template chronosMoveSink*(val: auto): untyped =
|
||||
bind isLiteral
|
||||
when not (defined(gcOrc) or defined(gcArc)) and val is seq|string:
|
||||
if isLiteral(val):
|
||||
val
|
||||
else:
|
||||
move(val)
|
||||
else:
|
||||
move(val)
|
||||
|
||||
@ -17,11 +17,6 @@ export srcloc
|
||||
when chronosStackTrace:
|
||||
type StackTrace = string
|
||||
|
||||
when chronosStrictException:
|
||||
{.pragma: closureIter, raises: [CatchableError], gcsafe.}
|
||||
else:
|
||||
{.pragma: closureIter, raises: [Exception], gcsafe.}
|
||||
|
||||
type
|
||||
LocationKind* {.pure.} = enum
|
||||
Create
|
||||
@ -37,6 +32,24 @@ type
|
||||
FutureState* {.pure.} = enum
|
||||
Pending, Completed, Cancelled, Failed
|
||||
|
||||
FutureFlag* {.pure.} = enum
|
||||
OwnCancelSchedule
|
||||
## When OwnCancelSchedule is set, the owner of the future is responsible
|
||||
## for implementing cancellation in one of 3 ways:
|
||||
##
|
||||
## * ensure that cancellation requests never reach the future by means of
|
||||
## not exposing it to user code, `await` and `tryCancel`
|
||||
## * set `cancelCallback` to `nil` to stop cancellation propagation - this
|
||||
## is appropriate when it is expected that the future will be completed
|
||||
## in a regular way "soon"
|
||||
## * set `cancelCallback` to a handler that implements cancellation in an
|
||||
## operation-specific way
|
||||
##
|
||||
## If `cancelCallback` is not set and the future gets cancelled, a
|
||||
## `Defect` will be raised.
|
||||
|
||||
FutureFlags* = set[FutureFlag]
|
||||
|
||||
InternalFutureBase* = object of RootObj
|
||||
# Internal untyped future representation - the fields are not part of the
|
||||
# public API and neither is `InternalFutureBase`, ie the inheritance
|
||||
@ -47,9 +60,9 @@ type
|
||||
internalCancelcb*: CallbackFunc
|
||||
internalChild*: FutureBase
|
||||
internalState*: FutureState
|
||||
internalFlags*: FutureFlags
|
||||
internalError*: ref CatchableError ## Stored exception
|
||||
internalMustCancel*: bool
|
||||
internalClosure*: iterator(f: FutureBase): FutureBase {.closureIter.}
|
||||
internalClosure*: iterator(f: FutureBase): FutureBase {.raises: [], gcsafe.}
|
||||
|
||||
when chronosFutureId:
|
||||
internalId*: uint
|
||||
@ -73,10 +86,15 @@ type
|
||||
cause*: FutureBase
|
||||
|
||||
FutureError* = object of CatchableError
|
||||
future*: FutureBase
|
||||
|
||||
CancelledError* = object of FutureError
|
||||
## Exception raised when accessing the value of a cancelled future
|
||||
|
||||
func raiseFutureDefect(msg: static string, fut: FutureBase) {.
|
||||
noinline, noreturn.} =
|
||||
raise (ref FutureDefect)(msg: msg, cause: fut)
|
||||
|
||||
when chronosFutureId:
|
||||
var currentID* {.threadvar.}: uint
|
||||
template id*(fut: FutureBase): uint = fut.internalId
|
||||
@ -94,12 +112,17 @@ when chronosFutureTracking:
|
||||
var futureList* {.threadvar.}: FutureList
|
||||
|
||||
# Internal utilities - these are not part of the stable API
|
||||
proc internalInitFutureBase*(
|
||||
fut: FutureBase,
|
||||
loc: ptr SrcLoc,
|
||||
state: FutureState) =
|
||||
proc internalInitFutureBase*(fut: FutureBase, loc: ptr SrcLoc,
|
||||
state: FutureState, flags: FutureFlags) =
|
||||
fut.internalState = state
|
||||
fut.internalLocation[LocationKind.Create] = loc
|
||||
fut.internalFlags = flags
|
||||
if FutureFlag.OwnCancelSchedule in flags:
|
||||
# Owners must replace `cancelCallback` with `nil` if they want to ignore
|
||||
# cancellations
|
||||
fut.internalCancelcb = proc(_: pointer) =
|
||||
raiseAssert "Cancellation request for non-cancellable future"
|
||||
|
||||
if state != FutureState.Pending:
|
||||
fut.internalLocation[LocationKind.Finish] = loc
|
||||
|
||||
@ -128,21 +151,34 @@ template init*[T](F: type Future[T], fromProc: static[string] = ""): Future[T] =
|
||||
## Specifying ``fromProc``, which is a string specifying the name of the proc
|
||||
## that this future belongs to, is a good habit as it helps with debugging.
|
||||
let res = Future[T]()
|
||||
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Pending)
|
||||
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Pending, {})
|
||||
res
|
||||
|
||||
template init*[T](F: type Future[T], fromProc: static[string] = "",
|
||||
flags: static[FutureFlags]): Future[T] =
|
||||
## Creates a new pending future.
|
||||
##
|
||||
## Specifying ``fromProc``, which is a string specifying the name of the proc
|
||||
## that this future belongs to, is a good habit as it helps with debugging.
|
||||
let res = Future[T]()
|
||||
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Pending,
|
||||
flags)
|
||||
res
|
||||
|
||||
template completed*(
|
||||
F: type Future, fromProc: static[string] = ""): Future[void] =
|
||||
## Create a new completed future
|
||||
let res = Future[T]()
|
||||
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Completed)
|
||||
let res = Future[void]()
|
||||
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Completed,
|
||||
{})
|
||||
res
|
||||
|
||||
template completed*[T: not void](
|
||||
F: type Future, valueParam: T, fromProc: static[string] = ""): Future[T] =
|
||||
## Create a new completed future
|
||||
let res = Future[T](internalValue: valueParam)
|
||||
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Completed)
|
||||
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Completed,
|
||||
{})
|
||||
res
|
||||
|
||||
template failed*[T](
|
||||
@ -150,19 +186,21 @@ template failed*[T](
|
||||
fromProc: static[string] = ""): Future[T] =
|
||||
## Create a new failed future
|
||||
let res = Future[T](internalError: errorParam)
|
||||
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Failed)
|
||||
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Failed, {})
|
||||
when chronosStackTrace:
|
||||
res.internalErrorStackTrace =
|
||||
if getStackTrace(res.error) == "":
|
||||
getStackTrace()
|
||||
else:
|
||||
getStackTrace(res.error)
|
||||
|
||||
res
|
||||
|
||||
func state*(future: FutureBase): FutureState =
|
||||
future.internalState
|
||||
|
||||
func flags*(future: FutureBase): FutureFlags =
|
||||
future.internalFlags
|
||||
|
||||
func finished*(future: FutureBase): bool {.inline.} =
|
||||
## Determines whether ``future`` has finished, i.e. ``future`` state changed
|
||||
## from state ``Pending`` to one of the states (``Finished``, ``Cancelled``,
|
||||
@ -184,20 +222,27 @@ func completed*(future: FutureBase): bool {.inline.} =
|
||||
func location*(future: FutureBase): array[LocationKind, ptr SrcLoc] =
|
||||
future.internalLocation
|
||||
|
||||
func value*[T](future: Future[T]): T =
|
||||
func value*[T: not void](future: Future[T]): lent T =
|
||||
## Return the value in a completed future - raises Defect when
|
||||
## `fut.completed()` is `false`.
|
||||
##
|
||||
## See `read` for a version that raises an catchable error when future
|
||||
## See `read` for a version that raises a catchable error when future
|
||||
## has not completed.
|
||||
when chronosStrictFutureAccess:
|
||||
if not future.completed():
|
||||
raise (ref FutureDefect)(
|
||||
msg: "Future not completed while accessing value",
|
||||
cause: future)
|
||||
raiseFutureDefect("Future not completed while accessing value", future)
|
||||
|
||||
when T isnot void:
|
||||
future.internalValue
|
||||
future.internalValue
|
||||
|
||||
func value*(future: Future[void]) =
|
||||
## Return the value in a completed future - raises Defect when
|
||||
## `fut.completed()` is `false`.
|
||||
##
|
||||
## See `read` for a version that raises a catchable error when future
|
||||
## has not completed.
|
||||
when chronosStrictFutureAccess:
|
||||
if not future.completed():
|
||||
raiseFutureDefect("Future not completed while accessing value", future)
|
||||
|
||||
func error*(future: FutureBase): ref CatchableError =
|
||||
## Return the error of `future`, or `nil` if future did not fail.
|
||||
@ -206,9 +251,8 @@ func error*(future: FutureBase): ref CatchableError =
|
||||
## future has not failed.
|
||||
when chronosStrictFutureAccess:
|
||||
if not future.failed() and not future.cancelled():
|
||||
raise (ref FutureDefect)(
|
||||
msg: "Future not failed/cancelled while accessing error",
|
||||
cause: future)
|
||||
raiseFutureDefect(
|
||||
"Future not failed/cancelled while accessing error", future)
|
||||
|
||||
future.internalError
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
{.push raises: [].}
|
||||
|
||||
import "."/[asyncloop, osdefs, osutils]
|
||||
import stew/results
|
||||
import results
|
||||
from nativesockets import Domain, Protocol, SockType, toInt
|
||||
export Domain, Protocol, SockType, results
|
||||
|
||||
@ -21,66 +21,113 @@ const
|
||||
asyncInvalidSocket* = AsyncFD(osdefs.INVALID_SOCKET)
|
||||
asyncInvalidPipe* = asyncInvalidSocket
|
||||
|
||||
proc setSocketBlocking*(s: SocketHandle, blocking: bool): bool =
|
||||
proc setSocketBlocking*(s: SocketHandle, blocking: bool): bool {.
|
||||
deprecated: "Please use setDescriptorBlocking() instead".} =
|
||||
## Sets blocking mode on socket.
|
||||
when defined(windows) or defined(nimdoc):
|
||||
var mode = clong(ord(not blocking))
|
||||
if osdefs.ioctlsocket(s, osdefs.FIONBIO, addr(mode)) == -1:
|
||||
false
|
||||
else:
|
||||
true
|
||||
else:
|
||||
let x: int = osdefs.fcntl(s, osdefs.F_GETFL, 0)
|
||||
if x == -1:
|
||||
false
|
||||
else:
|
||||
let mode =
|
||||
if blocking: x and not osdefs.O_NONBLOCK else: x or osdefs.O_NONBLOCK
|
||||
if osdefs.fcntl(s, osdefs.F_SETFL, mode) == -1:
|
||||
false
|
||||
else:
|
||||
true
|
||||
setDescriptorBlocking(s, blocking).isOkOr:
|
||||
return false
|
||||
true
|
||||
|
||||
proc setSockOpt*(socket: AsyncFD, level, optname, optval: int): bool =
|
||||
## `setsockopt()` for integer options.
|
||||
## Returns ``true`` on success, ``false`` on error.
|
||||
proc setSockOpt2*(socket: AsyncFD,
|
||||
level, optname, optval: int): Result[void, OSErrorCode] =
|
||||
var value = cint(optval)
|
||||
osdefs.setsockopt(SocketHandle(socket), cint(level), cint(optname),
|
||||
addr(value), SockLen(sizeof(value))) >= cint(0)
|
||||
let res = osdefs.setsockopt(SocketHandle(socket), cint(level), cint(optname),
|
||||
addr(value), SockLen(sizeof(value)))
|
||||
if res == -1:
|
||||
return err(osLastError())
|
||||
ok()
|
||||
|
||||
proc setSockOpt*(socket: AsyncFD, level, optname: int, value: pointer,
|
||||
valuelen: int): bool =
|
||||
proc setSockOpt2*(socket: AsyncFD, level, optname: int, value: pointer,
|
||||
valuelen: int): Result[void, OSErrorCode] =
|
||||
## `setsockopt()` for custom options (pointer and length).
|
||||
## Returns ``true`` on success, ``false`` on error.
|
||||
osdefs.setsockopt(SocketHandle(socket), cint(level), cint(optname), value,
|
||||
SockLen(valuelen)) >= cint(0)
|
||||
let res = osdefs.setsockopt(SocketHandle(socket), cint(level), cint(optname),
|
||||
value, SockLen(valuelen))
|
||||
if res == -1:
|
||||
return err(osLastError())
|
||||
ok()
|
||||
|
||||
proc getSockOpt*(socket: AsyncFD, level, optname: int, value: var int): bool =
|
||||
proc setSockOpt*(socket: AsyncFD, level, optname, optval: int): bool {.
|
||||
deprecated: "Please use setSockOpt2() instead".} =
|
||||
## `setsockopt()` for integer options.
|
||||
## Returns ``true`` on success, ``false`` on error.
|
||||
setSockOpt2(socket, level, optname, optval).isOk
|
||||
|
||||
proc setSockOpt*(socket: AsyncFD, level, optname: int, value: pointer,
|
||||
valuelen: int): bool {.
|
||||
deprecated: "Please use setSockOpt2() instead".} =
|
||||
## `setsockopt()` for custom options (pointer and length).
|
||||
## Returns ``true`` on success, ``false`` on error.
|
||||
setSockOpt2(socket, level, optname, value, valuelen).isOk
|
||||
|
||||
proc getSockOpt2*(socket: AsyncFD,
|
||||
level, optname: int): Result[cint, OSErrorCode] =
|
||||
var
|
||||
value: cint
|
||||
size = SockLen(sizeof(value))
|
||||
let res = osdefs.getsockopt(SocketHandle(socket), cint(level), cint(optname),
|
||||
addr(value), addr(size))
|
||||
if res == -1:
|
||||
return err(osLastError())
|
||||
ok(value)
|
||||
|
||||
proc getSockOpt2*(socket: AsyncFD, level, optname: int,
|
||||
T: type): Result[T, OSErrorCode] =
|
||||
var
|
||||
value = default(T)
|
||||
size = SockLen(sizeof(value))
|
||||
let res = osdefs.getsockopt(SocketHandle(socket), cint(level), cint(optname),
|
||||
cast[ptr byte](addr(value)), addr(size))
|
||||
if res == -1:
|
||||
return err(osLastError())
|
||||
ok(value)
|
||||
|
||||
proc getSockOpt*(socket: AsyncFD, level, optname: int, value: var int): bool {.
|
||||
deprecated: "Please use getSockOpt2() instead".} =
|
||||
## `getsockopt()` for integer options.
|
||||
## Returns ``true`` on success, ``false`` on error.
|
||||
var res: cint
|
||||
var size = SockLen(sizeof(res))
|
||||
if osdefs.getsockopt(SocketHandle(socket), cint(level), cint(optname),
|
||||
addr(res), addr(size)) >= cint(0):
|
||||
value = int(res)
|
||||
true
|
||||
else:
|
||||
false
|
||||
value = getSockOpt2(socket, level, optname).valueOr:
|
||||
return false
|
||||
true
|
||||
|
||||
proc getSockOpt*(socket: AsyncFD, level, optname: int, value: pointer,
|
||||
valuelen: var int): bool =
|
||||
proc getSockOpt*(socket: AsyncFD, level, optname: int, value: var pointer,
|
||||
valuelen: var int): bool {.
|
||||
deprecated: "Please use getSockOpt2() instead".} =
|
||||
## `getsockopt()` for custom options (pointer and length).
|
||||
## Returns ``true`` on success, ``false`` on error.
|
||||
osdefs.getsockopt(SocketHandle(socket), cint(level), cint(optname),
|
||||
value, cast[ptr SockLen](addr valuelen)) >= cint(0)
|
||||
|
||||
proc getSocketError*(socket: AsyncFD, err: var int): bool =
|
||||
proc getSocketError*(socket: AsyncFD, err: var int): bool {.
|
||||
deprecated: "Please use getSocketError() instead".} =
|
||||
## Recover error code associated with socket handle ``socket``.
|
||||
getSockOpt(socket, cint(osdefs.SOL_SOCKET), cint(osdefs.SO_ERROR), err)
|
||||
err = getSockOpt2(socket, cint(osdefs.SOL_SOCKET),
|
||||
cint(osdefs.SO_ERROR)).valueOr:
|
||||
return false
|
||||
true
|
||||
|
||||
proc getSocketError2*(socket: AsyncFD): Result[cint, OSErrorCode] =
|
||||
getSockOpt2(socket, cint(osdefs.SOL_SOCKET), cint(osdefs.SO_ERROR))
|
||||
|
||||
proc isAvailable*(domain: Domain): bool =
|
||||
when defined(windows):
|
||||
let fd = wsaSocket(toInt(domain), toInt(SockType.SOCK_STREAM),
|
||||
toInt(Protocol.IPPROTO_TCP), nil, GROUP(0), 0'u32)
|
||||
if fd == osdefs.INVALID_SOCKET:
|
||||
return if osLastError() == osdefs.WSAEAFNOSUPPORT: false else: true
|
||||
discard closeFd(fd)
|
||||
true
|
||||
else:
|
||||
let fd = osdefs.socket(toInt(domain), toInt(SockType.SOCK_STREAM),
|
||||
toInt(Protocol.IPPROTO_TCP))
|
||||
if fd == -1:
|
||||
return if osLastError() == osdefs.EAFNOSUPPORT: false else: true
|
||||
discard closeFd(fd)
|
||||
true
|
||||
|
||||
proc createAsyncSocket2*(domain: Domain, sockType: SockType,
|
||||
protocol: Protocol,
|
||||
inherit = true): Result[AsyncFD, OSErrorCode] =
|
||||
protocol: Protocol,
|
||||
inherit = true): Result[AsyncFD, OSErrorCode] =
|
||||
## Creates new asynchronous socket.
|
||||
when defined(windows):
|
||||
let flags =
|
||||
@ -93,15 +140,12 @@ proc createAsyncSocket2*(domain: Domain, sockType: SockType,
|
||||
if fd == osdefs.INVALID_SOCKET:
|
||||
return err(osLastError())
|
||||
|
||||
let bres = setDescriptorBlocking(fd, false)
|
||||
if bres.isErr():
|
||||
setDescriptorBlocking(fd, false).isOkOr:
|
||||
discard closeFd(fd)
|
||||
return err(bres.error())
|
||||
|
||||
let res = register2(AsyncFD(fd))
|
||||
if res.isErr():
|
||||
return err(error)
|
||||
register2(AsyncFD(fd)).isOkOr:
|
||||
discard closeFd(fd)
|
||||
return err(res.error())
|
||||
return err(error)
|
||||
|
||||
ok(AsyncFD(fd))
|
||||
else:
|
||||
@ -114,23 +158,20 @@ proc createAsyncSocket2*(domain: Domain, sockType: SockType,
|
||||
let fd = osdefs.socket(toInt(domain), socketType, toInt(protocol))
|
||||
if fd == -1:
|
||||
return err(osLastError())
|
||||
let res = register2(AsyncFD(fd))
|
||||
if res.isErr():
|
||||
register2(AsyncFD(fd)).isOkOr:
|
||||
discard closeFd(fd)
|
||||
return err(res.error())
|
||||
return err(error)
|
||||
ok(AsyncFD(fd))
|
||||
else:
|
||||
let fd = osdefs.socket(toInt(domain), toInt(sockType), toInt(protocol))
|
||||
if fd == -1:
|
||||
return err(osLastError())
|
||||
let bres = setDescriptorFlags(cint(fd), true, true)
|
||||
if bres.isErr():
|
||||
setDescriptorFlags(cint(fd), true, true).isOkOr:
|
||||
discard closeFd(fd)
|
||||
return err(bres.error())
|
||||
let res = register2(AsyncFD(fd))
|
||||
if res.isErr():
|
||||
return err(error)
|
||||
register2(AsyncFD(fd)).isOkOr:
|
||||
discard closeFd(fd)
|
||||
return err(bres.error())
|
||||
return err(error)
|
||||
ok(AsyncFD(fd))
|
||||
|
||||
proc wrapAsyncSocket2*(sock: cint|SocketHandle): Result[AsyncFD, OSErrorCode] =
|
||||
@ -230,3 +271,26 @@ proc createAsyncPipe*(): tuple[read: AsyncFD, write: AsyncFD] =
|
||||
else:
|
||||
let pipes = res.get()
|
||||
(read: AsyncFD(pipes.read), write: AsyncFD(pipes.write))
|
||||
|
||||
proc getDualstack*(fd: AsyncFD): Result[bool, OSErrorCode] =
|
||||
## Returns `true` if `IPV6_V6ONLY` socket option set to `false`.
|
||||
var
|
||||
flag = cint(0)
|
||||
size = SockLen(sizeof(flag))
|
||||
let res = osdefs.getsockopt(SocketHandle(fd), cint(osdefs.IPPROTO_IPV6),
|
||||
cint(osdefs.IPV6_V6ONLY), addr(flag), addr(size))
|
||||
if res == -1:
|
||||
return err(osLastError())
|
||||
ok(flag == cint(0))
|
||||
|
||||
proc setDualstack*(fd: AsyncFD, value: bool): Result[void, OSErrorCode] =
|
||||
## Sets `IPV6_V6ONLY` socket option value to `false` if `value == true` and
|
||||
## to `true` if `value == false`.
|
||||
var
|
||||
flag = cint(if value: 0 else: 1)
|
||||
size = SockLen(sizeof(flag))
|
||||
let res = osdefs.setsockopt(SocketHandle(fd), cint(osdefs.IPPROTO_IPV6),
|
||||
cint(osdefs.IPV6_V6ONLY), addr(flag), size)
|
||||
if res == -1:
|
||||
return err(osLastError())
|
||||
ok()
|
||||
|
||||
1277
chronos/internal/asyncengine.nim
Normal file
1277
chronos/internal/asyncengine.nim
Normal file
File diff suppressed because it is too large
Load Diff
1930
chronos/internal/asyncfutures.nim
Normal file
1930
chronos/internal/asyncfutures.nim
Normal file
File diff suppressed because it is too large
Load Diff
596
chronos/internal/asyncmacro.nim
Normal file
596
chronos/internal/asyncmacro.nim
Normal file
@ -0,0 +1,596 @@
|
||||
#
|
||||
#
|
||||
# Nim's Runtime Library
|
||||
# (c) Copyright 2015 Dominik Picheta
|
||||
# (c) Copyright 2018-Present Status Research & Development GmbH
|
||||
#
|
||||
# See the file "copying.txt", included in this
|
||||
# distribution, for details about the copyright.
|
||||
#
|
||||
|
||||
import
|
||||
std/[macros],
|
||||
../[futures, config],
|
||||
./raisesfutures
|
||||
|
||||
proc processBody(node, setResultSym: NimNode): NimNode {.compileTime.} =
|
||||
case node.kind
|
||||
of nnkReturnStmt:
|
||||
# `return ...` -> `setResult(...); return`
|
||||
let
|
||||
res = newNimNode(nnkStmtList, node)
|
||||
if node[0].kind != nnkEmpty:
|
||||
res.add newCall(setResultSym, processBody(node[0], setResultSym))
|
||||
res.add newNimNode(nnkReturnStmt, node).add(newEmptyNode())
|
||||
|
||||
res
|
||||
of RoutineNodes-{nnkTemplateDef}:
|
||||
# Skip nested routines since they have their own return value distinct from
|
||||
# the Future we inject
|
||||
node
|
||||
else:
|
||||
if node.kind == nnkYieldStmt:
|
||||
# asyncdispatch allows `yield` but this breaks cancellation
|
||||
warning(
|
||||
"`yield` in async procedures not supported - use `awaitne` instead",
|
||||
node)
|
||||
|
||||
for i in 0 ..< node.len:
|
||||
node[i] = processBody(node[i], setResultSym)
|
||||
node
|
||||
|
||||
proc wrapInTryFinally(
|
||||
fut, baseType, body, raises: NimNode,
|
||||
handleException: bool): NimNode {.compileTime.} =
|
||||
# creates:
|
||||
# try: `body`
|
||||
# [for raise in raises]:
|
||||
# except `raise`: closureSucceeded = false; `castFutureSym`.fail(exc)
|
||||
# finally:
|
||||
# if closureSucceeded:
|
||||
# `castFutureSym`.complete(result)
|
||||
#
|
||||
# Calling `complete` inside `finally` ensures that all success paths
|
||||
# (including early returns and code inside nested finally statements and
|
||||
# defer) are completed with the final contents of `result`
|
||||
let
|
||||
closureSucceeded = genSym(nskVar, "closureSucceeded")
|
||||
nTry = nnkTryStmt.newTree(body)
|
||||
excName = ident"exc"
|
||||
|
||||
# Depending on the exception type, we must have at most one of each of these
|
||||
# "special" exception handlers that are needed to implement cancellation and
|
||||
# Defect propagation
|
||||
var
|
||||
hasDefect = false
|
||||
hasCancelledError = false
|
||||
hasCatchableError = false
|
||||
|
||||
template addDefect =
|
||||
if not hasDefect:
|
||||
hasDefect = true
|
||||
# When a Defect is raised, the program is in an undefined state and
|
||||
# continuing running other tasks while the Future completion sits on the
|
||||
# callback queue may lead to further damage so we re-raise them eagerly.
|
||||
nTry.add nnkExceptBranch.newTree(
|
||||
nnkInfix.newTree(ident"as", ident"Defect", excName),
|
||||
nnkStmtList.newTree(
|
||||
nnkAsgn.newTree(closureSucceeded, ident"false"),
|
||||
nnkRaiseStmt.newTree(excName)
|
||||
)
|
||||
)
|
||||
template addCancelledError =
|
||||
if not hasCancelledError:
|
||||
hasCancelledError = true
|
||||
nTry.add nnkExceptBranch.newTree(
|
||||
ident"CancelledError",
|
||||
nnkStmtList.newTree(
|
||||
nnkAsgn.newTree(closureSucceeded, ident"false"),
|
||||
newCall(ident "cancelAndSchedule", fut)
|
||||
)
|
||||
)
|
||||
|
||||
template addCatchableError =
|
||||
if not hasCatchableError:
|
||||
hasCatchableError = true
|
||||
nTry.add nnkExceptBranch.newTree(
|
||||
nnkInfix.newTree(ident"as", ident"CatchableError", excName),
|
||||
nnkStmtList.newTree(
|
||||
nnkAsgn.newTree(closureSucceeded, ident"false"),
|
||||
newCall(ident "fail", fut, excName)
|
||||
))
|
||||
|
||||
var raises = if raises == nil:
|
||||
nnkTupleConstr.newTree(ident"CatchableError")
|
||||
elif isNoRaises(raises):
|
||||
nnkTupleConstr.newTree()
|
||||
else:
|
||||
raises.copyNimTree()
|
||||
|
||||
if handleException:
|
||||
raises.add(ident"Exception")
|
||||
|
||||
for exc in raises:
|
||||
if exc.eqIdent("Exception"):
|
||||
addCancelledError
|
||||
addCatchableError
|
||||
addDefect
|
||||
|
||||
# Because we store `CatchableError` in the Future, we cannot re-raise the
|
||||
# original exception
|
||||
nTry.add nnkExceptBranch.newTree(
|
||||
nnkInfix.newTree(ident"as", ident"Exception", excName),
|
||||
newCall(ident "fail", fut,
|
||||
nnkStmtList.newTree(
|
||||
nnkAsgn.newTree(closureSucceeded, ident"false"),
|
||||
quote do:
|
||||
(ref AsyncExceptionError)(
|
||||
msg: `excName`.msg, parent: `excName`)))
|
||||
)
|
||||
elif exc.eqIdent("CancelledError"):
|
||||
addCancelledError
|
||||
elif exc.eqIdent("CatchableError"):
|
||||
# Ensure cancellations are re-routed to the cancellation handler even if
|
||||
# not explicitly specified in the raises list
|
||||
addCancelledError
|
||||
addCatchableError
|
||||
else:
|
||||
nTry.add nnkExceptBranch.newTree(
|
||||
nnkInfix.newTree(ident"as", exc, excName),
|
||||
nnkStmtList.newTree(
|
||||
nnkAsgn.newTree(closureSucceeded, ident"false"),
|
||||
newCall(ident "fail", fut, excName)
|
||||
))
|
||||
|
||||
addDefect # Must not complete future on defect
|
||||
|
||||
nTry.add nnkFinally.newTree(
|
||||
nnkIfStmt.newTree(
|
||||
nnkElifBranch.newTree(
|
||||
closureSucceeded,
|
||||
if baseType.eqIdent("void"): # shortcut for non-generic void
|
||||
newCall(ident "complete", fut)
|
||||
else:
|
||||
nnkWhenStmt.newTree(
|
||||
nnkElifExpr.newTree(
|
||||
nnkInfix.newTree(ident "is", baseType, ident "void"),
|
||||
newCall(ident "complete", fut)
|
||||
),
|
||||
nnkElseExpr.newTree(
|
||||
newCall(ident "complete", fut, newCall(ident "move", ident "result"))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
nnkStmtList.newTree(
|
||||
newVarStmt(closureSucceeded, ident"true"),
|
||||
nTry
|
||||
)
|
||||
|
||||
proc getName(node: NimNode): string {.compileTime.} =
|
||||
case node.kind
|
||||
of nnkSym:
|
||||
return node.strVal
|
||||
of nnkPostfix:
|
||||
return node[1].strVal
|
||||
of nnkIdent:
|
||||
return node.strVal
|
||||
of nnkEmpty:
|
||||
return "anonymous"
|
||||
else:
|
||||
error("Unknown name.")
|
||||
|
||||
macro unsupported(s: static[string]): untyped =
|
||||
error s
|
||||
|
||||
proc params2(someProc: NimNode): NimNode {.compileTime.} =
|
||||
# until https://github.com/nim-lang/Nim/pull/19563 is available
|
||||
if someProc.kind == nnkProcTy:
|
||||
someProc[0]
|
||||
else:
|
||||
params(someProc)
|
||||
|
||||
proc cleanupOpenSymChoice(node: NimNode): NimNode {.compileTime.} =
|
||||
# Replace every Call -> OpenSymChoice by a Bracket expr
|
||||
# ref https://github.com/nim-lang/Nim/issues/11091
|
||||
if node.kind in nnkCallKinds and
|
||||
node[0].kind == nnkOpenSymChoice and node[0].eqIdent("[]"):
|
||||
result = newNimNode(nnkBracketExpr)
|
||||
for child in node[1..^1]:
|
||||
result.add(cleanupOpenSymChoice(child))
|
||||
else:
|
||||
result = node.copyNimNode()
|
||||
for child in node:
|
||||
result.add(cleanupOpenSymChoice(child))
|
||||
|
||||
type
|
||||
AsyncParams = tuple
|
||||
raw: bool
|
||||
raises: NimNode
|
||||
handleException: bool
|
||||
|
||||
proc decodeParams(params: NimNode): AsyncParams =
|
||||
# decodes the parameter tuple given in `async: (name: value, ...)` to its
|
||||
# recognised parts
|
||||
params.expectKind(nnkTupleConstr)
|
||||
|
||||
var
|
||||
raw = false
|
||||
raises: NimNode = nil
|
||||
handleException = false
|
||||
hasLocalAnnotations = false
|
||||
|
||||
for param in params:
|
||||
param.expectKind(nnkExprColonExpr)
|
||||
|
||||
if param[0].eqIdent("raises"):
|
||||
hasLocalAnnotations = true
|
||||
param[1].expectKind(nnkBracket)
|
||||
if param[1].len == 0:
|
||||
raises = makeNoRaises()
|
||||
else:
|
||||
raises = nnkTupleConstr.newTree()
|
||||
for possibleRaise in param[1]:
|
||||
raises.add(possibleRaise)
|
||||
elif param[0].eqIdent("raw"):
|
||||
# boolVal doesn't work in untyped macros it seems..
|
||||
raw = param[1].eqIdent("true")
|
||||
elif param[0].eqIdent("handleException"):
|
||||
hasLocalAnnotations = true
|
||||
handleException = param[1].eqIdent("true")
|
||||
else:
|
||||
warning("Unrecognised async parameter: " & repr(param[0]), param)
|
||||
|
||||
if not hasLocalAnnotations:
|
||||
handleException = chronosHandleException
|
||||
|
||||
(raw, raises, handleException)
|
||||
|
||||
proc isEmpty(n: NimNode): bool {.compileTime.} =
|
||||
# true iff node recursively contains only comments or empties
|
||||
case n.kind
|
||||
of nnkEmpty, nnkCommentStmt: true
|
||||
of nnkStmtList:
|
||||
for child in n:
|
||||
if not isEmpty(child): return false
|
||||
true
|
||||
else:
|
||||
false
|
||||
|
||||
proc asyncSingleProc(prc, params: NimNode): NimNode {.compileTime.} =
|
||||
## This macro transforms a single procedure into a closure iterator.
|
||||
## The ``async`` macro supports a stmtList holding multiple async procedures.
|
||||
if prc.kind notin {nnkProcTy, nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}:
|
||||
error("Cannot transform " & $prc.kind & " into an async proc." &
|
||||
" proc/method definition or lambda node expected.", prc)
|
||||
|
||||
for pragma in prc.pragma():
|
||||
if pragma.kind == nnkExprColonExpr and pragma[0].eqIdent("raises"):
|
||||
warning("The raises pragma doesn't work on async procedures - use " &
|
||||
"`async: (raises: [...]) instead.", prc)
|
||||
|
||||
let returnType = cleanupOpenSymChoice(prc.params2[0])
|
||||
|
||||
# Verify that the return type is a Future[T]
|
||||
let baseType =
|
||||
if returnType.kind == nnkEmpty:
|
||||
ident "void"
|
||||
elif not (
|
||||
returnType.kind == nnkBracketExpr and
|
||||
(eqIdent(returnType[0], "Future") or eqIdent(returnType[0], "InternalRaisesFuture"))):
|
||||
error(
|
||||
"Expected return type of 'Future' got '" & repr(returnType) & "'", prc)
|
||||
return
|
||||
else:
|
||||
returnType[1]
|
||||
|
||||
let
|
||||
# When the base type is known to be void (and not generic), we can simplify
|
||||
# code generation - however, in the case of generic async procedures it
|
||||
# could still end up being void, meaning void detection needs to happen
|
||||
# post-macro-expansion.
|
||||
baseTypeIsVoid = baseType.eqIdent("void")
|
||||
(raw, raises, handleException) = decodeParams(params)
|
||||
internalFutureType =
|
||||
if baseTypeIsVoid:
|
||||
newNimNode(nnkBracketExpr, prc).
|
||||
add(newIdentNode("Future")).
|
||||
add(baseType)
|
||||
else:
|
||||
returnType
|
||||
internalReturnType = if raises == nil:
|
||||
internalFutureType
|
||||
else:
|
||||
nnkBracketExpr.newTree(
|
||||
newIdentNode("InternalRaisesFuture"),
|
||||
baseType,
|
||||
raises
|
||||
)
|
||||
|
||||
prc.params2[0] = internalReturnType
|
||||
|
||||
if prc.kind notin {nnkProcTy, nnkLambda}:
|
||||
prc.addPragma(newColonExpr(ident "stackTrace", ident "off"))
|
||||
|
||||
# The proc itself doesn't raise
|
||||
prc.addPragma(
|
||||
nnkExprColonExpr.newTree(newIdentNode("raises"), nnkBracket.newTree()))
|
||||
|
||||
# `gcsafe` isn't deduced even though we require async code to be gcsafe
|
||||
# https://github.com/nim-lang/RFCs/issues/435
|
||||
prc.addPragma(newIdentNode("gcsafe"))
|
||||
|
||||
if raw: # raw async = body is left as-is
|
||||
if raises != nil and prc.kind notin {nnkProcTy, nnkLambda} and not isEmpty(prc.body):
|
||||
# Inject `raises` type marker that causes `newFuture` to return a raise-
|
||||
# tracking future instead of an ordinary future:
|
||||
#
|
||||
# type InternalRaisesFutureRaises = `raisesTuple`
|
||||
# `body`
|
||||
prc.body = nnkStmtList.newTree(
|
||||
nnkTypeSection.newTree(
|
||||
nnkTypeDef.newTree(
|
||||
nnkPragmaExpr.newTree(
|
||||
ident"InternalRaisesFutureRaises",
|
||||
nnkPragma.newTree(ident "used")),
|
||||
newEmptyNode(),
|
||||
raises,
|
||||
)
|
||||
),
|
||||
prc.body
|
||||
)
|
||||
|
||||
elif prc.kind in {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo} and
|
||||
not isEmpty(prc.body):
|
||||
let
|
||||
setResultSym = ident "setResult"
|
||||
procBody = prc.body.processBody(setResultSym)
|
||||
resultIdent = ident "result"
|
||||
fakeResult = quote do:
|
||||
template result: auto {.used.} =
|
||||
{.fatal: "You should not reference the `result` variable inside" &
|
||||
" a void async proc".}
|
||||
resultDecl =
|
||||
if baseTypeIsVoid: fakeResult
|
||||
else: nnkWhenStmt.newTree(
|
||||
# when `baseType` is void:
|
||||
nnkElifExpr.newTree(
|
||||
nnkInfix.newTree(ident "is", baseType, ident "void"),
|
||||
fakeResult
|
||||
),
|
||||
# else:
|
||||
nnkElseExpr.newTree(
|
||||
newStmtList(
|
||||
quote do: {.push warning[resultshadowed]: off.},
|
||||
# var result {.used.}: `baseType`
|
||||
# In the proc body, result may or may not end up being used
|
||||
# depending on how the body is written - with implicit returns /
|
||||
# expressions in particular, it is likely but not guaranteed that
|
||||
# it is not used. Ideally, we would avoid emitting it in this
|
||||
# case to avoid the default initializaiton. {.used.} typically
|
||||
# works better than {.push.} which has a tendency to leak out of
|
||||
# scope.
|
||||
# TODO figure out if there's a way to detect `result` usage in
|
||||
# the proc body _after_ template exapnsion, and therefore
|
||||
# avoid creating this variable - one option is to create an
|
||||
# addtional when branch witha fake `result` and check
|
||||
# `compiles(procBody)` - this is not without cost though
|
||||
nnkVarSection.newTree(nnkIdentDefs.newTree(
|
||||
nnkPragmaExpr.newTree(
|
||||
resultIdent,
|
||||
nnkPragma.newTree(ident "used")),
|
||||
baseType, newEmptyNode())
|
||||
),
|
||||
quote do: {.pop.},
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# ```nim
|
||||
# template `setResultSym`(code: untyped) {.used.} =
|
||||
# when typeof(code) is void: code
|
||||
# else: `resultIdent` = code
|
||||
# ```
|
||||
#
|
||||
# this is useful to handle implicit returns, but also
|
||||
# to bind the `result` to the one we declare here
|
||||
setResultDecl =
|
||||
if baseTypeIsVoid: # shortcut for non-generic void
|
||||
newEmptyNode()
|
||||
else:
|
||||
nnkTemplateDef.newTree(
|
||||
setResultSym,
|
||||
newEmptyNode(), newEmptyNode(),
|
||||
nnkFormalParams.newTree(
|
||||
newEmptyNode(),
|
||||
nnkIdentDefs.newTree(
|
||||
ident"code",
|
||||
ident"untyped",
|
||||
newEmptyNode(),
|
||||
)
|
||||
),
|
||||
nnkPragma.newTree(ident"used"),
|
||||
newEmptyNode(),
|
||||
nnkWhenStmt.newTree(
|
||||
nnkElifBranch.newTree(
|
||||
nnkInfix.newTree(
|
||||
ident"is", nnkTypeOfExpr.newTree(ident"code"), ident"void"),
|
||||
ident"code"
|
||||
),
|
||||
nnkElse.newTree(
|
||||
newAssignment(resultIdent, ident"code")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
internalFutureSym = ident "chronosInternalRetFuture"
|
||||
castFutureSym = nnkCast.newTree(internalFutureType, internalFutureSym)
|
||||
# Wrapping in try/finally ensures that early returns are handled properly
|
||||
# and that `defer` is processed in the right scope
|
||||
completeDecl = wrapInTryFinally(
|
||||
castFutureSym, baseType,
|
||||
if baseTypeIsVoid: procBody # shortcut for non-generic `void`
|
||||
else: newCall(setResultSym, procBody),
|
||||
raises,
|
||||
handleException
|
||||
)
|
||||
|
||||
closureBody = newStmtList(resultDecl, setResultDecl, completeDecl)
|
||||
|
||||
internalFutureParameter = nnkIdentDefs.newTree(
|
||||
internalFutureSym, newIdentNode("FutureBase"), newEmptyNode())
|
||||
prcName = prc.name.getName
|
||||
iteratorNameSym = genSym(nskIterator, $prcName)
|
||||
closureIterator = newProc(
|
||||
iteratorNameSym,
|
||||
[newIdentNode("FutureBase"), internalFutureParameter],
|
||||
closureBody, nnkIteratorDef)
|
||||
|
||||
iteratorNameSym.copyLineInfo(prc)
|
||||
|
||||
closureIterator.pragma = newNimNode(nnkPragma, lineInfoFrom=prc.body)
|
||||
closureIterator.addPragma(newIdentNode("closure"))
|
||||
|
||||
# `async` code must be gcsafe
|
||||
closureIterator.addPragma(newIdentNode("gcsafe"))
|
||||
|
||||
# Exceptions are caught inside the iterator and stored in the future
|
||||
closureIterator.addPragma(nnkExprColonExpr.newTree(
|
||||
newIdentNode("raises"),
|
||||
nnkBracket.newTree()
|
||||
))
|
||||
|
||||
# The body of the original procedure (now moved to the iterator) is replaced
|
||||
# with:
|
||||
#
|
||||
# ```nim
|
||||
# let resultFuture = newFuture[T]()
|
||||
# resultFuture.internalClosure = `iteratorNameSym`
|
||||
# futureContinue(resultFuture)
|
||||
# return resultFuture
|
||||
# ```
|
||||
#
|
||||
# Declared at the end to be sure that the closure doesn't reference it,
|
||||
# avoid cyclic ref (#203)
|
||||
#
|
||||
# Do not change this code to `quote do` version because `instantiationInfo`
|
||||
# will be broken for `newFuture()` call.
|
||||
|
||||
let
|
||||
outerProcBody = newNimNode(nnkStmtList, prc.body)
|
||||
|
||||
# Copy comment for nimdoc
|
||||
if prc.body.len > 0 and prc.body[0].kind == nnkCommentStmt:
|
||||
outerProcBody.add(prc.body[0])
|
||||
|
||||
outerProcBody.add(closureIterator)
|
||||
|
||||
let
|
||||
retFutureSym = ident "resultFuture"
|
||||
newFutProc = if raises == nil:
|
||||
nnkBracketExpr.newTree(ident "newFuture", baseType)
|
||||
else:
|
||||
nnkBracketExpr.newTree(ident "newInternalRaisesFuture", baseType, raises)
|
||||
|
||||
retFutureSym.copyLineInfo(prc)
|
||||
outerProcBody.add(
|
||||
newLetStmt(
|
||||
retFutureSym,
|
||||
newCall(newFutProc, newLit(prcName))
|
||||
)
|
||||
)
|
||||
|
||||
outerProcBody.add(
|
||||
newAssignment(
|
||||
newDotExpr(retFutureSym, newIdentNode("internalClosure")),
|
||||
iteratorNameSym)
|
||||
)
|
||||
|
||||
outerProcBody.add(
|
||||
newCall(newIdentNode("futureContinue"), retFutureSym)
|
||||
)
|
||||
|
||||
outerProcBody.add newNimNode(nnkReturnStmt, prc.body[^1]).add(retFutureSym)
|
||||
|
||||
prc.body = outerProcBody
|
||||
|
||||
when chronosDumpAsync:
|
||||
echo repr prc
|
||||
|
||||
prc
|
||||
|
||||
template await*[T](f: Future[T]): T =
|
||||
## Ensure that the given `Future` is finished, then return its value.
|
||||
##
|
||||
## If the `Future` failed or was cancelled, the corresponding exception will
|
||||
## be raised instead.
|
||||
##
|
||||
## If the `Future` is pending, execution of the current `async` procedure
|
||||
## will be suspended until the `Future` is finished.
|
||||
when declared(chronosInternalRetFuture):
|
||||
chronosInternalRetFuture.internalChild = f
|
||||
# `futureContinue` calls the iterator generated by the `async`
|
||||
# transformation - `yield` gives control back to `futureContinue` which is
|
||||
# responsible for resuming execution once the yielded future is finished
|
||||
yield chronosInternalRetFuture.internalChild
|
||||
# `child` released by `futureContinue`
|
||||
cast[type(f)](chronosInternalRetFuture.internalChild).internalRaiseIfError(f)
|
||||
|
||||
when T isnot void:
|
||||
cast[type(f)](chronosInternalRetFuture.internalChild).value()
|
||||
else:
|
||||
unsupported "await is only available within {.async.}"
|
||||
|
||||
template await*[T, E](fut: InternalRaisesFuture[T, E]): T =
|
||||
## Ensure that the given `Future` is finished, then return its value.
|
||||
##
|
||||
## If the `Future` failed or was cancelled, the corresponding exception will
|
||||
## be raised instead.
|
||||
##
|
||||
## If the `Future` is pending, execution of the current `async` procedure
|
||||
## will be suspended until the `Future` is finished.
|
||||
when declared(chronosInternalRetFuture):
|
||||
chronosInternalRetFuture.internalChild = fut
|
||||
# `futureContinue` calls the iterator generated by the `async`
|
||||
# transformation - `yield` gives control back to `futureContinue` which is
|
||||
# responsible for resuming execution once the yielded future is finished
|
||||
yield chronosInternalRetFuture.internalChild
|
||||
# `child` released by `futureContinue`
|
||||
cast[type(fut)](
|
||||
chronosInternalRetFuture.internalChild).internalRaiseIfError(E, fut)
|
||||
|
||||
when T isnot void:
|
||||
cast[type(fut)](chronosInternalRetFuture.internalChild).value()
|
||||
else:
|
||||
unsupported "await is only available within {.async.}"
|
||||
|
||||
template awaitne*[T](f: Future[T]): Future[T] =
|
||||
when declared(chronosInternalRetFuture):
|
||||
chronosInternalRetFuture.internalChild = f
|
||||
yield chronosInternalRetFuture.internalChild
|
||||
cast[type(f)](chronosInternalRetFuture.internalChild)
|
||||
else:
|
||||
unsupported "awaitne is only available within {.async.}"
|
||||
|
||||
macro async*(params, prc: untyped): untyped =
|
||||
## Macro which processes async procedures into the appropriate
|
||||
## iterators and yield statements.
|
||||
if prc.kind == nnkStmtList:
|
||||
result = newStmtList()
|
||||
for oneProc in prc:
|
||||
result.add asyncSingleProc(oneProc, params)
|
||||
else:
|
||||
result = asyncSingleProc(prc, params)
|
||||
|
||||
macro async*(prc: untyped): untyped =
|
||||
## Macro which processes async procedures into the appropriate
|
||||
## iterators and yield statements.
|
||||
|
||||
if prc.kind == nnkStmtList:
|
||||
result = newStmtList()
|
||||
for oneProc in prc:
|
||||
result.add asyncSingleProc(oneProc, nnkTupleConstr.newTree())
|
||||
else:
|
||||
result = asyncSingleProc(prc, nnkTupleConstr.newTree())
|
||||
9
chronos/internal/errors.nim
Normal file
9
chronos/internal/errors.nim
Normal file
@ -0,0 +1,9 @@
|
||||
type
|
||||
AsyncError* = object of CatchableError
|
||||
## Generic async exception
|
||||
AsyncTimeoutError* = object of AsyncError
|
||||
## Timeout exception
|
||||
|
||||
AsyncExceptionError* = object of AsyncError
|
||||
## Error raised in `handleException` mode - the original exception is
|
||||
## available from the `parent` field.
|
||||
248
chronos/internal/raisesfutures.nim
Normal file
248
chronos/internal/raisesfutures.nim
Normal file
@ -0,0 +1,248 @@
|
||||
import
|
||||
std/[macros, sequtils],
|
||||
../futures
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
type
|
||||
InternalRaisesFuture*[T, E] = ref object of Future[T]
|
||||
## Future with a tuple of possible exception types
|
||||
## eg InternalRaisesFuture[void, (ValueError, OSError)]
|
||||
##
|
||||
## This type gets injected by `async: (raises: ...)` and similar utilities
|
||||
## and should not be used manually as the internal exception representation
|
||||
## is subject to change in future chronos versions.
|
||||
# TODO https://github.com/nim-lang/Nim/issues/23418
|
||||
# TODO https://github.com/nim-lang/Nim/issues/23419
|
||||
when E is void:
|
||||
dummy: E
|
||||
else:
|
||||
dummy: array[0, E]
|
||||
|
||||
proc makeNoRaises*(): NimNode {.compileTime.} =
|
||||
# An empty tuple would have been easier but...
|
||||
# https://github.com/nim-lang/Nim/issues/22863
|
||||
# https://github.com/nim-lang/Nim/issues/22865
|
||||
|
||||
ident"void"
|
||||
|
||||
proc dig(n: NimNode): NimNode {.compileTime.} =
|
||||
# Dig through the layers of type to find the raises list
|
||||
if n.eqIdent("void"):
|
||||
n
|
||||
elif n.kind == nnkBracketExpr:
|
||||
if n[0].eqIdent("tuple"):
|
||||
n
|
||||
elif n[0].eqIdent("typeDesc"):
|
||||
dig(getType(n[1]))
|
||||
else:
|
||||
echo astGenRepr(n)
|
||||
raiseAssert "Unkown bracket"
|
||||
elif n.kind == nnkTupleConstr:
|
||||
n
|
||||
else:
|
||||
dig(getType(getTypeInst(n)))
|
||||
|
||||
proc isNoRaises*(n: NimNode): bool {.compileTime.} =
|
||||
dig(n).eqIdent("void")
|
||||
|
||||
iterator members(tup: NimNode): NimNode =
|
||||
# Given a typedesc[tuple] = (A, B, C), yields the tuple members (A, B C)
|
||||
if not isNoRaises(tup):
|
||||
for n in getType(getTypeInst(tup)[1])[1..^1]:
|
||||
yield n
|
||||
|
||||
proc members(tup: NimNode): seq[NimNode] {.compileTime.} =
|
||||
for t in tup.members():
|
||||
result.add(t)
|
||||
|
||||
macro hasException(raises: typedesc, ident: static string): bool =
|
||||
newLit(raises.members.anyIt(it.eqIdent(ident)))
|
||||
|
||||
macro Raising*[T](F: typedesc[Future[T]], E: typed): untyped =
|
||||
## Given a Future type instance, return a type storing `{.raises.}`
|
||||
## information
|
||||
##
|
||||
## Note; this type may change in the future
|
||||
|
||||
# An earlier version used `E: varargs[typedesc]` here but this is buggyt/no
|
||||
# longer supported in 2.0 in certain cases:
|
||||
# https://github.com/nim-lang/Nim/issues/23432
|
||||
let
|
||||
e =
|
||||
case E.getTypeInst().typeKind()
|
||||
of ntyTypeDesc: @[E]
|
||||
of ntyArray:
|
||||
for x in E:
|
||||
if x.getTypeInst().typeKind != ntyTypeDesc:
|
||||
error("Expected typedesc, got " & repr(x), x)
|
||||
E.mapIt(it)
|
||||
else:
|
||||
error("Expected typedesc, got " & repr(E), E)
|
||||
@[]
|
||||
|
||||
let raises = if e.len == 0:
|
||||
makeNoRaises()
|
||||
else:
|
||||
nnkTupleConstr.newTree(e)
|
||||
nnkBracketExpr.newTree(
|
||||
ident "InternalRaisesFuture",
|
||||
nnkDotExpr.newTree(F, ident"T"),
|
||||
raises
|
||||
)
|
||||
|
||||
template init*[T, E](
|
||||
F: type InternalRaisesFuture[T, E], fromProc: static[string] = ""): F =
|
||||
## Creates a new pending future.
|
||||
##
|
||||
## Specifying ``fromProc``, which is a string specifying the name of the proc
|
||||
## that this future belongs to, is a good habit as it helps with debugging.
|
||||
when not hasException(type(E), "CancelledError"):
|
||||
static:
|
||||
raiseAssert "Manually created futures must either own cancellation schedule or raise CancelledError"
|
||||
|
||||
|
||||
let res = F()
|
||||
internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Pending, {})
|
||||
res
|
||||
|
||||
template init*[T, E](
|
||||
F: type InternalRaisesFuture[T, E], fromProc: static[string] = "",
|
||||
flags: static[FutureFlags]): F =
|
||||
## Creates a new pending future.
|
||||
##
|
||||
## Specifying ``fromProc``, which is a string specifying the name of the proc
|
||||
## that this future belongs to, is a good habit as it helps with debugging.
|
||||
let res = F()
|
||||
when not hasException(type(E), "CancelledError"):
|
||||
static:
|
||||
doAssert FutureFlag.OwnCancelSchedule in flags,
|
||||
"Manually created futures must either own cancellation schedule or raise CancelledError"
|
||||
|
||||
internalInitFutureBase(
|
||||
res, getSrcLocation(fromProc), FutureState.Pending, flags)
|
||||
res
|
||||
|
||||
proc containsSignature(members: openArray[NimNode], typ: NimNode): bool {.compileTime.} =
|
||||
let typHash = signatureHash(typ)
|
||||
|
||||
for err in members:
|
||||
if signatureHash(err) == typHash:
|
||||
return true
|
||||
false
|
||||
|
||||
# Utilities for working with the E part of InternalRaisesFuture - unstable
|
||||
macro prepend*(tup: typedesc, typs: varargs[typed]): typedesc =
|
||||
result = nnkTupleConstr.newTree()
|
||||
for err in typs:
|
||||
if not tup.members().containsSignature(err):
|
||||
result.add err
|
||||
|
||||
for err in tup.members():
|
||||
result.add err
|
||||
|
||||
if result.len == 0:
|
||||
result = makeNoRaises()
|
||||
|
||||
macro remove*(tup: typedesc, typs: varargs[typed]): typedesc =
|
||||
result = nnkTupleConstr.newTree()
|
||||
for err in tup.members():
|
||||
if not typs[0..^1].containsSignature(err):
|
||||
result.add err
|
||||
|
||||
if result.len == 0:
|
||||
result = makeNoRaises()
|
||||
|
||||
macro union*(tup0: typedesc, tup1: typedesc): typedesc =
|
||||
## Join the types of the two tuples deduplicating the entries
|
||||
result = nnkTupleConstr.newTree()
|
||||
|
||||
for err in tup0.members():
|
||||
var found = false
|
||||
for err2 in tup1.members():
|
||||
if signatureHash(err) == signatureHash(err2):
|
||||
found = true
|
||||
if not found:
|
||||
result.add err
|
||||
|
||||
for err2 in tup1.members():
|
||||
result.add err2
|
||||
if result.len == 0:
|
||||
result = makeNoRaises()
|
||||
|
||||
proc getRaisesTypes*(raises: NimNode): NimNode =
|
||||
let typ = getType(raises)
|
||||
case typ.typeKind
|
||||
of ntyTypeDesc: typ[1]
|
||||
else: typ
|
||||
|
||||
macro checkRaises*[T: CatchableError](
|
||||
future: InternalRaisesFuture, raises: typed, error: ref T,
|
||||
warn: static bool = true): untyped =
|
||||
## Generate code that checks that the given error is compatible with the
|
||||
## raises restrictions of `future`.
|
||||
##
|
||||
## This check is done either at compile time or runtime depending on the
|
||||
## information available at compile time - in particular, if the raises
|
||||
## inherit from `error`, we end up with the equivalent of a downcast which
|
||||
## raises a Defect if it fails.
|
||||
let
|
||||
raises = getRaisesTypes(raises)
|
||||
|
||||
expectKind(getTypeInst(error), nnkRefTy)
|
||||
let toMatch = getTypeInst(error)[0]
|
||||
|
||||
|
||||
if isNoRaises(raises):
|
||||
error(
|
||||
"`fail`: `" & repr(toMatch) & "` incompatible with `raises: []`", future)
|
||||
return
|
||||
|
||||
var
|
||||
typeChecker = ident"false"
|
||||
maybeChecker = ident"false"
|
||||
runtimeChecker = ident"false"
|
||||
|
||||
for errorType in raises[1..^1]:
|
||||
typeChecker = infix(typeChecker, "or", infix(toMatch, "is", errorType))
|
||||
maybeChecker = infix(maybeChecker, "or", infix(errorType, "is", toMatch))
|
||||
runtimeChecker = infix(
|
||||
runtimeChecker, "or",
|
||||
infix(error, "of", nnkBracketExpr.newTree(ident"typedesc", errorType)))
|
||||
|
||||
let
|
||||
errorMsg = "`fail`: `" & repr(toMatch) & "` incompatible with `raises: " & repr(raises[1..^1]) & "`"
|
||||
warningMsg = "Can't verify `fail` exception type at compile time - expected one of " & repr(raises[1..^1]) & ", got `" & repr(toMatch) & "`"
|
||||
# A warning from this line means exception type will be verified at runtime
|
||||
warning = if warn:
|
||||
quote do: {.warning: `warningMsg`.}
|
||||
else: newEmptyNode()
|
||||
|
||||
# Cannot check inhertance in macro so we let `static` do the heavy lifting
|
||||
quote do:
|
||||
when not(`typeChecker`):
|
||||
when not(`maybeChecker`):
|
||||
static:
|
||||
{.error: `errorMsg`.}
|
||||
else:
|
||||
`warning`
|
||||
assert(`runtimeChecker`, `errorMsg`)
|
||||
|
||||
func failed*[T](future: InternalRaisesFuture[T, void]): bool {.inline.} =
|
||||
## Determines whether ``future`` finished with an error.
|
||||
static:
|
||||
warning("No exceptions possible with this operation, `failed` always returns false")
|
||||
|
||||
false
|
||||
|
||||
func error*[T](future: InternalRaisesFuture[T, void]): ref CatchableError {.
|
||||
raises: [].} =
|
||||
static:
|
||||
warning("No exceptions possible with this operation, `error` always returns nil")
|
||||
nil
|
||||
|
||||
func readError*[T](future: InternalRaisesFuture[T, void]): ref CatchableError {.
|
||||
raises: [ValueError].} =
|
||||
static:
|
||||
warning("No exceptions possible with this operation, `readError` always raises")
|
||||
raise newException(ValueError, "No error in future.")
|
||||
@ -97,12 +97,12 @@ proc new*(t: typedesc[Selector], T: typedesc): SelectResult[Selector[T]] =
|
||||
var nmask: Sigset
|
||||
if sigemptyset(nmask) < 0:
|
||||
return err(osLastError())
|
||||
let epollFd = epoll_create(asyncEventsCount)
|
||||
let epollFd = epoll_create(chronosEventsCount)
|
||||
if epollFd < 0:
|
||||
return err(osLastError())
|
||||
let selector = Selector[T](
|
||||
epollFd: epollFd,
|
||||
fds: initTable[int32, SelectorKey[T]](asyncInitialSize),
|
||||
fds: initTable[int32, SelectorKey[T]](chronosInitialSize),
|
||||
signalMask: nmask,
|
||||
virtualId: -1'i32, # Should start with -1, because `InvalidIdent` == -1
|
||||
childrenExited: false,
|
||||
@ -411,7 +411,7 @@ proc registerProcess*[T](s: Selector, pid: int, data: T): SelectResult[cint] =
|
||||
s.freeKey(fdi32)
|
||||
s.freeProcess(int32(pid))
|
||||
return err(res.error())
|
||||
s.pidFd = Opt.some(cast[cint](res.get()))
|
||||
s.pidFd = Opt.some(res.get())
|
||||
|
||||
ok(cint(fdi32))
|
||||
|
||||
@ -627,7 +627,7 @@ proc selectInto2*[T](s: Selector[T], timeout: int,
|
||||
readyKeys: var openArray[ReadyKey]
|
||||
): SelectResult[int] =
|
||||
var
|
||||
queueEvents: array[asyncEventsCount, EpollEvent]
|
||||
queueEvents: array[chronosEventsCount, EpollEvent]
|
||||
k: int = 0
|
||||
|
||||
verifySelectParams(timeout, -1, int(high(cint)))
|
||||
@ -668,7 +668,7 @@ proc selectInto2*[T](s: Selector[T], timeout: int,
|
||||
ok(k)
|
||||
|
||||
proc select2*[T](s: Selector[T], timeout: int): SelectResult[seq[ReadyKey]] =
|
||||
var res = newSeq[ReadyKey](asyncEventsCount)
|
||||
var res = newSeq[ReadyKey](chronosEventsCount)
|
||||
let count = ? selectInto2(s, timeout, res)
|
||||
res.setLen(count)
|
||||
ok(res)
|
||||
|
||||
@ -110,7 +110,7 @@ proc new*(t: typedesc[Selector], T: typedesc): SelectResult[Selector[T]] =
|
||||
|
||||
let selector = Selector[T](
|
||||
kqFd: kqFd,
|
||||
fds: initTable[int32, SelectorKey[T]](asyncInitialSize),
|
||||
fds: initTable[int32, SelectorKey[T]](chronosInitialSize),
|
||||
virtualId: -1'i32, # Should start with -1, because `InvalidIdent` == -1
|
||||
virtualHoles: initDeque[int32]()
|
||||
)
|
||||
@ -559,7 +559,7 @@ proc selectInto2*[T](s: Selector[T], timeout: int,
|
||||
): SelectResult[int] =
|
||||
var
|
||||
tv: Timespec
|
||||
queueEvents: array[asyncEventsCount, KEvent]
|
||||
queueEvents: array[chronosEventsCount, KEvent]
|
||||
|
||||
verifySelectParams(timeout, -1, high(int))
|
||||
|
||||
@ -575,7 +575,7 @@ proc selectInto2*[T](s: Selector[T], timeout: int,
|
||||
addr tv
|
||||
else:
|
||||
nil
|
||||
maxEventsCount = cint(min(asyncEventsCount, len(readyKeys)))
|
||||
maxEventsCount = cint(min(chronosEventsCount, len(readyKeys)))
|
||||
eventsCount =
|
||||
block:
|
||||
var res = 0
|
||||
@ -601,7 +601,7 @@ proc selectInto2*[T](s: Selector[T], timeout: int,
|
||||
|
||||
proc select2*[T](s: Selector[T],
|
||||
timeout: int): Result[seq[ReadyKey], OSErrorCode] =
|
||||
var res = newSeq[ReadyKey](asyncEventsCount)
|
||||
var res = newSeq[ReadyKey](chronosEventsCount)
|
||||
let count = ? selectInto2(s, timeout, res)
|
||||
res.setLen(count)
|
||||
ok(res)
|
||||
|
||||
@ -16,7 +16,7 @@ import stew/base10
|
||||
type
|
||||
SelectorImpl[T] = object
|
||||
fds: Table[int32, SelectorKey[T]]
|
||||
pollfds: seq[TPollFd]
|
||||
pollfds: seq[TPollfd]
|
||||
Selector*[T] = ref SelectorImpl[T]
|
||||
|
||||
type
|
||||
@ -50,7 +50,7 @@ proc freeKey[T](s: Selector[T], key: int32) =
|
||||
|
||||
proc new*(t: typedesc[Selector], T: typedesc): SelectResult[Selector[T]] =
|
||||
let selector = Selector[T](
|
||||
fds: initTable[int32, SelectorKey[T]](asyncInitialSize)
|
||||
fds: initTable[int32, SelectorKey[T]](chronosInitialSize)
|
||||
)
|
||||
ok(selector)
|
||||
|
||||
@ -72,7 +72,7 @@ proc trigger2*(event: SelectEvent): SelectResult[void] =
|
||||
if res == -1:
|
||||
err(osLastError())
|
||||
elif res != sizeof(uint64):
|
||||
err(OSErrorCode(osdefs.EINVAL))
|
||||
err(osdefs.EINVAL)
|
||||
else:
|
||||
ok()
|
||||
|
||||
@ -98,13 +98,14 @@ template toPollEvents(events: set[Event]): cshort =
|
||||
res
|
||||
|
||||
template pollAdd[T](s: Selector[T], sock: cint, events: set[Event]) =
|
||||
s.pollfds.add(TPollFd(fd: sock, events: toPollEvents(events), revents: 0))
|
||||
s.pollfds.add(TPollfd(fd: sock, events: toPollEvents(events), revents: 0))
|
||||
|
||||
template pollUpdate[T](s: Selector[T], sock: cint, events: set[Event]) =
|
||||
var updated = false
|
||||
for mitem in s.pollfds.mitems():
|
||||
if mitem.fd == sock:
|
||||
mitem.events = toPollEvents(events)
|
||||
updated = true
|
||||
break
|
||||
if not(updated):
|
||||
raiseAssert "Descriptor [" & $sock & "] is not registered in the queue!"
|
||||
@ -177,7 +178,6 @@ proc unregister2*[T](s: Selector[T], event: SelectEvent): SelectResult[void] =
|
||||
|
||||
proc prepareKey[T](s: Selector[T], event: var TPollfd): Opt[ReadyKey] =
|
||||
let
|
||||
defaultKey = SelectorKey[T](ident: InvalidIdent)
|
||||
fdi32 = int32(event.fd)
|
||||
revents = event.revents
|
||||
|
||||
@ -220,11 +220,14 @@ proc selectInto2*[T](s: Selector[T], timeout: int,
|
||||
verifySelectParams(timeout, -1, int(high(cint)))
|
||||
|
||||
let
|
||||
maxEventsCount = min(len(s.pollfds), len(readyKeys))
|
||||
maxEventsCount = culong(min(len(s.pollfds), len(readyKeys)))
|
||||
# Without `culong` conversion, this code could fail with RangeError
|
||||
# defect on explicit Tnfds(integer) conversion (probably related to
|
||||
# combination of nim+clang (android toolchain)).
|
||||
eventsCount =
|
||||
if maxEventsCount > 0:
|
||||
let res = handleEintr(poll(addr(s.pollfds[0]), Tnfds(maxEventsCount),
|
||||
timeout))
|
||||
cint(timeout)))
|
||||
if res < 0:
|
||||
return err(osLastError())
|
||||
res
|
||||
@ -241,7 +244,7 @@ proc selectInto2*[T](s: Selector[T], timeout: int,
|
||||
ok(k)
|
||||
|
||||
proc select2*[T](s: Selector[T], timeout: int): SelectResult[seq[ReadyKey]] =
|
||||
var res = newSeq[ReadyKey](asyncEventsCount)
|
||||
var res = newSeq[ReadyKey](chronosEventsCount)
|
||||
let count = ? selectInto2(s, timeout, res)
|
||||
res.setLen(count)
|
||||
ok(res)
|
||||
|
||||
@ -122,6 +122,7 @@ when defined(windows):
|
||||
SO_UPDATE_ACCEPT_CONTEXT* = 0x700B
|
||||
SO_CONNECT_TIME* = 0x700C
|
||||
SO_UPDATE_CONNECT_CONTEXT* = 0x7010
|
||||
SO_PROTOCOL_INFOW* = 0x2005
|
||||
|
||||
FILE_FLAG_FIRST_PIPE_INSTANCE* = 0x00080000'u32
|
||||
FILE_FLAG_OPEN_NO_RECALL* = 0x00100000'u32
|
||||
@ -258,6 +259,9 @@ when defined(windows):
|
||||
FIONBIO* = WSAIOW(102, 126)
|
||||
|
||||
HANDLE_FLAG_INHERIT* = 1'u32
|
||||
IPV6_V6ONLY* = 27
|
||||
MAX_PROTOCOL_CHAIN* = 7
|
||||
WSAPROTOCOL_LEN* = 255
|
||||
|
||||
type
|
||||
LONG* = int32
|
||||
@ -441,6 +445,32 @@ when defined(windows):
|
||||
prefix*: SOCKADDR_INET
|
||||
prefixLength*: uint8
|
||||
|
||||
WSAPROTOCOLCHAIN* {.final, pure.} = object
|
||||
chainLen*: int32
|
||||
chainEntries*: array[MAX_PROTOCOL_CHAIN, DWORD]
|
||||
|
||||
WSAPROTOCOL_INFO* {.final, pure.} = object
|
||||
dwServiceFlags1*: uint32
|
||||
dwServiceFlags2*: uint32
|
||||
dwServiceFlags3*: uint32
|
||||
dwServiceFlags4*: uint32
|
||||
dwProviderFlags*: uint32
|
||||
providerId*: GUID
|
||||
dwCatalogEntryId*: DWORD
|
||||
protocolChain*: WSAPROTOCOLCHAIN
|
||||
iVersion*: int32
|
||||
iAddressFamily*: int32
|
||||
iMaxSockAddr*: int32
|
||||
iMinSockAddr*: int32
|
||||
iSocketType*: int32
|
||||
iProtocol*: int32
|
||||
iProtocolMaxOffset*: int32
|
||||
iNetworkByteOrder*: int32
|
||||
iSecurityScheme*: int32
|
||||
dwMessageSize*: uint32
|
||||
dwProviderReserved*: uint32
|
||||
szProtocol*: array[WSAPROTOCOL_LEN + 1, WCHAR]
|
||||
|
||||
MibIpForwardRow2* {.final, pure.} = object
|
||||
interfaceLuid*: uint64
|
||||
interfaceIndex*: uint32
|
||||
@ -708,7 +738,7 @@ when defined(windows):
|
||||
res: var ptr AddrInfo): cint {.
|
||||
stdcall, dynlib: "ws2_32", importc: "getaddrinfo", sideEffect.}
|
||||
|
||||
proc freeaddrinfo*(ai: ptr AddrInfo) {.
|
||||
proc freeAddrInfo*(ai: ptr AddrInfo) {.
|
||||
stdcall, dynlib: "ws2_32", importc: "freeaddrinfo", sideEffect.}
|
||||
|
||||
proc createIoCompletionPort*(fileHandle: HANDLE,
|
||||
@ -880,7 +910,7 @@ elif defined(macos) or defined(macosx):
|
||||
sigemptyset, sigaddset, sigismember, fcntl, accept,
|
||||
pipe, write, signal, read, setsockopt, getsockopt,
|
||||
getcwd, chdir, waitpid, kill, select, pselect,
|
||||
socketpair,
|
||||
socketpair, poll, freeAddrInfo,
|
||||
Timeval, Timespec, Pid, Mode, Time, Sigset, SockAddr,
|
||||
SockLen, Sockaddr_storage, Sockaddr_in, Sockaddr_in6,
|
||||
Sockaddr_un, SocketHandle, AddrInfo, RLimit, TFdSet,
|
||||
@ -890,7 +920,7 @@ elif defined(macos) or defined(macosx):
|
||||
O_NONBLOCK, SOL_SOCKET, SOCK_RAW, SOCK_DGRAM,
|
||||
SOCK_STREAM, MSG_NOSIGNAL, MSG_PEEK,
|
||||
AF_INET, AF_INET6, AF_UNIX, SO_ERROR, SO_REUSEADDR,
|
||||
SO_REUSEPORT, SO_BROADCAST, IPPROTO_IP,
|
||||
SO_REUSEPORT, SO_BROADCAST, IPPROTO_IP, IPPROTO_IPV6,
|
||||
IPV6_MULTICAST_HOPS, SOCK_DGRAM, RLIMIT_NOFILE,
|
||||
SIG_BLOCK, SIG_UNBLOCK, SHUT_RD, SHUT_WR, SHUT_RDWR,
|
||||
SIGHUP, SIGINT, SIGQUIT, SIGILL, SIGTRAP, SIGABRT,
|
||||
@ -905,7 +935,7 @@ elif defined(macos) or defined(macosx):
|
||||
sigemptyset, sigaddset, sigismember, fcntl, accept,
|
||||
pipe, write, signal, read, setsockopt, getsockopt,
|
||||
getcwd, chdir, waitpid, kill, select, pselect,
|
||||
socketpair,
|
||||
socketpair, poll, freeAddrInfo,
|
||||
Timeval, Timespec, Pid, Mode, Time, Sigset, SockAddr,
|
||||
SockLen, Sockaddr_storage, Sockaddr_in, Sockaddr_in6,
|
||||
Sockaddr_un, SocketHandle, AddrInfo, RLimit, TFdSet,
|
||||
@ -915,7 +945,7 @@ elif defined(macos) or defined(macosx):
|
||||
O_NONBLOCK, SOL_SOCKET, SOCK_RAW, SOCK_DGRAM,
|
||||
SOCK_STREAM, MSG_NOSIGNAL, MSG_PEEK,
|
||||
AF_INET, AF_INET6, AF_UNIX, SO_ERROR, SO_REUSEADDR,
|
||||
SO_REUSEPORT, SO_BROADCAST, IPPROTO_IP,
|
||||
SO_REUSEPORT, SO_BROADCAST, IPPROTO_IP, IPPROTO_IPV6,
|
||||
IPV6_MULTICAST_HOPS, SOCK_DGRAM, RLIMIT_NOFILE,
|
||||
SIG_BLOCK, SIG_UNBLOCK, SHUT_RD, SHUT_WR, SHUT_RDWR,
|
||||
SIGHUP, SIGINT, SIGQUIT, SIGILL, SIGTRAP, SIGABRT,
|
||||
@ -929,6 +959,21 @@ elif defined(macos) or defined(macosx):
|
||||
numer*: uint32
|
||||
denom*: uint32
|
||||
|
||||
TPollfd* {.importc: "struct pollfd", pure, final,
|
||||
header: "<poll.h>".} = object
|
||||
fd*: cint
|
||||
events*: cshort
|
||||
revents*: cshort
|
||||
|
||||
Tnfds* {.importc: "nfds_t", header: "<poll.h>".} = culong
|
||||
|
||||
const
|
||||
POLLIN* = 0x0001
|
||||
POLLOUT* = 0x0004
|
||||
POLLERR* = 0x0008
|
||||
POLLHUP* = 0x0010
|
||||
POLLNVAL* = 0x0020
|
||||
|
||||
proc posix_gettimeofday*(tp: var Timeval, unused: pointer = nil) {.
|
||||
importc: "gettimeofday", header: "<sys/time.h>".}
|
||||
|
||||
@ -938,6 +983,9 @@ elif defined(macos) or defined(macosx):
|
||||
proc mach_absolute_time*(): uint64 {.
|
||||
importc, header: "<mach/mach_time.h>".}
|
||||
|
||||
proc poll*(a1: ptr TPollfd, a2: Tnfds, a3: cint): cint {.
|
||||
importc, header: "<poll.h>", sideEffect.}
|
||||
|
||||
elif defined(linux):
|
||||
from std/posix import close, shutdown, sigemptyset, sigaddset, sigismember,
|
||||
sigdelset, write, read, waitid, getaddrinfo,
|
||||
@ -947,20 +995,22 @@ elif defined(linux):
|
||||
unlink, listen, sendmsg, recvmsg, getpid, fcntl,
|
||||
pthread_sigmask, sigprocmask, clock_gettime, signal,
|
||||
getcwd, chdir, waitpid, kill, select, pselect,
|
||||
socketpair,
|
||||
socketpair, poll, freeAddrInfo,
|
||||
ClockId, Itimerspec, Timespec, Sigset, Time, Pid, Mode,
|
||||
SigInfo, Id, Tmsghdr, IOVec, RLimit, Timeval, TFdSet,
|
||||
SockAddr, SockLen, Sockaddr_storage, Sockaddr_in,
|
||||
Sockaddr_in6, Sockaddr_un, AddrInfo, SocketHandle,
|
||||
Suseconds,
|
||||
Suseconds, TPollfd, Tnfds,
|
||||
FD_CLR, FD_ISSET, FD_SET, FD_ZERO,
|
||||
CLOCK_MONOTONIC, F_GETFL, F_SETFL, F_GETFD, F_SETFD,
|
||||
FD_CLOEXEC, O_NONBLOCK, SIG_BLOCK, SIG_UNBLOCK,
|
||||
SOL_SOCKET, SO_ERROR, RLIMIT_NOFILE, MSG_NOSIGNAL,
|
||||
MSG_PEEK,
|
||||
AF_INET, AF_INET6, AF_UNIX, SO_REUSEADDR, SO_REUSEPORT,
|
||||
SO_BROADCAST, IPPROTO_IP, IPV6_MULTICAST_HOPS,
|
||||
SO_BROADCAST, IPPROTO_IP, IPPROTO_IPV6,
|
||||
IPV6_MULTICAST_HOPS,
|
||||
SOCK_DGRAM, SOCK_STREAM, SHUT_RD, SHUT_WR, SHUT_RDWR,
|
||||
POLLIN, POLLOUT, POLLERR, POLLHUP, POLLNVAL,
|
||||
SIGHUP, SIGINT, SIGQUIT, SIGILL, SIGTRAP, SIGABRT,
|
||||
SIGBUS, SIGFPE, SIGKILL, SIGUSR1, SIGSEGV, SIGUSR2,
|
||||
SIGPIPE, SIGALRM, SIGTERM, SIGPIPE, SIGCHLD, SIGSTOP,
|
||||
@ -974,20 +1024,21 @@ elif defined(linux):
|
||||
unlink, listen, sendmsg, recvmsg, getpid, fcntl,
|
||||
pthread_sigmask, sigprocmask, clock_gettime, signal,
|
||||
getcwd, chdir, waitpid, kill, select, pselect,
|
||||
socketpair,
|
||||
socketpair, poll, freeAddrInfo,
|
||||
ClockId, Itimerspec, Timespec, Sigset, Time, Pid, Mode,
|
||||
SigInfo, Id, Tmsghdr, IOVec, RLimit, TFdSet, Timeval,
|
||||
SockAddr, SockLen, Sockaddr_storage, Sockaddr_in,
|
||||
Sockaddr_in6, Sockaddr_un, AddrInfo, SocketHandle,
|
||||
Suseconds,
|
||||
Suseconds, TPollfd, Tnfds,
|
||||
FD_CLR, FD_ISSET, FD_SET, FD_ZERO,
|
||||
CLOCK_MONOTONIC, F_GETFL, F_SETFL, F_GETFD, F_SETFD,
|
||||
FD_CLOEXEC, O_NONBLOCK, SIG_BLOCK, SIG_UNBLOCK,
|
||||
SOL_SOCKET, SO_ERROR, RLIMIT_NOFILE, MSG_NOSIGNAL,
|
||||
MSG_PEEK,
|
||||
AF_INET, AF_INET6, AF_UNIX, SO_REUSEADDR, SO_REUSEPORT,
|
||||
SO_BROADCAST, IPPROTO_IP, IPV6_MULTICAST_HOPS,
|
||||
SO_BROADCAST, IPPROTO_IP, IPPROTO_IPV6, IPV6_MULTICAST_HOPS,
|
||||
SOCK_DGRAM, SOCK_STREAM, SHUT_RD, SHUT_WR, SHUT_RDWR,
|
||||
POLLIN, POLLOUT, POLLERR, POLLHUP, POLLNVAL,
|
||||
SIGHUP, SIGINT, SIGQUIT, SIGILL, SIGTRAP, SIGABRT,
|
||||
SIGBUS, SIGFPE, SIGKILL, SIGUSR1, SIGSEGV, SIGUSR2,
|
||||
SIGPIPE, SIGALRM, SIGTERM, SIGPIPE, SIGCHLD, SIGSTOP,
|
||||
@ -1097,20 +1148,21 @@ elif defined(freebsd) or defined(openbsd) or defined(netbsd) or
|
||||
sigaddset, sigismember, fcntl, accept, pipe, write,
|
||||
signal, read, setsockopt, getsockopt, clock_gettime,
|
||||
getcwd, chdir, waitpid, kill, select, pselect,
|
||||
socketpair,
|
||||
socketpair, poll, freeAddrInfo,
|
||||
Timeval, Timespec, Pid, Mode, Time, Sigset, SockAddr,
|
||||
SockLen, Sockaddr_storage, Sockaddr_in, Sockaddr_in6,
|
||||
Sockaddr_un, SocketHandle, AddrInfo, RLimit, TFdSet,
|
||||
Suseconds,
|
||||
Suseconds, TPollfd, Tnfds,
|
||||
FD_CLR, FD_ISSET, FD_SET, FD_ZERO,
|
||||
F_GETFL, F_SETFL, F_GETFD, F_SETFD, FD_CLOEXEC,
|
||||
O_NONBLOCK, SOL_SOCKET, SOCK_RAW, SOCK_DGRAM,
|
||||
SOCK_STREAM, MSG_NOSIGNAL, MSG_PEEK,
|
||||
AF_INET, AF_INET6, AF_UNIX, SO_ERROR, SO_REUSEADDR,
|
||||
SO_REUSEPORT, SO_BROADCAST, IPPROTO_IP,
|
||||
SO_REUSEPORT, SO_BROADCAST, IPPROTO_IP, IPPROTO_IPV6,
|
||||
IPV6_MULTICAST_HOPS, SOCK_DGRAM, RLIMIT_NOFILE,
|
||||
SIG_BLOCK, SIG_UNBLOCK, CLOCK_MONOTONIC,
|
||||
SHUT_RD, SHUT_WR, SHUT_RDWR,
|
||||
POLLIN, POLLOUT, POLLERR, POLLHUP, POLLNVAL,
|
||||
SIGHUP, SIGINT, SIGQUIT, SIGILL, SIGTRAP, SIGABRT,
|
||||
SIGBUS, SIGFPE, SIGKILL, SIGUSR1, SIGSEGV, SIGUSR2,
|
||||
SIGPIPE, SIGALRM, SIGTERM, SIGPIPE, SIGCHLD, SIGSTOP,
|
||||
@ -1123,20 +1175,21 @@ elif defined(freebsd) or defined(openbsd) or defined(netbsd) or
|
||||
sigaddset, sigismember, fcntl, accept, pipe, write,
|
||||
signal, read, setsockopt, getsockopt, clock_gettime,
|
||||
getcwd, chdir, waitpid, kill, select, pselect,
|
||||
socketpair,
|
||||
socketpair, poll, freeAddrInfo,
|
||||
Timeval, Timespec, Pid, Mode, Time, Sigset, SockAddr,
|
||||
SockLen, Sockaddr_storage, Sockaddr_in, Sockaddr_in6,
|
||||
Sockaddr_un, SocketHandle, AddrInfo, RLimit, TFdSet,
|
||||
Suseconds,
|
||||
Suseconds, TPollfd, Tnfds,
|
||||
FD_CLR, FD_ISSET, FD_SET, FD_ZERO,
|
||||
F_GETFL, F_SETFL, F_GETFD, F_SETFD, FD_CLOEXEC,
|
||||
O_NONBLOCK, SOL_SOCKET, SOCK_RAW, SOCK_DGRAM,
|
||||
SOCK_STREAM, MSG_NOSIGNAL, MSG_PEEK,
|
||||
AF_INET, AF_INET6, AF_UNIX, SO_ERROR, SO_REUSEADDR,
|
||||
SO_REUSEPORT, SO_BROADCAST, IPPROTO_IP,
|
||||
SO_REUSEPORT, SO_BROADCAST, IPPROTO_IP, IPPROTO_IPV6,
|
||||
IPV6_MULTICAST_HOPS, SOCK_DGRAM, RLIMIT_NOFILE,
|
||||
SIG_BLOCK, SIG_UNBLOCK, CLOCK_MONOTONIC,
|
||||
SHUT_RD, SHUT_WR, SHUT_RDWR,
|
||||
POLLIN, POLLOUT, POLLERR, POLLHUP, POLLNVAL,
|
||||
SIGHUP, SIGINT, SIGQUIT, SIGILL, SIGTRAP, SIGABRT,
|
||||
SIGBUS, SIGFPE, SIGKILL, SIGUSR1, SIGSEGV, SIGUSR2,
|
||||
SIGPIPE, SIGALRM, SIGTERM, SIGPIPE, SIGCHLD, SIGSTOP,
|
||||
@ -1160,47 +1213,52 @@ when defined(linux):
|
||||
SOCK_CLOEXEC* = 0x80000
|
||||
TCP_NODELAY* = cint(1)
|
||||
IPPROTO_TCP* = 6
|
||||
elif defined(freebsd) or defined(netbsd) or defined(dragonfly):
|
||||
O_CLOEXEC* = 0x80000
|
||||
POSIX_SPAWN_USEVFORK* = 0x40
|
||||
IPV6_V6ONLY* = 26
|
||||
elif defined(freebsd):
|
||||
const
|
||||
SOCK_NONBLOCK* = 0x20000000
|
||||
SOCK_CLOEXEC* = 0x10000000
|
||||
TCP_NODELAY* = cint(1)
|
||||
IPPROTO_TCP* = 6
|
||||
O_CLOEXEC* = 0x00100000
|
||||
POSIX_SPAWN_USEVFORK* = 0x00
|
||||
IPV6_V6ONLY* = 27
|
||||
elif defined(netbsd):
|
||||
const
|
||||
SOCK_NONBLOCK* = 0x20000000
|
||||
SOCK_CLOEXEC* = 0x10000000
|
||||
TCP_NODELAY* = cint(1)
|
||||
IPPROTO_TCP* = 6
|
||||
O_CLOEXEC* = 0x00400000
|
||||
POSIX_SPAWN_USEVFORK* = 0x00
|
||||
IPV6_V6ONLY* = 27
|
||||
elif defined(dragonfly):
|
||||
const
|
||||
SOCK_NONBLOCK* = 0x20000000
|
||||
SOCK_CLOEXEC* = 0x10000000
|
||||
TCP_NODELAY* = cint(1)
|
||||
IPPROTO_TCP* = 6
|
||||
O_CLOEXEC* = 0x00020000
|
||||
POSIX_SPAWN_USEVFORK* = 0x00
|
||||
IPV6_V6ONLY* = 27
|
||||
elif defined(openbsd):
|
||||
const
|
||||
SOCK_CLOEXEC* = 0x8000
|
||||
SOCK_NONBLOCK* = 0x4000
|
||||
TCP_NODELAY* = cint(1)
|
||||
IPPROTO_TCP* = 6
|
||||
O_CLOEXEC* = 0x10000
|
||||
POSIX_SPAWN_USEVFORK* = 0x00
|
||||
IPV6_V6ONLY* = 27
|
||||
elif defined(macos) or defined(macosx):
|
||||
const
|
||||
TCP_NODELAY* = cint(1)
|
||||
IP_MULTICAST_TTL* = cint(10)
|
||||
IPPROTO_TCP* = 6
|
||||
|
||||
when defined(linux):
|
||||
const
|
||||
O_CLOEXEC* = 0x80000
|
||||
POSIX_SPAWN_USEVFORK* = 0x40
|
||||
elif defined(freebsd):
|
||||
const
|
||||
O_CLOEXEC* = 0x00100000
|
||||
POSIX_SPAWN_USEVFORK* = 0x00
|
||||
elif defined(openbsd):
|
||||
const
|
||||
O_CLOEXEC* = 0x10000
|
||||
POSIX_SPAWN_USEVFORK* = 0x00
|
||||
elif defined(netbsd):
|
||||
const
|
||||
O_CLOEXEC* = 0x00400000
|
||||
POSIX_SPAWN_USEVFORK* = 0x00
|
||||
elif defined(dragonfly):
|
||||
const
|
||||
O_CLOEXEC* = 0x00020000
|
||||
POSIX_SPAWN_USEVFORK* = 0x00
|
||||
elif defined(macos) or defined(macosx):
|
||||
const
|
||||
POSIX_SPAWN_USEVFORK* = 0x00
|
||||
IPV6_V6ONLY* = 27
|
||||
|
||||
when defined(linux) or defined(macos) or defined(macosx) or defined(freebsd) or
|
||||
defined(openbsd) or defined(netbsd) or defined(dragonfly):
|
||||
@ -1468,6 +1526,8 @@ when defined(posix):
|
||||
INVALID_HANDLE_VALUE* = cint(-1)
|
||||
|
||||
proc `==`*(x: SocketHandle, y: int): bool = int(x) == y
|
||||
when defined(nimdoc):
|
||||
proc `==`*(x: SocketHandle, y: SocketHandle): bool {.borrow.}
|
||||
|
||||
when defined(macosx) or defined(macos) or defined(bsd):
|
||||
const
|
||||
@ -1595,6 +1655,8 @@ elif defined(linux):
|
||||
# RTA_PRIORITY* = 6'u16
|
||||
RTA_PREFSRC* = 7'u16
|
||||
# RTA_METRICS* = 8'u16
|
||||
RTM_NEWLINK* = 16'u16
|
||||
RTM_NEWROUTE* = 24'u16
|
||||
|
||||
RTM_F_LOOKUP_TABLE* = 0x1000
|
||||
|
||||
|
||||
@ -1328,6 +1328,7 @@ elif defined(windows):
|
||||
ERROR_CONNECTION_REFUSED* = OSErrorCode(1225)
|
||||
ERROR_CONNECTION_ABORTED* = OSErrorCode(1236)
|
||||
WSAEMFILE* = OSErrorCode(10024)
|
||||
WSAEAFNOSUPPORT* = OSErrorCode(10047)
|
||||
WSAENETDOWN* = OSErrorCode(10050)
|
||||
WSAENETRESET* = OSErrorCode(10052)
|
||||
WSAECONNABORTED* = OSErrorCode(10053)
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
# Licensed under either of
|
||||
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
||||
# MIT license (LICENSE-MIT)
|
||||
import stew/results
|
||||
import osdefs, oserrno
|
||||
import results
|
||||
import "."/[osdefs, oserrno]
|
||||
|
||||
export results
|
||||
|
||||
@ -346,6 +346,10 @@ else:
|
||||
return err(osLastError())
|
||||
ok()
|
||||
|
||||
proc setDescriptorBlocking*(s: SocketHandle,
|
||||
value: bool): Result[void, OSErrorCode] =
|
||||
setDescriptorBlocking(cint(s), value)
|
||||
|
||||
proc setDescriptorInheritance*(s: cint,
|
||||
value: bool): Result[void, OSErrorCode] =
|
||||
let flags = handleEintr(osdefs.fcntl(s, osdefs.F_GETFD))
|
||||
|
||||
@ -88,8 +88,8 @@ proc worker(bucket: TokenBucket) {.async.} =
|
||||
#buckets
|
||||
sleeper = sleepAsync(milliseconds(timeToTarget))
|
||||
await sleeper or eventWaiter
|
||||
sleeper.cancel()
|
||||
eventWaiter.cancel()
|
||||
sleeper.cancelSoon()
|
||||
eventWaiter.cancelSoon()
|
||||
else:
|
||||
await eventWaiter
|
||||
|
||||
|
||||
@ -31,32 +31,11 @@
|
||||
# support - changes could potentially be backported to nim but are not
|
||||
# backwards-compatible.
|
||||
|
||||
import stew/results
|
||||
import osdefs, osutils, oserrno
|
||||
import results
|
||||
import "."/[config, osdefs, osutils, oserrno]
|
||||
export results, oserrno
|
||||
|
||||
const
|
||||
asyncEventsCount* {.intdefine.} = 64
|
||||
## Number of epoll events retrieved by syscall.
|
||||
asyncInitialSize* {.intdefine.} = 64
|
||||
## Initial size of Selector[T]'s array of file descriptors.
|
||||
asyncEventEngine* {.strdefine.} =
|
||||
when defined(linux):
|
||||
"epoll"
|
||||
elif defined(macosx) or defined(macos) or defined(ios) or
|
||||
defined(freebsd) or defined(netbsd) or defined(openbsd) or
|
||||
defined(dragonfly):
|
||||
"kqueue"
|
||||
elif defined(posix):
|
||||
"poll"
|
||||
else:
|
||||
""
|
||||
## Engine type which is going to be used by module.
|
||||
|
||||
hasThreadSupport = compileOption("threads")
|
||||
|
||||
when defined(nimdoc):
|
||||
|
||||
type
|
||||
Selector*[T] = ref object
|
||||
## An object which holds descriptors to be checked for read/write status
|
||||
@ -281,7 +260,9 @@ else:
|
||||
var err = newException(IOSelectorsException, msg)
|
||||
raise err
|
||||
|
||||
when asyncEventEngine in ["epoll", "kqueue"]:
|
||||
when chronosEventEngine in ["epoll", "kqueue"]:
|
||||
const hasThreadSupport = compileOption("threads")
|
||||
|
||||
proc blockSignals(newmask: Sigset,
|
||||
oldmask: var Sigset): Result[void, OSErrorCode] =
|
||||
var nmask = newmask
|
||||
@ -324,11 +305,11 @@ else:
|
||||
doAssert((timeout >= min) and (timeout <= max),
|
||||
"Cannot select with incorrect timeout value, got " & $timeout)
|
||||
|
||||
when asyncEventEngine == "epoll":
|
||||
include ./ioselects/ioselectors_epoll
|
||||
elif asyncEventEngine == "kqueue":
|
||||
include ./ioselects/ioselectors_kqueue
|
||||
elif asyncEventEngine == "poll":
|
||||
include ./ioselects/ioselectors_poll
|
||||
else:
|
||||
{.fatal: "Event engine `" & asyncEventEngine & "` is not supported!".}
|
||||
when chronosEventEngine == "epoll":
|
||||
include ./ioselects/ioselectors_epoll
|
||||
elif chronosEventEngine == "kqueue":
|
||||
include ./ioselects/ioselectors_kqueue
|
||||
elif chronosEventEngine == "poll":
|
||||
include ./ioselects/ioselectors_poll
|
||||
else:
|
||||
{.fatal: "Event engine `" & chronosEventEngine & "` is not supported!".}
|
||||
|
||||
@ -38,8 +38,12 @@ when defined(nimdoc):
|
||||
## be prepared to retry the call if there were unsent bytes.
|
||||
##
|
||||
## On error, ``-1`` is returned.
|
||||
elif defined(emscripten):
|
||||
|
||||
elif defined(linux) or defined(android):
|
||||
proc sendfile*(outfd, infd: int, offset: int, count: var int): int =
|
||||
raiseAssert "sendfile() is not implemented yet"
|
||||
|
||||
elif (defined(linux) or defined(android)) and not(defined(emscripten)):
|
||||
|
||||
proc osSendFile*(outfd, infd: cint, offset: ptr int, count: int): int
|
||||
{.importc: "sendfile", header: "<sys/sendfile.h>".}
|
||||
|
||||
@ -9,12 +9,12 @@
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import ../asyncloop, ../asyncsync
|
||||
import ../transports/common, ../transports/stream
|
||||
import ../[config, asyncloop, asyncsync, bipbuffer]
|
||||
import ../transports/[common, stream]
|
||||
export asyncloop, asyncsync, stream, common
|
||||
|
||||
const
|
||||
AsyncStreamDefaultBufferSize* = 4096
|
||||
AsyncStreamDefaultBufferSize* = chronosStreamDefaultBufferSize
|
||||
## Default reading stream internal buffer size.
|
||||
AsyncStreamDefaultQueueSize* = 0
|
||||
## Default writing stream internal queue size.
|
||||
@ -24,22 +24,21 @@ const
|
||||
## AsyncStreamWriter leaks tracker name
|
||||
|
||||
type
|
||||
AsyncStreamError* = object of CatchableError
|
||||
AsyncStreamError* = object of AsyncError
|
||||
AsyncStreamIncorrectDefect* = object of Defect
|
||||
AsyncStreamIncompleteError* = object of AsyncStreamError
|
||||
AsyncStreamLimitError* = object of AsyncStreamError
|
||||
AsyncStreamUseClosedError* = object of AsyncStreamError
|
||||
AsyncStreamReadError* = object of AsyncStreamError
|
||||
par*: ref CatchableError
|
||||
AsyncStreamWriteError* = object of AsyncStreamError
|
||||
par*: ref CatchableError
|
||||
AsyncStreamWriteEOFError* = object of AsyncStreamWriteError
|
||||
|
||||
AsyncBuffer* = object
|
||||
offset*: int
|
||||
buffer*: seq[byte]
|
||||
backend*: BipBuffer
|
||||
events*: array[2, AsyncEvent]
|
||||
|
||||
AsyncBufferRef* = ref AsyncBuffer
|
||||
|
||||
WriteType* = enum
|
||||
Pointer, Sequence, String
|
||||
|
||||
@ -53,7 +52,7 @@ type
|
||||
dataStr*: string
|
||||
size*: int
|
||||
offset*: int
|
||||
future*: Future[void]
|
||||
future*: Future[void].Raising([CancelledError, AsyncStreamError])
|
||||
|
||||
AsyncStreamState* = enum
|
||||
Running, ## Stream is online and working
|
||||
@ -64,10 +63,10 @@ type
|
||||
Closed ## Stream was closed
|
||||
|
||||
StreamReaderLoop* = proc (stream: AsyncStreamReader): Future[void] {.
|
||||
gcsafe, raises: [].}
|
||||
async: (raises: []).}
|
||||
## Main read loop for read streams.
|
||||
StreamWriterLoop* = proc (stream: AsyncStreamWriter): Future[void] {.
|
||||
gcsafe, raises: [].}
|
||||
async: (raises: []).}
|
||||
## Main write loop for write streams.
|
||||
|
||||
AsyncStreamReader* = ref object of RootRef
|
||||
@ -75,11 +74,11 @@ type
|
||||
tsource*: StreamTransport
|
||||
readerLoop*: StreamReaderLoop
|
||||
state*: AsyncStreamState
|
||||
buffer*: AsyncBuffer
|
||||
buffer*: AsyncBufferRef
|
||||
udata: pointer
|
||||
error*: ref AsyncStreamError
|
||||
bytesCount*: uint64
|
||||
future: Future[void]
|
||||
future: Future[void].Raising([])
|
||||
|
||||
AsyncStreamWriter* = ref object of RootRef
|
||||
wsource*: AsyncStreamWriter
|
||||
@ -90,7 +89,7 @@ type
|
||||
error*: ref AsyncStreamError
|
||||
udata: pointer
|
||||
bytesCount*: uint64
|
||||
future: Future[void]
|
||||
future: Future[void].Raising([])
|
||||
|
||||
AsyncStream* = object of RootObj
|
||||
reader*: AsyncStreamReader
|
||||
@ -98,84 +97,51 @@ type
|
||||
|
||||
AsyncStreamRW* = AsyncStreamReader | AsyncStreamWriter
|
||||
|
||||
proc init*(t: typedesc[AsyncBuffer], size: int): AsyncBuffer =
|
||||
AsyncBuffer(
|
||||
buffer: newSeq[byte](size),
|
||||
events: [newAsyncEvent(), newAsyncEvent()],
|
||||
offset: 0
|
||||
proc new*(t: typedesc[AsyncBufferRef], size: int): AsyncBufferRef =
|
||||
AsyncBufferRef(
|
||||
backend: BipBuffer.init(size),
|
||||
events: [newAsyncEvent(), newAsyncEvent()]
|
||||
)
|
||||
|
||||
proc getBuffer*(sb: AsyncBuffer): pointer {.inline.} =
|
||||
unsafeAddr sb.buffer[sb.offset]
|
||||
|
||||
proc bufferLen*(sb: AsyncBuffer): int {.inline.} =
|
||||
len(sb.buffer) - sb.offset
|
||||
|
||||
proc getData*(sb: AsyncBuffer): pointer {.inline.} =
|
||||
unsafeAddr sb.buffer[0]
|
||||
|
||||
template dataLen*(sb: AsyncBuffer): int =
|
||||
sb.offset
|
||||
|
||||
proc `[]`*(sb: AsyncBuffer, index: int): byte {.inline.} =
|
||||
doAssert(index < sb.offset)
|
||||
sb.buffer[index]
|
||||
|
||||
proc update*(sb: var AsyncBuffer, size: int) {.inline.} =
|
||||
sb.offset += size
|
||||
|
||||
proc wait*(sb: var AsyncBuffer): Future[void] =
|
||||
template wait*(sb: AsyncBufferRef): untyped =
|
||||
sb.events[0].clear()
|
||||
sb.events[1].fire()
|
||||
sb.events[0].wait()
|
||||
|
||||
proc transfer*(sb: var AsyncBuffer): Future[void] =
|
||||
template transfer*(sb: AsyncBufferRef): untyped =
|
||||
sb.events[1].clear()
|
||||
sb.events[0].fire()
|
||||
sb.events[1].wait()
|
||||
|
||||
proc forget*(sb: var AsyncBuffer) {.inline.} =
|
||||
proc forget*(sb: AsyncBufferRef) {.inline.} =
|
||||
sb.events[1].clear()
|
||||
sb.events[0].fire()
|
||||
|
||||
proc shift*(sb: var AsyncBuffer, size: int) {.inline.} =
|
||||
if sb.offset > size:
|
||||
moveMem(addr sb.buffer[0], addr sb.buffer[size], sb.offset - size)
|
||||
sb.offset = sb.offset - size
|
||||
else:
|
||||
sb.offset = 0
|
||||
|
||||
proc copyData*(sb: AsyncBuffer, dest: pointer, offset, length: int) {.inline.} =
|
||||
copyMem(cast[pointer](cast[uint](dest) + cast[uint](offset)),
|
||||
unsafeAddr sb.buffer[0], length)
|
||||
|
||||
proc upload*(sb: ptr AsyncBuffer, pbytes: ptr byte,
|
||||
nbytes: int): Future[void] {.async.} =
|
||||
proc upload*(sb: AsyncBufferRef, pbytes: ptr byte,
|
||||
nbytes: int): Future[void] {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
## You can upload any amount of bytes to the buffer. If size of internal
|
||||
## buffer is not enough to fit all the data at once, data will be uploaded
|
||||
## via chunks of size up to internal buffer size.
|
||||
var length = nbytes
|
||||
var srcBuffer = cast[ptr UncheckedArray[byte]](pbytes)
|
||||
var srcOffset = 0
|
||||
var
|
||||
length = nbytes
|
||||
srcBuffer = pbytes.toUnchecked()
|
||||
offset = 0
|
||||
|
||||
while length > 0:
|
||||
let size = min(length, sb[].bufferLen())
|
||||
let size = min(length, sb.backend.availSpace())
|
||||
if size == 0:
|
||||
# Internal buffer is full, we need to transfer data to consumer.
|
||||
await sb[].transfer()
|
||||
# Internal buffer is full, we need to notify consumer.
|
||||
await sb.transfer()
|
||||
else:
|
||||
let (data, _) = sb.backend.reserve()
|
||||
# Copy data from `pbytes` to internal buffer.
|
||||
copyMem(addr sb[].buffer[sb.offset], addr srcBuffer[srcOffset], size)
|
||||
sb[].offset = sb[].offset + size
|
||||
srcOffset = srcOffset + size
|
||||
copyMem(data, addr srcBuffer[offset], size)
|
||||
sb.backend.commit(size)
|
||||
offset = offset + size
|
||||
length = length - size
|
||||
# We notify consumers that new data is available.
|
||||
sb[].forget()
|
||||
|
||||
template toDataOpenArray*(sb: AsyncBuffer): auto =
|
||||
toOpenArray(sb.buffer, 0, sb.offset - 1)
|
||||
|
||||
template toBufferOpenArray*(sb: AsyncBuffer): auto =
|
||||
toOpenArray(sb.buffer, sb.offset, len(sb.buffer) - 1)
|
||||
sb.forget()
|
||||
|
||||
template copyOut*(dest: pointer, item: WriteItem, length: int) =
|
||||
if item.kind == Pointer:
|
||||
@ -186,18 +152,20 @@ template copyOut*(dest: pointer, item: WriteItem, length: int) =
|
||||
elif item.kind == String:
|
||||
copyMem(dest, unsafeAddr item.dataStr[item.offset], length)
|
||||
|
||||
proc newAsyncStreamReadError(p: ref CatchableError): ref AsyncStreamReadError {.
|
||||
noinline.} =
|
||||
proc newAsyncStreamReadError(
|
||||
p: ref TransportError
|
||||
): ref AsyncStreamReadError {.noinline.} =
|
||||
var w = newException(AsyncStreamReadError, "Read stream failed")
|
||||
w.msg = w.msg & ", originated from [" & $p.name & "] " & p.msg
|
||||
w.par = p
|
||||
w.parent = p
|
||||
w
|
||||
|
||||
proc newAsyncStreamWriteError(p: ref CatchableError): ref AsyncStreamWriteError {.
|
||||
noinline.} =
|
||||
proc newAsyncStreamWriteError(
|
||||
p: ref TransportError
|
||||
): ref AsyncStreamWriteError {.noinline.} =
|
||||
var w = newException(AsyncStreamWriteError, "Write stream failed")
|
||||
w.msg = w.msg & ", originated from [" & $p.name & "] " & p.msg
|
||||
w.par = p
|
||||
w.parent = p
|
||||
w
|
||||
|
||||
proc newAsyncStreamIncompleteError*(): ref AsyncStreamIncompleteError {.
|
||||
@ -242,7 +210,7 @@ proc atEof*(rstream: AsyncStreamReader): bool =
|
||||
rstream.rsource.atEof()
|
||||
else:
|
||||
(rstream.state != AsyncStreamState.Running) and
|
||||
(rstream.buffer.dataLen() == 0)
|
||||
(len(rstream.buffer.backend) == 0)
|
||||
|
||||
proc atEof*(wstream: AsyncStreamWriter): bool =
|
||||
## Returns ``true`` is writing stream ``wstream`` closed or finished.
|
||||
@ -330,12 +298,12 @@ template checkStreamFinished*(t: untyped) =
|
||||
|
||||
template readLoop(body: untyped): untyped =
|
||||
while true:
|
||||
if rstream.buffer.dataLen() == 0:
|
||||
if len(rstream.buffer.backend) == 0:
|
||||
if rstream.state == AsyncStreamState.Error:
|
||||
raise rstream.error
|
||||
|
||||
let (consumed, done) = body
|
||||
rstream.buffer.shift(consumed)
|
||||
rstream.buffer.backend.consume(consumed)
|
||||
rstream.bytesCount = rstream.bytesCount + uint64(consumed)
|
||||
if done:
|
||||
break
|
||||
@ -344,11 +312,12 @@ template readLoop(body: untyped): untyped =
|
||||
await rstream.buffer.wait()
|
||||
|
||||
proc readExactly*(rstream: AsyncStreamReader, pbytes: pointer,
|
||||
nbytes: int) {.async.} =
|
||||
nbytes: int) {.
|
||||
async: (raises: [CancelledError, AsyncStreamError]).} =
|
||||
## Read exactly ``nbytes`` bytes from read-only stream ``rstream`` and store
|
||||
## it to ``pbytes``.
|
||||
##
|
||||
## If EOF is received and ``nbytes`` is not yet readed, the procedure
|
||||
## If EOF is received and ``nbytes`` is not yet read, the procedure
|
||||
## will raise ``AsyncStreamIncompleteError``.
|
||||
doAssert(not(isNil(pbytes)), "pbytes must not be nil")
|
||||
doAssert(nbytes >= 0, "nbytes must be non-negative integer")
|
||||
@ -365,26 +334,33 @@ proc readExactly*(rstream: AsyncStreamReader, pbytes: pointer,
|
||||
raise exc
|
||||
except TransportIncompleteError:
|
||||
raise newAsyncStreamIncompleteError()
|
||||
except CatchableError as exc:
|
||||
except TransportError as exc:
|
||||
raise newAsyncStreamReadError(exc)
|
||||
else:
|
||||
if isNil(rstream.readerLoop):
|
||||
await readExactly(rstream.rsource, pbytes, nbytes)
|
||||
else:
|
||||
var index = 0
|
||||
var pbuffer = cast[ptr UncheckedArray[byte]](pbytes)
|
||||
var
|
||||
index = 0
|
||||
pbuffer = pbytes.toUnchecked()
|
||||
readLoop():
|
||||
if rstream.buffer.dataLen() == 0:
|
||||
if len(rstream.buffer.backend) == 0:
|
||||
if rstream.atEof():
|
||||
raise newAsyncStreamIncompleteError()
|
||||
let count = min(nbytes - index, rstream.buffer.dataLen())
|
||||
if count > 0:
|
||||
rstream.buffer.copyData(addr pbuffer[index], 0, count)
|
||||
index += count
|
||||
(consumed: count, done: index == nbytes)
|
||||
var bytesRead = 0
|
||||
for (region, rsize) in rstream.buffer.backend.regions():
|
||||
let count = min(nbytes - index, rsize)
|
||||
bytesRead += count
|
||||
if count > 0:
|
||||
copyMem(addr pbuffer[index], region, count)
|
||||
index += count
|
||||
if index == nbytes:
|
||||
break
|
||||
(consumed: bytesRead, done: index == nbytes)
|
||||
|
||||
proc readOnce*(rstream: AsyncStreamReader, pbytes: pointer,
|
||||
nbytes: int): Future[int] {.async.} =
|
||||
nbytes: int): Future[int] {.
|
||||
async: (raises: [CancelledError, AsyncStreamError]).} =
|
||||
## Perform one read operation on read-only stream ``rstream``.
|
||||
##
|
||||
## If internal buffer is not empty, ``nbytes`` bytes will be transferred from
|
||||
@ -398,24 +374,31 @@ proc readOnce*(rstream: AsyncStreamReader, pbytes: pointer,
|
||||
return await readOnce(rstream.tsource, pbytes, nbytes)
|
||||
except CancelledError as exc:
|
||||
raise exc
|
||||
except CatchableError as exc:
|
||||
except TransportError as exc:
|
||||
raise newAsyncStreamReadError(exc)
|
||||
else:
|
||||
if isNil(rstream.readerLoop):
|
||||
return await readOnce(rstream.rsource, pbytes, nbytes)
|
||||
else:
|
||||
var count = 0
|
||||
var
|
||||
pbuffer = pbytes.toUnchecked()
|
||||
index = 0
|
||||
readLoop():
|
||||
if rstream.buffer.dataLen() == 0:
|
||||
if len(rstream.buffer.backend) == 0:
|
||||
(0, rstream.atEof())
|
||||
else:
|
||||
count = min(rstream.buffer.dataLen(), nbytes)
|
||||
rstream.buffer.copyData(pbytes, 0, count)
|
||||
(count, true)
|
||||
return count
|
||||
for (region, rsize) in rstream.buffer.backend.regions():
|
||||
let size = min(rsize, nbytes - index)
|
||||
copyMem(addr pbuffer[index], region, size)
|
||||
index += size
|
||||
if index >= nbytes:
|
||||
break
|
||||
(index, true)
|
||||
index
|
||||
|
||||
proc readUntil*(rstream: AsyncStreamReader, pbytes: pointer, nbytes: int,
|
||||
sep: seq[byte]): Future[int] {.async.} =
|
||||
sep: seq[byte]): Future[int] {.
|
||||
async: (raises: [CancelledError, AsyncStreamError]).} =
|
||||
## Read data from the read-only stream ``rstream`` until separator ``sep`` is
|
||||
## found.
|
||||
##
|
||||
@ -446,37 +429,42 @@ proc readUntil*(rstream: AsyncStreamReader, pbytes: pointer, nbytes: int,
|
||||
raise newAsyncStreamIncompleteError()
|
||||
except TransportLimitError:
|
||||
raise newAsyncStreamLimitError()
|
||||
except CatchableError as exc:
|
||||
except TransportError as exc:
|
||||
raise newAsyncStreamReadError(exc)
|
||||
else:
|
||||
if isNil(rstream.readerLoop):
|
||||
return await readUntil(rstream.rsource, pbytes, nbytes, sep)
|
||||
else:
|
||||
var pbuffer = cast[ptr UncheckedArray[byte]](pbytes)
|
||||
var state = 0
|
||||
var k = 0
|
||||
var
|
||||
pbuffer = pbytes.toUnchecked()
|
||||
state = 0
|
||||
k = 0
|
||||
readLoop():
|
||||
if rstream.atEof():
|
||||
raise newAsyncStreamIncompleteError()
|
||||
|
||||
var index = 0
|
||||
while index < rstream.buffer.dataLen():
|
||||
for ch in rstream.buffer.backend:
|
||||
if k >= nbytes:
|
||||
raise newAsyncStreamLimitError()
|
||||
let ch = rstream.buffer[index]
|
||||
|
||||
inc(index)
|
||||
pbuffer[k] = ch
|
||||
inc(k)
|
||||
|
||||
if sep[state] == ch:
|
||||
inc(state)
|
||||
if state == len(sep):
|
||||
break
|
||||
else:
|
||||
state = 0
|
||||
|
||||
(index, state == len(sep))
|
||||
return k
|
||||
k
|
||||
|
||||
proc readLine*(rstream: AsyncStreamReader, limit = 0,
|
||||
sep = "\r\n"): Future[string] {.async.} =
|
||||
sep = "\r\n"): Future[string] {.
|
||||
async: (raises: [CancelledError, AsyncStreamError]).} =
|
||||
## Read one line from read-only stream ``rstream``, where ``"line"`` is a
|
||||
## sequence of bytes ending with ``sep`` (default is ``"\r\n"``).
|
||||
##
|
||||
@ -495,25 +483,26 @@ proc readLine*(rstream: AsyncStreamReader, limit = 0,
|
||||
return await readLine(rstream.tsource, limit, sep)
|
||||
except CancelledError as exc:
|
||||
raise exc
|
||||
except CatchableError as exc:
|
||||
except TransportError as exc:
|
||||
raise newAsyncStreamReadError(exc)
|
||||
else:
|
||||
if isNil(rstream.readerLoop):
|
||||
return await readLine(rstream.rsource, limit, sep)
|
||||
else:
|
||||
let lim = if limit <= 0: -1 else: limit
|
||||
var state = 0
|
||||
var res = ""
|
||||
var
|
||||
state = 0
|
||||
res = ""
|
||||
|
||||
readLoop():
|
||||
if rstream.atEof():
|
||||
(0, true)
|
||||
else:
|
||||
var index = 0
|
||||
while index < rstream.buffer.dataLen():
|
||||
let ch = char(rstream.buffer[index])
|
||||
for ch in rstream.buffer.backend:
|
||||
inc(index)
|
||||
|
||||
if sep[state] == ch:
|
||||
if sep[state] == char(ch):
|
||||
inc(state)
|
||||
if state == len(sep):
|
||||
break
|
||||
@ -524,13 +513,17 @@ proc readLine*(rstream: AsyncStreamReader, limit = 0,
|
||||
res.add(sep[0 ..< missing])
|
||||
else:
|
||||
res.add(sep[0 ..< state])
|
||||
res.add(ch)
|
||||
state = 0
|
||||
|
||||
res.add(char(ch))
|
||||
if len(res) == lim:
|
||||
break
|
||||
(index, (state == len(sep)) or (lim == len(res)))
|
||||
return res
|
||||
|
||||
proc read*(rstream: AsyncStreamReader): Future[seq[byte]] {.async.} =
|
||||
(index, (state == len(sep)) or (lim == len(res)))
|
||||
res
|
||||
|
||||
proc read*(rstream: AsyncStreamReader): Future[seq[byte]] {.
|
||||
async: (raises: [CancelledError, AsyncStreamError]).} =
|
||||
## Read all bytes from read-only stream ``rstream``.
|
||||
##
|
||||
## This procedure allocates buffer seq[byte] and return it as result.
|
||||
@ -543,23 +536,26 @@ proc read*(rstream: AsyncStreamReader): Future[seq[byte]] {.async.} =
|
||||
raise exc
|
||||
except TransportLimitError:
|
||||
raise newAsyncStreamLimitError()
|
||||
except CatchableError as exc:
|
||||
except TransportError as exc:
|
||||
raise newAsyncStreamReadError(exc)
|
||||
else:
|
||||
if isNil(rstream.readerLoop):
|
||||
return await read(rstream.rsource)
|
||||
else:
|
||||
var res = newSeq[byte]()
|
||||
var res: seq[byte]
|
||||
readLoop():
|
||||
if rstream.atEof():
|
||||
(0, true)
|
||||
else:
|
||||
let count = rstream.buffer.dataLen()
|
||||
res.add(rstream.buffer.buffer.toOpenArray(0, count - 1))
|
||||
(count, false)
|
||||
return res
|
||||
var bytesRead = 0
|
||||
for (region, rsize) in rstream.buffer.backend.regions():
|
||||
bytesRead += rsize
|
||||
res.add(region.toUnchecked().toOpenArray(0, rsize - 1))
|
||||
(bytesRead, false)
|
||||
res
|
||||
|
||||
proc read*(rstream: AsyncStreamReader, n: int): Future[seq[byte]] {.async.} =
|
||||
proc read*(rstream: AsyncStreamReader, n: int): Future[seq[byte]] {.
|
||||
async: (raises: [CancelledError, AsyncStreamError]).} =
|
||||
## Read all bytes (n <= 0) or exactly `n` bytes from read-only stream
|
||||
## ``rstream``.
|
||||
##
|
||||
@ -571,7 +567,7 @@ proc read*(rstream: AsyncStreamReader, n: int): Future[seq[byte]] {.async.} =
|
||||
return await read(rstream.tsource, n)
|
||||
except CancelledError as exc:
|
||||
raise exc
|
||||
except CatchableError as exc:
|
||||
except TransportError as exc:
|
||||
raise newAsyncStreamReadError(exc)
|
||||
else:
|
||||
if isNil(rstream.readerLoop):
|
||||
@ -585,12 +581,16 @@ proc read*(rstream: AsyncStreamReader, n: int): Future[seq[byte]] {.async.} =
|
||||
if rstream.atEof():
|
||||
(0, true)
|
||||
else:
|
||||
let count = min(rstream.buffer.dataLen(), n - len(res))
|
||||
res.add(rstream.buffer.buffer.toOpenArray(0, count - 1))
|
||||
(count, len(res) == n)
|
||||
return res
|
||||
var bytesRead = 0
|
||||
for (region, rsize) in rstream.buffer.backend.regions():
|
||||
let count = min(rsize, n - len(res))
|
||||
bytesRead += count
|
||||
res.add(region.toUnchecked().toOpenArray(0, count - 1))
|
||||
(bytesRead, len(res) == n)
|
||||
res
|
||||
|
||||
proc consume*(rstream: AsyncStreamReader): Future[int] {.async.} =
|
||||
proc consume*(rstream: AsyncStreamReader): Future[int] {.
|
||||
async: (raises: [CancelledError, AsyncStreamError]).} =
|
||||
## Consume (discard) all bytes from read-only stream ``rstream``.
|
||||
##
|
||||
## Return number of bytes actually consumed (discarded).
|
||||
@ -603,7 +603,7 @@ proc consume*(rstream: AsyncStreamReader): Future[int] {.async.} =
|
||||
raise exc
|
||||
except TransportLimitError:
|
||||
raise newAsyncStreamLimitError()
|
||||
except CatchableError as exc:
|
||||
except TransportError as exc:
|
||||
raise newAsyncStreamReadError(exc)
|
||||
else:
|
||||
if isNil(rstream.readerLoop):
|
||||
@ -614,11 +614,13 @@ proc consume*(rstream: AsyncStreamReader): Future[int] {.async.} =
|
||||
if rstream.atEof():
|
||||
(0, true)
|
||||
else:
|
||||
res += rstream.buffer.dataLen()
|
||||
(rstream.buffer.dataLen(), false)
|
||||
return res
|
||||
let used = len(rstream.buffer.backend)
|
||||
res += used
|
||||
(used, false)
|
||||
res
|
||||
|
||||
proc consume*(rstream: AsyncStreamReader, n: int): Future[int] {.async.} =
|
||||
proc consume*(rstream: AsyncStreamReader, n: int): Future[int] {.
|
||||
async: (raises: [CancelledError, AsyncStreamError]).} =
|
||||
## Consume (discard) all bytes (n <= 0) or ``n`` bytes from read-only stream
|
||||
## ``rstream``.
|
||||
##
|
||||
@ -632,7 +634,7 @@ proc consume*(rstream: AsyncStreamReader, n: int): Future[int] {.async.} =
|
||||
raise exc
|
||||
except TransportLimitError:
|
||||
raise newAsyncStreamLimitError()
|
||||
except CatchableError as exc:
|
||||
except TransportError as exc:
|
||||
raise newAsyncStreamReadError(exc)
|
||||
else:
|
||||
if isNil(rstream.readerLoop):
|
||||
@ -643,16 +645,15 @@ proc consume*(rstream: AsyncStreamReader, n: int): Future[int] {.async.} =
|
||||
else:
|
||||
var res = 0
|
||||
readLoop():
|
||||
if rstream.atEof():
|
||||
(0, true)
|
||||
else:
|
||||
let count = min(rstream.buffer.dataLen(), n - res)
|
||||
res += count
|
||||
(count, res == n)
|
||||
return res
|
||||
let
|
||||
used = len(rstream.buffer.backend)
|
||||
count = min(used, n - res)
|
||||
res += count
|
||||
(count, res == n)
|
||||
res
|
||||
|
||||
proc readMessage*(rstream: AsyncStreamReader, pred: ReadMessagePredicate) {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError, AsyncStreamError]).} =
|
||||
## Read all bytes from stream ``rstream`` until ``predicate`` callback
|
||||
## will not be satisfied.
|
||||
##
|
||||
@ -673,25 +674,29 @@ proc readMessage*(rstream: AsyncStreamReader, pred: ReadMessagePredicate) {.
|
||||
await readMessage(rstream.tsource, pred)
|
||||
except CancelledError as exc:
|
||||
raise exc
|
||||
except CatchableError as exc:
|
||||
except TransportError as exc:
|
||||
raise newAsyncStreamReadError(exc)
|
||||
else:
|
||||
if isNil(rstream.readerLoop):
|
||||
await readMessage(rstream.rsource, pred)
|
||||
else:
|
||||
readLoop():
|
||||
let count = rstream.buffer.dataLen()
|
||||
if count == 0:
|
||||
if len(rstream.buffer.backend) == 0:
|
||||
if rstream.atEof():
|
||||
pred([])
|
||||
else:
|
||||
# Case, when transport's buffer is not yet filled with data.
|
||||
(0, false)
|
||||
else:
|
||||
pred(rstream.buffer.buffer.toOpenArray(0, count - 1))
|
||||
var res: tuple[consumed: int, done: bool]
|
||||
for (region, rsize) in rstream.buffer.backend.regions():
|
||||
res = pred(region.toUnchecked().toOpenArray(0, rsize - 1))
|
||||
break
|
||||
res
|
||||
|
||||
proc write*(wstream: AsyncStreamWriter, pbytes: pointer,
|
||||
nbytes: int) {.async.} =
|
||||
nbytes: int) {.
|
||||
async: (raises: [CancelledError, AsyncStreamError]).} =
|
||||
## Write sequence of bytes pointed by ``pbytes`` of length ``nbytes`` to
|
||||
## writer stream ``wstream``.
|
||||
##
|
||||
@ -708,9 +713,7 @@ proc write*(wstream: AsyncStreamWriter, pbytes: pointer,
|
||||
res = await write(wstream.tsource, pbytes, nbytes)
|
||||
except CancelledError as exc:
|
||||
raise exc
|
||||
except AsyncStreamError as exc:
|
||||
raise exc
|
||||
except CatchableError as exc:
|
||||
except TransportError as exc:
|
||||
raise newAsyncStreamWriteError(exc)
|
||||
if res != nbytes:
|
||||
raise newAsyncStreamIncompleteError()
|
||||
@ -720,23 +723,17 @@ proc write*(wstream: AsyncStreamWriter, pbytes: pointer,
|
||||
await write(wstream.wsource, pbytes, nbytes)
|
||||
wstream.bytesCount = wstream.bytesCount + uint64(nbytes)
|
||||
else:
|
||||
var item = WriteItem(kind: Pointer)
|
||||
item.dataPtr = pbytes
|
||||
item.size = nbytes
|
||||
item.future = newFuture[void]("async.stream.write(pointer)")
|
||||
try:
|
||||
await wstream.queue.put(item)
|
||||
await item.future
|
||||
wstream.bytesCount = wstream.bytesCount + uint64(item.size)
|
||||
except CancelledError as exc:
|
||||
raise exc
|
||||
except AsyncStreamError as exc:
|
||||
raise exc
|
||||
except CatchableError as exc:
|
||||
raise newAsyncStreamWriteError(exc)
|
||||
let item = WriteItem(
|
||||
kind: Pointer, dataPtr: pbytes, size: nbytes,
|
||||
future: Future[void].Raising([CancelledError, AsyncStreamError])
|
||||
.init("async.stream.write(pointer)"))
|
||||
await wstream.queue.put(item)
|
||||
await item.future
|
||||
wstream.bytesCount = wstream.bytesCount + uint64(item.size)
|
||||
|
||||
proc write*(wstream: AsyncStreamWriter, sbytes: sink seq[byte],
|
||||
msglen = -1) {.async.} =
|
||||
proc write*(wstream: AsyncStreamWriter, sbytes: seq[byte],
|
||||
msglen = -1) {.
|
||||
async: (raises: [CancelledError, AsyncStreamError]).} =
|
||||
## Write sequence of bytes ``sbytes`` of length ``msglen`` to writer
|
||||
## stream ``wstream``.
|
||||
##
|
||||
@ -758,7 +755,7 @@ proc write*(wstream: AsyncStreamWriter, sbytes: sink seq[byte],
|
||||
res = await write(wstream.tsource, sbytes, length)
|
||||
except CancelledError as exc:
|
||||
raise exc
|
||||
except CatchableError as exc:
|
||||
except TransportError as exc:
|
||||
raise newAsyncStreamWriteError(exc)
|
||||
if res != length:
|
||||
raise newAsyncStreamIncompleteError()
|
||||
@ -768,29 +765,17 @@ proc write*(wstream: AsyncStreamWriter, sbytes: sink seq[byte],
|
||||
await write(wstream.wsource, sbytes, length)
|
||||
wstream.bytesCount = wstream.bytesCount + uint64(length)
|
||||
else:
|
||||
var item = WriteItem(kind: Sequence)
|
||||
when declared(shallowCopy):
|
||||
if not(isLiteral(sbytes)):
|
||||
shallowCopy(item.dataSeq, sbytes)
|
||||
else:
|
||||
item.dataSeq = sbytes
|
||||
else:
|
||||
item.dataSeq = sbytes
|
||||
item.size = length
|
||||
item.future = newFuture[void]("async.stream.write(seq)")
|
||||
try:
|
||||
await wstream.queue.put(item)
|
||||
await item.future
|
||||
wstream.bytesCount = wstream.bytesCount + uint64(item.size)
|
||||
except CancelledError as exc:
|
||||
raise exc
|
||||
except AsyncStreamError as exc:
|
||||
raise exc
|
||||
except CatchableError as exc:
|
||||
raise newAsyncStreamWriteError(exc)
|
||||
let item = WriteItem(
|
||||
kind: Sequence, dataSeq: sbytes, size: length,
|
||||
future: Future[void].Raising([CancelledError, AsyncStreamError])
|
||||
.init("async.stream.write(seq)"))
|
||||
await wstream.queue.put(item)
|
||||
await item.future
|
||||
wstream.bytesCount = wstream.bytesCount + uint64(item.size)
|
||||
|
||||
proc write*(wstream: AsyncStreamWriter, sbytes: sink string,
|
||||
msglen = -1) {.async.} =
|
||||
proc write*(wstream: AsyncStreamWriter, sbytes: string,
|
||||
msglen = -1) {.
|
||||
async: (raises: [CancelledError, AsyncStreamError]).} =
|
||||
## Write string ``sbytes`` of length ``msglen`` to writer stream ``wstream``.
|
||||
##
|
||||
## String ``sbytes`` must not be zero-length.
|
||||
@ -811,7 +796,7 @@ proc write*(wstream: AsyncStreamWriter, sbytes: sink string,
|
||||
res = await write(wstream.tsource, sbytes, length)
|
||||
except CancelledError as exc:
|
||||
raise exc
|
||||
except CatchableError as exc:
|
||||
except TransportError as exc:
|
||||
raise newAsyncStreamWriteError(exc)
|
||||
if res != length:
|
||||
raise newAsyncStreamIncompleteError()
|
||||
@ -821,28 +806,16 @@ proc write*(wstream: AsyncStreamWriter, sbytes: sink string,
|
||||
await write(wstream.wsource, sbytes, length)
|
||||
wstream.bytesCount = wstream.bytesCount + uint64(length)
|
||||
else:
|
||||
var item = WriteItem(kind: String)
|
||||
when declared(shallowCopy):
|
||||
if not(isLiteral(sbytes)):
|
||||
shallowCopy(item.dataStr, sbytes)
|
||||
else:
|
||||
item.dataStr = sbytes
|
||||
else:
|
||||
item.dataStr = sbytes
|
||||
item.size = length
|
||||
item.future = newFuture[void]("async.stream.write(string)")
|
||||
try:
|
||||
await wstream.queue.put(item)
|
||||
await item.future
|
||||
wstream.bytesCount = wstream.bytesCount + uint64(item.size)
|
||||
except CancelledError as exc:
|
||||
raise exc
|
||||
except AsyncStreamError as exc:
|
||||
raise exc
|
||||
except CatchableError as exc:
|
||||
raise newAsyncStreamWriteError(exc)
|
||||
let item = WriteItem(
|
||||
kind: String, dataStr: sbytes, size: length,
|
||||
future: Future[void].Raising([CancelledError, AsyncStreamError])
|
||||
.init("async.stream.write(string)"))
|
||||
await wstream.queue.put(item)
|
||||
await item.future
|
||||
wstream.bytesCount = wstream.bytesCount + uint64(item.size)
|
||||
|
||||
proc finish*(wstream: AsyncStreamWriter) {.async.} =
|
||||
proc finish*(wstream: AsyncStreamWriter) {.
|
||||
async: (raises: [CancelledError, AsyncStreamError]).} =
|
||||
## Finish write stream ``wstream``.
|
||||
checkStreamClosed(wstream)
|
||||
# For AsyncStreamWriter Finished state could be set manually or by stream's
|
||||
@ -852,40 +825,18 @@ proc finish*(wstream: AsyncStreamWriter) {.async.} =
|
||||
if isNil(wstream.writerLoop):
|
||||
await wstream.wsource.finish()
|
||||
else:
|
||||
var item = WriteItem(kind: Pointer)
|
||||
item.size = 0
|
||||
item.future = newFuture[void]("async.stream.finish")
|
||||
try:
|
||||
await wstream.queue.put(item)
|
||||
await item.future
|
||||
except CancelledError as exc:
|
||||
raise exc
|
||||
except AsyncStreamError as exc:
|
||||
raise exc
|
||||
except CatchableError as exc:
|
||||
raise newAsyncStreamWriteError(exc)
|
||||
let item = WriteItem(
|
||||
kind: Pointer, size: 0,
|
||||
future: Future[void].Raising([CancelledError, AsyncStreamError])
|
||||
.init("async.stream.finish"))
|
||||
await wstream.queue.put(item)
|
||||
await item.future
|
||||
|
||||
proc join*(rw: AsyncStreamRW): Future[void] =
|
||||
proc join*(rw: AsyncStreamRW): Future[void] {.
|
||||
async: (raw: true, raises: [CancelledError]).} =
|
||||
## Get Future[void] which will be completed when stream become finished or
|
||||
## closed.
|
||||
when rw is AsyncStreamReader:
|
||||
var retFuture = newFuture[void]("async.stream.reader.join")
|
||||
else:
|
||||
var retFuture = newFuture[void]("async.stream.writer.join")
|
||||
|
||||
proc continuation(udata: pointer) {.gcsafe.} =
|
||||
retFuture.complete()
|
||||
|
||||
proc cancellation(udata: pointer) {.gcsafe.} =
|
||||
rw.future.removeCallback(continuation, cast[pointer](retFuture))
|
||||
|
||||
if not(rw.future.finished()):
|
||||
rw.future.addCallback(continuation, cast[pointer](retFuture))
|
||||
retFuture.cancelCallback = cancellation
|
||||
else:
|
||||
retFuture.complete()
|
||||
|
||||
return retFuture
|
||||
rw.future.join()
|
||||
|
||||
proc close*(rw: AsyncStreamRW) =
|
||||
## Close and frees resources of stream ``rw``.
|
||||
@ -913,7 +864,7 @@ proc close*(rw: AsyncStreamRW) =
|
||||
callSoon(continuation)
|
||||
else:
|
||||
rw.future.addCallback(continuation)
|
||||
rw.future.cancel()
|
||||
rw.future.cancelSoon()
|
||||
elif rw is AsyncStreamWriter:
|
||||
if isNil(rw.wsource) or isNil(rw.writerLoop) or isNil(rw.future):
|
||||
callSoon(continuation)
|
||||
@ -922,26 +873,29 @@ proc close*(rw: AsyncStreamRW) =
|
||||
callSoon(continuation)
|
||||
else:
|
||||
rw.future.addCallback(continuation)
|
||||
rw.future.cancel()
|
||||
rw.future.cancelSoon()
|
||||
|
||||
proc closeWait*(rw: AsyncStreamRW): Future[void] =
|
||||
proc closeWait*(rw: AsyncStreamRW): Future[void] {.async: (raises: []).} =
|
||||
## Close and frees resources of stream ``rw``.
|
||||
rw.close()
|
||||
rw.join()
|
||||
if not rw.closed():
|
||||
rw.close()
|
||||
await noCancel(rw.join())
|
||||
|
||||
proc startReader(rstream: AsyncStreamReader) =
|
||||
rstream.state = Running
|
||||
if not isNil(rstream.readerLoop):
|
||||
rstream.future = rstream.readerLoop(rstream)
|
||||
else:
|
||||
rstream.future = newFuture[void]("async.stream.empty.reader")
|
||||
rstream.future = Future[void].Raising([]).init(
|
||||
"async.stream.empty.reader", {FutureFlag.OwnCancelSchedule})
|
||||
|
||||
proc startWriter(wstream: AsyncStreamWriter) =
|
||||
wstream.state = Running
|
||||
if not isNil(wstream.writerLoop):
|
||||
wstream.future = wstream.writerLoop(wstream)
|
||||
else:
|
||||
wstream.future = newFuture[void]("async.stream.empty.writer")
|
||||
wstream.future = Future[void].Raising([]).init(
|
||||
"async.stream.empty.writer", {FutureFlag.OwnCancelSchedule})
|
||||
|
||||
proc init*(child, wsource: AsyncStreamWriter, loop: StreamWriterLoop,
|
||||
queueSize = AsyncStreamDefaultQueueSize) =
|
||||
@ -975,7 +929,8 @@ proc init*(child, rsource: AsyncStreamReader, loop: StreamReaderLoop,
|
||||
child.readerLoop = loop
|
||||
child.rsource = rsource
|
||||
child.tsource = rsource.tsource
|
||||
child.buffer = AsyncBuffer.init(bufferSize)
|
||||
let size = max(AsyncStreamDefaultBufferSize, bufferSize)
|
||||
child.buffer = AsyncBufferRef.new(size)
|
||||
trackCounter(AsyncStreamReaderTrackerName)
|
||||
child.startReader()
|
||||
|
||||
@ -987,7 +942,8 @@ proc init*[T](child, rsource: AsyncStreamReader, loop: StreamReaderLoop,
|
||||
child.readerLoop = loop
|
||||
child.rsource = rsource
|
||||
child.tsource = rsource.tsource
|
||||
child.buffer = AsyncBuffer.init(bufferSize)
|
||||
let size = max(AsyncStreamDefaultBufferSize, bufferSize)
|
||||
child.buffer = AsyncBufferRef.new(size)
|
||||
if not isNil(udata):
|
||||
GC_ref(udata)
|
||||
child.udata = cast[pointer](udata)
|
||||
@ -1126,6 +1082,22 @@ proc newAsyncStreamReader*(tsource: StreamTransport): AsyncStreamReader =
|
||||
res.init(tsource)
|
||||
res
|
||||
|
||||
proc newAsyncStreamReader*[T](rsource: AsyncStreamReader,
|
||||
udata: ref T): AsyncStreamReader =
|
||||
## Create copy of AsyncStreamReader object ``rsource``.
|
||||
##
|
||||
## ``udata`` - user object which will be associated with new AsyncStreamReader
|
||||
## object.
|
||||
var res = AsyncStreamReader()
|
||||
res.init(rsource, udata)
|
||||
res
|
||||
|
||||
proc newAsyncStreamReader*(rsource: AsyncStreamReader): AsyncStreamReader =
|
||||
## Create copy of AsyncStreamReader object ``rsource``.
|
||||
var res = AsyncStreamReader()
|
||||
res.init(rsource)
|
||||
res
|
||||
|
||||
proc newAsyncStreamWriter*[T](wsource: AsyncStreamWriter,
|
||||
loop: StreamWriterLoop,
|
||||
queueSize = AsyncStreamDefaultQueueSize,
|
||||
@ -1191,22 +1163,6 @@ proc newAsyncStreamWriter*(wsource: AsyncStreamWriter): AsyncStreamWriter =
|
||||
res.init(wsource)
|
||||
res
|
||||
|
||||
proc newAsyncStreamReader*[T](rsource: AsyncStreamWriter,
|
||||
udata: ref T): AsyncStreamWriter =
|
||||
## Create copy of AsyncStreamReader object ``rsource``.
|
||||
##
|
||||
## ``udata`` - user object which will be associated with new AsyncStreamReader
|
||||
## object.
|
||||
var res = AsyncStreamReader()
|
||||
res.init(rsource, udata)
|
||||
res
|
||||
|
||||
proc newAsyncStreamReader*(rsource: AsyncStreamReader): AsyncStreamReader =
|
||||
## Create copy of AsyncStreamReader object ``rsource``.
|
||||
var res = AsyncStreamReader()
|
||||
res.init(rsource)
|
||||
res
|
||||
|
||||
proc getUserData*[T](rw: AsyncStreamRW): T {.inline.} =
|
||||
## Obtain user data associated with AsyncStreamReader or AsyncStreamWriter
|
||||
## object ``rw``.
|
||||
|
||||
@ -14,9 +14,12 @@
|
||||
##
|
||||
## For stream writing it means that you should write exactly bounded size
|
||||
## of bytes.
|
||||
import stew/results
|
||||
import ../asyncloop, ../timer
|
||||
import asyncstream, ../transports/stream, ../transports/common
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import results
|
||||
import ../[asyncloop, timer, bipbuffer, config]
|
||||
import asyncstream, ../transports/[stream, common]
|
||||
export asyncloop, asyncstream, stream, timer, common
|
||||
|
||||
type
|
||||
@ -41,7 +44,7 @@ type
|
||||
BoundedStreamRW* = BoundedStreamReader | BoundedStreamWriter
|
||||
|
||||
const
|
||||
BoundedBufferSize* = 4096
|
||||
BoundedBufferSize* = chronosStreamDefaultBufferSize
|
||||
BoundarySizeDefectMessage = "Boundary must not be empty array"
|
||||
|
||||
template newBoundedStreamIncompleteError(): ref BoundedStreamError =
|
||||
@ -52,7 +55,8 @@ template newBoundedStreamOverflowError(): ref BoundedStreamOverflowError =
|
||||
newException(BoundedStreamOverflowError, "Stream boundary exceeded")
|
||||
|
||||
proc readUntilBoundary(rstream: AsyncStreamReader, pbytes: pointer,
|
||||
nbytes: int, sep: seq[byte]): Future[int] {.async.} =
|
||||
nbytes: int, sep: seq[byte]): Future[int] {.
|
||||
async: (raises: [CancelledError, AsyncStreamError]).} =
|
||||
doAssert(not(isNil(pbytes)), "pbytes must not be nil")
|
||||
doAssert(nbytes >= 0, "nbytes must be non-negative value")
|
||||
checkStreamClosed(rstream)
|
||||
@ -96,10 +100,10 @@ func endsWith(s, suffix: openArray[byte]): bool =
|
||||
inc(i)
|
||||
if i >= len(suffix): return true
|
||||
|
||||
proc boundedReadLoop(stream: AsyncStreamReader) {.async.} =
|
||||
proc boundedReadLoop(stream: AsyncStreamReader) {.async: (raises: []).} =
|
||||
var rstream = BoundedStreamReader(stream)
|
||||
rstream.state = AsyncStreamState.Running
|
||||
var buffer = newSeq[byte](rstream.buffer.bufferLen())
|
||||
var buffer = newSeq[byte](rstream.buffer.backend.availSpace())
|
||||
while true:
|
||||
let toRead =
|
||||
if rstream.boundSize.isNone():
|
||||
@ -123,7 +127,7 @@ proc boundedReadLoop(stream: AsyncStreamReader) {.async.} =
|
||||
# There should be one step between transferring last bytes to the
|
||||
# consumer and declaring stream EOF. Otherwise could not be
|
||||
# consumed.
|
||||
await upload(addr rstream.buffer, addr buffer[0], length)
|
||||
await upload(rstream.buffer, addr buffer[0], length)
|
||||
if rstream.state == AsyncStreamState.Running:
|
||||
rstream.state = AsyncStreamState.Finished
|
||||
else:
|
||||
@ -131,7 +135,7 @@ proc boundedReadLoop(stream: AsyncStreamReader) {.async.} =
|
||||
# There should be one step between transferring last bytes to the
|
||||
# consumer and declaring stream EOF. Otherwise could not be
|
||||
# consumed.
|
||||
await upload(addr rstream.buffer, addr buffer[0], res)
|
||||
await upload(rstream.buffer, addr buffer[0], res)
|
||||
|
||||
if (res < toRead) and rstream.rsource.atEof():
|
||||
case rstream.cmpop
|
||||
@ -147,7 +151,7 @@ proc boundedReadLoop(stream: AsyncStreamReader) {.async.} =
|
||||
# There should be one step between transferring last bytes to the
|
||||
# consumer and declaring stream EOF. Otherwise could not be
|
||||
# consumed.
|
||||
await upload(addr rstream.buffer, addr buffer[0], res)
|
||||
await upload(rstream.buffer, addr buffer[0], res)
|
||||
|
||||
if (res < toRead) and rstream.rsource.atEof():
|
||||
case rstream.cmpop
|
||||
@ -186,12 +190,16 @@ proc boundedReadLoop(stream: AsyncStreamReader) {.async.} =
|
||||
break
|
||||
of AsyncStreamState.Finished:
|
||||
# Send `EOF` state to the consumer and wait until it will be received.
|
||||
await rstream.buffer.transfer()
|
||||
try:
|
||||
await rstream.buffer.transfer()
|
||||
except CancelledError:
|
||||
rstream.state = AsyncStreamState.Error
|
||||
rstream.error = newBoundedStreamIncompleteError()
|
||||
break
|
||||
of AsyncStreamState.Closing, AsyncStreamState.Closed:
|
||||
break
|
||||
|
||||
proc boundedWriteLoop(stream: AsyncStreamWriter) {.async.} =
|
||||
proc boundedWriteLoop(stream: AsyncStreamWriter) {.async: (raises: []).} =
|
||||
var error: ref AsyncStreamError
|
||||
var wstream = BoundedStreamWriter(stream)
|
||||
|
||||
@ -255,7 +263,11 @@ proc boundedWriteLoop(stream: AsyncStreamWriter) {.async.} =
|
||||
|
||||
doAssert(not(isNil(error)))
|
||||
while not(wstream.queue.empty()):
|
||||
let item = wstream.queue.popFirstNoWait()
|
||||
let item =
|
||||
try:
|
||||
wstream.queue.popFirstNoWait()
|
||||
except AsyncQueueEmptyError:
|
||||
raiseAssert "AsyncQueue should not be empty at this moment"
|
||||
if not(item.future.finished()):
|
||||
item.future.fail(error)
|
||||
|
||||
|
||||
@ -8,13 +8,16 @@
|
||||
# MIT license (LICENSE-MIT)
|
||||
|
||||
## This module implements HTTP/1.1 chunked-encoded stream reading and writing.
|
||||
import ../asyncloop, ../timer
|
||||
import asyncstream, ../transports/stream, ../transports/common
|
||||
import stew/results
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import ../[asyncloop, timer, bipbuffer, config]
|
||||
import asyncstream, ../transports/[stream, common]
|
||||
import results
|
||||
export asyncloop, asyncstream, stream, timer, common, results
|
||||
|
||||
const
|
||||
ChunkBufferSize = 4096
|
||||
ChunkBufferSize = chronosStreamDefaultBufferSize
|
||||
MaxChunkHeaderSize = 1024
|
||||
ChunkHeaderValueSize = 8
|
||||
# This is limit for chunk size to 8 hexadecimal digits, so maximum
|
||||
@ -95,7 +98,7 @@ proc setChunkSize(buffer: var openArray[byte], length: int64): int =
|
||||
buffer[c + 1] = byte(0x0A)
|
||||
(c + 2)
|
||||
|
||||
proc chunkedReadLoop(stream: AsyncStreamReader) {.async.} =
|
||||
proc chunkedReadLoop(stream: AsyncStreamReader) {.async: (raises: []).} =
|
||||
var rstream = ChunkedStreamReader(stream)
|
||||
var buffer = newSeq[byte](MaxChunkHeaderSize)
|
||||
rstream.state = AsyncStreamState.Running
|
||||
@ -115,11 +118,11 @@ proc chunkedReadLoop(stream: AsyncStreamReader) {.async.} =
|
||||
var chunksize = cres.get()
|
||||
if chunksize > 0'u64:
|
||||
while chunksize > 0'u64:
|
||||
let toRead = int(min(chunksize,
|
||||
uint64(rstream.buffer.bufferLen())))
|
||||
await rstream.rsource.readExactly(rstream.buffer.getBuffer(),
|
||||
toRead)
|
||||
rstream.buffer.update(toRead)
|
||||
let
|
||||
(data, rsize) = rstream.buffer.backend.reserve()
|
||||
toRead = int(min(chunksize, uint64(rsize)))
|
||||
await rstream.rsource.readExactly(data, toRead)
|
||||
rstream.buffer.backend.commit(toRead)
|
||||
await rstream.buffer.transfer()
|
||||
chunksize = chunksize - uint64(toRead)
|
||||
|
||||
@ -156,6 +159,10 @@ proc chunkedReadLoop(stream: AsyncStreamReader) {.async.} =
|
||||
if rstream.state == AsyncStreamState.Running:
|
||||
rstream.state = AsyncStreamState.Error
|
||||
rstream.error = exc
|
||||
except AsyncStreamError as exc:
|
||||
if rstream.state == AsyncStreamState.Running:
|
||||
rstream.state = AsyncStreamState.Error
|
||||
rstream.error = exc
|
||||
|
||||
if rstream.state != AsyncStreamState.Running:
|
||||
# We need to notify consumer about error/close, but we do not care about
|
||||
@ -163,7 +170,7 @@ proc chunkedReadLoop(stream: AsyncStreamReader) {.async.} =
|
||||
rstream.buffer.forget()
|
||||
break
|
||||
|
||||
proc chunkedWriteLoop(stream: AsyncStreamWriter) {.async.} =
|
||||
proc chunkedWriteLoop(stream: AsyncStreamWriter) {.async: (raises: []).} =
|
||||
var wstream = ChunkedStreamWriter(stream)
|
||||
var buffer: array[16, byte]
|
||||
var error: ref AsyncStreamError
|
||||
@ -220,7 +227,11 @@ proc chunkedWriteLoop(stream: AsyncStreamWriter) {.async.} =
|
||||
if not(item.future.finished()):
|
||||
item.future.fail(error)
|
||||
while not(wstream.queue.empty()):
|
||||
let pitem = wstream.queue.popFirstNoWait()
|
||||
let pitem =
|
||||
try:
|
||||
wstream.queue.popFirstNoWait()
|
||||
except AsyncQueueEmptyError:
|
||||
raiseAssert "AsyncQueue should not be empty at this moment"
|
||||
if not(pitem.future.finished()):
|
||||
pitem.future.fail(error)
|
||||
break
|
||||
|
||||
@ -9,13 +9,19 @@
|
||||
|
||||
## This module implements Transport Layer Security (TLS) stream. This module
|
||||
## uses sources of BearSSL <https://www.bearssl.org> by Thomas Pornin.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import
|
||||
bearssl/[brssl, ec, errors, pem, rsa, ssl, x509],
|
||||
bearssl/certs/cacert
|
||||
import ../asyncloop, ../timer, ../asyncsync
|
||||
import asyncstream, ../transports/stream, ../transports/common
|
||||
import ".."/[asyncloop, asyncsync, config, timer]
|
||||
import asyncstream, ../transports/[stream, common]
|
||||
export asyncloop, asyncsync, timer, asyncstream
|
||||
|
||||
const
|
||||
TLSSessionCacheBufferSize* = chronosTLSSessionCacheBufferSize
|
||||
|
||||
type
|
||||
TLSStreamKind {.pure.} = enum
|
||||
Client, Server
|
||||
@ -59,7 +65,7 @@ type
|
||||
|
||||
PEMContext = ref object
|
||||
data: seq[byte]
|
||||
|
||||
|
||||
TrustAnchorStore* = ref object
|
||||
anchors: seq[X509TrustAnchor]
|
||||
|
||||
@ -71,7 +77,7 @@ type
|
||||
scontext: ptr SslServerContext
|
||||
stream*: TLSAsyncStream
|
||||
handshaked*: bool
|
||||
handshakeFut*: Future[void]
|
||||
handshakeFut*: Future[void].Raising([CancelledError, AsyncStreamError])
|
||||
|
||||
TLSStreamReader* = ref object of AsyncStreamReader
|
||||
case kind: TLSStreamKind
|
||||
@ -81,7 +87,7 @@ type
|
||||
scontext: ptr SslServerContext
|
||||
stream*: TLSAsyncStream
|
||||
handshaked*: bool
|
||||
handshakeFut*: Future[void]
|
||||
handshakeFut*: Future[void].Raising([CancelledError, AsyncStreamError])
|
||||
|
||||
TLSAsyncStream* = ref object of RootRef
|
||||
xwc*: X509NoanchorContext
|
||||
@ -91,18 +97,17 @@ type
|
||||
x509*: X509MinimalContext
|
||||
reader*: TLSStreamReader
|
||||
writer*: TLSStreamWriter
|
||||
mainLoop*: Future[void]
|
||||
mainLoop*: Future[void].Raising([])
|
||||
trustAnchors: TrustAnchorStore
|
||||
|
||||
SomeTLSStreamType* = TLSStreamReader|TLSStreamWriter|TLSAsyncStream
|
||||
SomeTrustAnchorType* = TrustAnchorStore | openArray[X509TrustAnchor]
|
||||
|
||||
TLSStreamError* = object of AsyncStreamError
|
||||
TLSStreamHandshakeError* = object of TLSStreamError
|
||||
TLSStreamInitError* = object of TLSStreamError
|
||||
TLSStreamReadError* = object of TLSStreamError
|
||||
par*: ref AsyncStreamError
|
||||
TLSStreamWriteError* = object of TLSStreamError
|
||||
par*: ref AsyncStreamError
|
||||
TLSStreamProtocolError* = object of TLSStreamError
|
||||
errCode*: int
|
||||
|
||||
@ -110,7 +115,7 @@ proc newTLSStreamWriteError(p: ref AsyncStreamError): ref TLSStreamWriteError {.
|
||||
noinline.} =
|
||||
var w = newException(TLSStreamWriteError, "Write stream failed")
|
||||
w.msg = w.msg & ", originated from [" & $p.name & "] " & p.msg
|
||||
w.par = p
|
||||
w.parent = p
|
||||
w
|
||||
|
||||
template newTLSStreamProtocolImpl[T](message: T): ref TLSStreamProtocolError =
|
||||
@ -136,38 +141,41 @@ template newTLSUnexpectedProtocolError(): ref TLSStreamProtocolError =
|
||||
proc newTLSStreamProtocolError[T](message: T): ref TLSStreamProtocolError =
|
||||
newTLSStreamProtocolImpl(message)
|
||||
|
||||
proc raiseTLSStreamProtocolError[T](message: T) {.noreturn, noinline.} =
|
||||
proc raiseTLSStreamProtocolError[T](message: T) {.
|
||||
noreturn, noinline, raises: [TLSStreamProtocolError].} =
|
||||
raise newTLSStreamProtocolImpl(message)
|
||||
|
||||
proc new*(T: typedesc[TrustAnchorStore], anchors: openArray[X509TrustAnchor]): TrustAnchorStore =
|
||||
proc new*(T: typedesc[TrustAnchorStore],
|
||||
anchors: openArray[X509TrustAnchor]): TrustAnchorStore =
|
||||
var res: seq[X509TrustAnchor]
|
||||
for anchor in anchors:
|
||||
res.add(anchor)
|
||||
doAssert(unsafeAddr(anchor) != unsafeAddr(res[^1]), "Anchors should be copied")
|
||||
return TrustAnchorStore(anchors: res)
|
||||
doAssert(unsafeAddr(anchor) != unsafeAddr(res[^1]),
|
||||
"Anchors should be copied")
|
||||
TrustAnchorStore(anchors: res)
|
||||
|
||||
proc tlsWriteRec(engine: ptr SslEngineContext,
|
||||
writer: TLSStreamWriter): Future[TLSResult] {.async.} =
|
||||
writer: TLSStreamWriter): Future[TLSResult] {.
|
||||
async: (raises: []).} =
|
||||
try:
|
||||
var length = 0'u
|
||||
var buf = sslEngineSendrecBuf(engine[], length)
|
||||
doAssert(length != 0 and not isNil(buf))
|
||||
await writer.wsource.write(buf, int(length))
|
||||
sslEngineSendrecAck(engine[], length)
|
||||
return TLSResult.Success
|
||||
TLSResult.Success
|
||||
except AsyncStreamError as exc:
|
||||
writer.state = AsyncStreamState.Error
|
||||
writer.error = exc
|
||||
return TLSResult.Error
|
||||
TLSResult.Error
|
||||
except CancelledError:
|
||||
if writer.state == AsyncStreamState.Running:
|
||||
writer.state = AsyncStreamState.Stopped
|
||||
return TLSResult.Stopped
|
||||
|
||||
return TLSResult.Error
|
||||
TLSResult.Stopped
|
||||
|
||||
proc tlsWriteApp(engine: ptr SslEngineContext,
|
||||
writer: TLSStreamWriter): Future[TLSResult] {.async.} =
|
||||
writer: TLSStreamWriter): Future[TLSResult] {.
|
||||
async: (raises: []).} =
|
||||
try:
|
||||
var item = await writer.queue.get()
|
||||
if item.size > 0:
|
||||
@ -179,7 +187,6 @@ proc tlsWriteApp(engine: ptr SslEngineContext,
|
||||
# (and discarded).
|
||||
writer.state = AsyncStreamState.Finished
|
||||
return TLSResult.WriteEof
|
||||
|
||||
let toWrite = min(int(length), item.size)
|
||||
copyOut(buf, item, toWrite)
|
||||
if int(length) >= item.size:
|
||||
@ -187,28 +194,29 @@ proc tlsWriteApp(engine: ptr SslEngineContext,
|
||||
sslEngineSendappAck(engine[], uint(item.size))
|
||||
sslEngineFlush(engine[], 0)
|
||||
item.future.complete()
|
||||
return TLSResult.Success
|
||||
else:
|
||||
# BearSSL is not ready to accept whole item, so we will send
|
||||
# only part of item and adjust offset.
|
||||
item.offset = item.offset + int(length)
|
||||
item.size = item.size - int(length)
|
||||
writer.queue.addFirstNoWait(item)
|
||||
try:
|
||||
writer.queue.addFirstNoWait(item)
|
||||
except AsyncQueueFullError:
|
||||
raiseAssert "AsyncQueue should not be full at this moment"
|
||||
sslEngineSendappAck(engine[], length)
|
||||
return TLSResult.Success
|
||||
TLSResult.Success
|
||||
else:
|
||||
sslEngineClose(engine[])
|
||||
item.future.complete()
|
||||
return TLSResult.Success
|
||||
TLSResult.Success
|
||||
except CancelledError:
|
||||
if writer.state == AsyncStreamState.Running:
|
||||
writer.state = AsyncStreamState.Stopped
|
||||
return TLSResult.Stopped
|
||||
|
||||
return TLSResult.Error
|
||||
TLSResult.Stopped
|
||||
|
||||
proc tlsReadRec(engine: ptr SslEngineContext,
|
||||
reader: TLSStreamReader): Future[TLSResult] {.async.} =
|
||||
reader: TLSStreamReader): Future[TLSResult] {.
|
||||
async: (raises: []).} =
|
||||
try:
|
||||
var length = 0'u
|
||||
var buf = sslEngineRecvrecBuf(engine[], length)
|
||||
@ -216,38 +224,35 @@ proc tlsReadRec(engine: ptr SslEngineContext,
|
||||
sslEngineRecvrecAck(engine[], uint(res))
|
||||
if res == 0:
|
||||
sslEngineClose(engine[])
|
||||
return TLSResult.ReadEof
|
||||
TLSResult.ReadEof
|
||||
else:
|
||||
return TLSResult.Success
|
||||
TLSResult.Success
|
||||
except AsyncStreamError as exc:
|
||||
reader.state = AsyncStreamState.Error
|
||||
reader.error = exc
|
||||
return TLSResult.Error
|
||||
TLSResult.Error
|
||||
except CancelledError:
|
||||
if reader.state == AsyncStreamState.Running:
|
||||
reader.state = AsyncStreamState.Stopped
|
||||
return TLSResult.Stopped
|
||||
|
||||
return TLSResult.Error
|
||||
TLSResult.Stopped
|
||||
|
||||
proc tlsReadApp(engine: ptr SslEngineContext,
|
||||
reader: TLSStreamReader): Future[TLSResult] {.async.} =
|
||||
reader: TLSStreamReader): Future[TLSResult] {.
|
||||
async: (raises: []).} =
|
||||
try:
|
||||
var length = 0'u
|
||||
var buf = sslEngineRecvappBuf(engine[], length)
|
||||
await upload(addr reader.buffer, buf, int(length))
|
||||
await upload(reader.buffer, buf, int(length))
|
||||
sslEngineRecvappAck(engine[], length)
|
||||
return TLSResult.Success
|
||||
TLSResult.Success
|
||||
except CancelledError:
|
||||
if reader.state == AsyncStreamState.Running:
|
||||
reader.state = AsyncStreamState.Stopped
|
||||
return TLSResult.Stopped
|
||||
|
||||
return TLSResult.Error
|
||||
TLSResult.Stopped
|
||||
|
||||
template readAndReset(fut: untyped) =
|
||||
if fut.finished():
|
||||
let res = fut.read()
|
||||
let res = fut.value()
|
||||
case res
|
||||
of TLSResult.Success, TLSResult.WriteEof, TLSResult.Stopped:
|
||||
fut = nil
|
||||
@ -263,22 +268,6 @@ template readAndReset(fut: untyped) =
|
||||
loopState = AsyncStreamState.Finished
|
||||
break
|
||||
|
||||
proc cancelAndWait*(a, b, c, d: Future[TLSResult]): Future[void] =
|
||||
var waiting: seq[Future[TLSResult]]
|
||||
if not(isNil(a)) and not(a.finished()):
|
||||
a.cancel()
|
||||
waiting.add(a)
|
||||
if not(isNil(b)) and not(b.finished()):
|
||||
b.cancel()
|
||||
waiting.add(b)
|
||||
if not(isNil(c)) and not(c.finished()):
|
||||
c.cancel()
|
||||
waiting.add(c)
|
||||
if not(isNil(d)) and not(d.finished()):
|
||||
d.cancel()
|
||||
waiting.add(d)
|
||||
allFutures(waiting)
|
||||
|
||||
proc dumpState*(state: cuint): string =
|
||||
var res = ""
|
||||
if (state and SSL_CLOSED) == SSL_CLOSED:
|
||||
@ -298,10 +287,10 @@ proc dumpState*(state: cuint): string =
|
||||
res.add("SSL_RECVAPP")
|
||||
"{" & res & "}"
|
||||
|
||||
proc tlsLoop*(stream: TLSAsyncStream) {.async.} =
|
||||
proc tlsLoop*(stream: TLSAsyncStream) {.async: (raises: []).} =
|
||||
var
|
||||
sendRecFut, sendAppFut: Future[TLSResult]
|
||||
recvRecFut, recvAppFut: Future[TLSResult]
|
||||
sendRecFut, sendAppFut: Future[TLSResult].Raising([])
|
||||
recvRecFut, recvAppFut: Future[TLSResult].Raising([])
|
||||
|
||||
let engine =
|
||||
case stream.reader.kind
|
||||
@ -313,7 +302,7 @@ proc tlsLoop*(stream: TLSAsyncStream) {.async.} =
|
||||
var loopState = AsyncStreamState.Running
|
||||
|
||||
while true:
|
||||
var waiting: seq[Future[TLSResult]]
|
||||
var waiting: seq[Future[TLSResult].Raising([])]
|
||||
var state = sslEngineCurrentState(engine[])
|
||||
|
||||
if (state and SSL_CLOSED) == SSL_CLOSED:
|
||||
@ -364,6 +353,8 @@ proc tlsLoop*(stream: TLSAsyncStream) {.async.} =
|
||||
if len(waiting) > 0:
|
||||
try:
|
||||
discard await one(waiting)
|
||||
except ValueError:
|
||||
raiseAssert "array should not be empty at this moment"
|
||||
except CancelledError:
|
||||
if loopState == AsyncStreamState.Running:
|
||||
loopState = AsyncStreamState.Stopped
|
||||
@ -371,8 +362,18 @@ proc tlsLoop*(stream: TLSAsyncStream) {.async.} =
|
||||
if loopState != AsyncStreamState.Running:
|
||||
break
|
||||
|
||||
# Cancelling and waiting all the pending operations
|
||||
await cancelAndWait(sendRecFut, sendAppFut, recvRecFut, recvAppFut)
|
||||
# Cancelling and waiting and all the pending operations
|
||||
var pending: seq[FutureBase]
|
||||
if not(isNil(sendRecFut)) and not(sendRecFut.finished()):
|
||||
pending.add(sendRecFut.cancelAndWait())
|
||||
if not(isNil(sendAppFut)) and not(sendAppFut.finished()):
|
||||
pending.add(sendAppFut.cancelAndWait())
|
||||
if not(isNil(recvRecFut)) and not(recvRecFut.finished()):
|
||||
pending.add(recvRecFut.cancelAndWait())
|
||||
if not(isNil(recvAppFut)) and not(recvAppFut.finished()):
|
||||
pending.add(recvAppFut.cancelAndWait())
|
||||
await noCancel(allFutures(pending))
|
||||
|
||||
# Calculating error
|
||||
let error =
|
||||
case loopState
|
||||
@ -406,7 +407,11 @@ proc tlsLoop*(stream: TLSAsyncStream) {.async.} =
|
||||
if not(isNil(error)):
|
||||
# Completing all pending writes
|
||||
while(not(stream.writer.queue.empty())):
|
||||
let item = stream.writer.queue.popFirstNoWait()
|
||||
let item =
|
||||
try:
|
||||
stream.writer.queue.popFirstNoWait()
|
||||
except AsyncQueueEmptyError:
|
||||
raiseAssert "AsyncQueue should not be empty at this moment"
|
||||
if not(item.future.finished()):
|
||||
item.future.fail(error)
|
||||
# Completing handshake
|
||||
@ -426,18 +431,18 @@ proc tlsLoop*(stream: TLSAsyncStream) {.async.} =
|
||||
# Completing readers
|
||||
stream.reader.buffer.forget()
|
||||
|
||||
proc tlsWriteLoop(stream: AsyncStreamWriter) {.async.} =
|
||||
proc tlsWriteLoop(stream: AsyncStreamWriter) {.async: (raises: []).} =
|
||||
var wstream = TLSStreamWriter(stream)
|
||||
wstream.state = AsyncStreamState.Running
|
||||
await stepsAsync(1)
|
||||
await noCancel(sleepAsync(0.milliseconds))
|
||||
if isNil(wstream.stream.mainLoop):
|
||||
wstream.stream.mainLoop = tlsLoop(wstream.stream)
|
||||
await wstream.stream.mainLoop
|
||||
|
||||
proc tlsReadLoop(stream: AsyncStreamReader) {.async.} =
|
||||
proc tlsReadLoop(stream: AsyncStreamReader) {.async: (raises: []).} =
|
||||
var rstream = TLSStreamReader(stream)
|
||||
rstream.state = AsyncStreamState.Running
|
||||
await stepsAsync(1)
|
||||
await noCancel(sleepAsync(0.milliseconds))
|
||||
if isNil(rstream.stream.mainLoop):
|
||||
rstream.stream.mainLoop = tlsLoop(rstream.stream)
|
||||
await rstream.stream.mainLoop
|
||||
@ -453,15 +458,16 @@ proc getSignerAlgo(xc: X509Certificate): int =
|
||||
else:
|
||||
int(x509DecoderGetSignerKeyType(dc))
|
||||
|
||||
proc newTLSClientAsyncStream*(rsource: AsyncStreamReader,
|
||||
wsource: AsyncStreamWriter,
|
||||
serverName: string,
|
||||
bufferSize = SSL_BUFSIZE_BIDI,
|
||||
minVersion = TLSVersion.TLS12,
|
||||
maxVersion = TLSVersion.TLS12,
|
||||
flags: set[TLSFlags] = {},
|
||||
trustAnchors: TrustAnchorStore | openArray[X509TrustAnchor] = MozillaTrustAnchors
|
||||
): TLSAsyncStream =
|
||||
proc newTLSClientAsyncStream*(
|
||||
rsource: AsyncStreamReader,
|
||||
wsource: AsyncStreamWriter,
|
||||
serverName: string,
|
||||
bufferSize = SSL_BUFSIZE_BIDI,
|
||||
minVersion = TLSVersion.TLS12,
|
||||
maxVersion = TLSVersion.TLS12,
|
||||
flags: set[TLSFlags] = {},
|
||||
trustAnchors: SomeTrustAnchorType = MozillaTrustAnchors
|
||||
): TLSAsyncStream {.raises: [TLSStreamInitError].} =
|
||||
## Create new TLS asynchronous stream for outbound (client) connections
|
||||
## using reading stream ``rsource`` and writing stream ``wsource``.
|
||||
##
|
||||
@ -478,13 +484,14 @@ proc newTLSClientAsyncStream*(rsource: AsyncStreamReader,
|
||||
## ``minVersion`` of bigger then ``maxVersion`` you will get an error.
|
||||
##
|
||||
## ``flags`` - custom TLS connection flags.
|
||||
##
|
||||
##
|
||||
## ``trustAnchors`` - use this if you want to use certificate trust
|
||||
## anchors other than the default Mozilla trust anchors. If you pass
|
||||
## a ``TrustAnchorStore`` you should reuse the same instance for
|
||||
## every call to avoid making a copy of the trust anchors per call.
|
||||
when trustAnchors is TrustAnchorStore:
|
||||
doAssert(len(trustAnchors.anchors) > 0, "Empty trust anchor list is invalid")
|
||||
doAssert(len(trustAnchors.anchors) > 0,
|
||||
"Empty trust anchor list is invalid")
|
||||
else:
|
||||
doAssert(len(trustAnchors) > 0, "Empty trust anchor list is invalid")
|
||||
var res = TLSAsyncStream()
|
||||
@ -503,8 +510,10 @@ proc newTLSClientAsyncStream*(rsource: AsyncStreamReader,
|
||||
|
||||
if TLSFlags.NoVerifyHost in flags:
|
||||
sslClientInitFull(res.ccontext, addr res.x509, nil, 0)
|
||||
x509NoanchorInit(res.xwc, addr res.x509.vtable)
|
||||
sslEngineSetX509(res.ccontext.eng, addr res.xwc.vtable)
|
||||
x509NoanchorInit(res.xwc,
|
||||
X509ClassPointerConst(addr res.x509.vtable))
|
||||
sslEngineSetX509(res.ccontext.eng,
|
||||
X509ClassPointerConst(addr res.xwc.vtable))
|
||||
else:
|
||||
when trustAnchors is TrustAnchorStore:
|
||||
res.trustAnchors = trustAnchors
|
||||
@ -524,7 +533,7 @@ proc newTLSClientAsyncStream*(rsource: AsyncStreamReader,
|
||||
uint16(maxVersion))
|
||||
|
||||
if TLSFlags.NoVerifyServerName in flags:
|
||||
let err = sslClientReset(res.ccontext, "", 0)
|
||||
let err = sslClientReset(res.ccontext, nil, 0)
|
||||
if err == 0:
|
||||
raise newException(TLSStreamInitError, "Could not initialize TLS layer")
|
||||
else:
|
||||
@ -550,7 +559,8 @@ proc newTLSServerAsyncStream*(rsource: AsyncStreamReader,
|
||||
minVersion = TLSVersion.TLS11,
|
||||
maxVersion = TLSVersion.TLS12,
|
||||
cache: TLSSessionCache = nil,
|
||||
flags: set[TLSFlags] = {}): TLSAsyncStream =
|
||||
flags: set[TLSFlags] = {}): TLSAsyncStream {.
|
||||
raises: [TLSStreamInitError, TLSStreamProtocolError].} =
|
||||
## Create new TLS asynchronous stream for inbound (server) connections
|
||||
## using reading stream ``rsource`` and writing stream ``wsource``.
|
||||
##
|
||||
@ -603,7 +613,8 @@ proc newTLSServerAsyncStream*(rsource: AsyncStreamReader,
|
||||
uint16(maxVersion))
|
||||
|
||||
if not isNil(cache):
|
||||
sslServerSetCache(res.scontext, addr cache.context.vtable)
|
||||
sslServerSetCache(
|
||||
res.scontext, SslSessionCacheClassPointerConst(addr cache.context.vtable))
|
||||
|
||||
if TLSFlags.EnforceServerPref in flags:
|
||||
sslEngineAddFlags(res.scontext.eng, OPT_ENFORCE_SERVER_PREFERENCES)
|
||||
@ -618,10 +629,8 @@ proc newTLSServerAsyncStream*(rsource: AsyncStreamReader,
|
||||
if err == 0:
|
||||
raise newException(TLSStreamInitError, "Could not initialize TLS layer")
|
||||
|
||||
init(AsyncStreamWriter(res.writer), wsource, tlsWriteLoop,
|
||||
bufferSize)
|
||||
init(AsyncStreamReader(res.reader), rsource, tlsReadLoop,
|
||||
bufferSize)
|
||||
init(AsyncStreamWriter(res.writer), wsource, tlsWriteLoop, bufferSize)
|
||||
init(AsyncStreamReader(res.reader), rsource, tlsReadLoop, bufferSize)
|
||||
res
|
||||
|
||||
proc copyKey(src: RsaPrivateKey): TLSPrivateKey =
|
||||
@ -662,7 +671,8 @@ proc copyKey(src: EcPrivateKey): TLSPrivateKey =
|
||||
res.eckey.curve = src.curve
|
||||
res
|
||||
|
||||
proc init*(tt: typedesc[TLSPrivateKey], data: openArray[byte]): TLSPrivateKey =
|
||||
proc init*(tt: typedesc[TLSPrivateKey], data: openArray[byte]): TLSPrivateKey {.
|
||||
raises: [TLSStreamProtocolError].} =
|
||||
## Initialize TLS private key from array of bytes ``data``.
|
||||
##
|
||||
## This procedure initializes private key using raw, DER-encoded format,
|
||||
@ -685,7 +695,8 @@ proc init*(tt: typedesc[TLSPrivateKey], data: openArray[byte]): TLSPrivateKey =
|
||||
raiseTLSStreamProtocolError("Unknown key type (" & $keyType & ")")
|
||||
res
|
||||
|
||||
proc pemDecode*(data: openArray[char]): seq[PEMElement] =
|
||||
proc pemDecode*(data: openArray[char]): seq[PEMElement] {.
|
||||
raises: [TLSStreamProtocolError].} =
|
||||
## Decode PEM encoded string and get array of binary blobs.
|
||||
if len(data) == 0:
|
||||
raiseTLSStreamProtocolError("Empty PEM message")
|
||||
@ -726,7 +737,8 @@ proc pemDecode*(data: openArray[char]): seq[PEMElement] =
|
||||
raiseTLSStreamProtocolError("Invalid PEM encoding")
|
||||
res
|
||||
|
||||
proc init*(tt: typedesc[TLSPrivateKey], data: openArray[char]): TLSPrivateKey =
|
||||
proc init*(tt: typedesc[TLSPrivateKey], data: openArray[char]): TLSPrivateKey {.
|
||||
raises: [TLSStreamProtocolError].} =
|
||||
## Initialize TLS private key from string ``data``.
|
||||
##
|
||||
## This procedure initializes private key using unencrypted PKCS#8 PEM
|
||||
@ -744,7 +756,8 @@ proc init*(tt: typedesc[TLSPrivateKey], data: openArray[char]): TLSPrivateKey =
|
||||
res
|
||||
|
||||
proc init*(tt: typedesc[TLSCertificate],
|
||||
data: openArray[char]): TLSCertificate =
|
||||
data: openArray[char]): TLSCertificate {.
|
||||
raises: [TLSStreamProtocolError].} =
|
||||
## Initialize TLS certificates from string ``data``.
|
||||
##
|
||||
## This procedure initializes array of certificates from PEM encoded string.
|
||||
@ -770,18 +783,21 @@ proc init*(tt: typedesc[TLSCertificate],
|
||||
raiseTLSStreamProtocolError("Could not find any certificates")
|
||||
res
|
||||
|
||||
proc init*(tt: typedesc[TLSSessionCache], size: int = 4096): TLSSessionCache =
|
||||
proc init*(tt: typedesc[TLSSessionCache],
|
||||
size: int = TLSSessionCacheBufferSize): TLSSessionCache =
|
||||
## Create new TLS session cache with size ``size``.
|
||||
##
|
||||
## One cached item is near 100 bytes size.
|
||||
var rsize = min(size, 4096)
|
||||
let rsize = min(size, 4096)
|
||||
var res = TLSSessionCache(storage: newSeq[byte](rsize))
|
||||
sslSessionCacheLruInit(addr res.context, addr res.storage[0], rsize)
|
||||
res
|
||||
|
||||
proc handshake*(rws: SomeTLSStreamType): Future[void] =
|
||||
proc handshake*(rws: SomeTLSStreamType): Future[void] {.
|
||||
async: (raw: true, raises: [CancelledError, AsyncStreamError]).} =
|
||||
## Wait until initial TLS handshake will be successfully performed.
|
||||
var retFuture = newFuture[void]("tlsstream.handshake")
|
||||
let retFuture = Future[void].Raising([CancelledError, AsyncStreamError])
|
||||
.init("tlsstream.handshake")
|
||||
when rws is TLSStreamReader:
|
||||
if rws.handshaked:
|
||||
retFuture.complete()
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
# MIT license (LICENSE-MIT)
|
||||
|
||||
## This module implements some core async thread synchronization primitives.
|
||||
import stew/results
|
||||
import results
|
||||
import "."/[timer, asyncloop]
|
||||
|
||||
export results
|
||||
@ -272,7 +272,8 @@ proc waitSync*(signal: ThreadSignalPtr,
|
||||
else:
|
||||
return ok(true)
|
||||
|
||||
proc fire*(signal: ThreadSignalPtr): Future[void] =
|
||||
proc fire*(signal: ThreadSignalPtr): Future[void] {.
|
||||
async: (raises: [AsyncError, CancelledError], raw: true).} =
|
||||
## Set state of ``signal`` to signaled in asynchronous way.
|
||||
var retFuture = newFuture[void]("asyncthreadsignal.fire")
|
||||
when defined(windows):
|
||||
@ -356,14 +357,17 @@ proc fire*(signal: ThreadSignalPtr): Future[void] =
|
||||
retFuture
|
||||
|
||||
when defined(windows):
|
||||
proc wait*(signal: ThreadSignalPtr) {.async.} =
|
||||
proc wait*(signal: ThreadSignalPtr) {.
|
||||
async: (raises: [AsyncError, CancelledError]).} =
|
||||
let handle = signal[].event
|
||||
let res = await waitForSingleObject(handle, InfiniteDuration)
|
||||
# There should be no other response, because we use `InfiniteDuration`.
|
||||
doAssert(res == WaitableResult.Ok)
|
||||
else:
|
||||
proc wait*(signal: ThreadSignalPtr): Future[void] =
|
||||
var retFuture = newFuture[void]("asyncthreadsignal.wait")
|
||||
proc wait*(signal: ThreadSignalPtr): Future[void] {.
|
||||
async: (raises: [AsyncError, CancelledError], raw: true).} =
|
||||
let retFuture = Future[void].Raising([AsyncError, CancelledError]).init(
|
||||
"asyncthreadsignal.wait")
|
||||
var data = 1'u64
|
||||
let eventFd =
|
||||
when defined(linux):
|
||||
|
||||
@ -370,53 +370,42 @@ template add(a: var string, b: Base10Buf[uint64]) =
|
||||
for index in 0 ..< b.len:
|
||||
a.add(char(b.data[index]))
|
||||
|
||||
func `$`*(a: Duration): string {.inline.} =
|
||||
## Returns string representation of Duration ``a`` as nanoseconds value.
|
||||
var res = ""
|
||||
var v = a.value
|
||||
func toString*(a: timer.Duration, parts = int.high): string =
|
||||
## Returns a pretty string representation of Duration ``a`` - the
|
||||
## number of parts returned can be limited thus truncating the output to
|
||||
## an approximation that grows more precise as the duration becomes smaller
|
||||
var
|
||||
res = newStringOfCap(32)
|
||||
v = a.nanoseconds()
|
||||
parts = parts
|
||||
|
||||
template f(n: string, T: Duration) =
|
||||
if parts <= 0:
|
||||
return res
|
||||
|
||||
if v >= T.nanoseconds():
|
||||
res.add(Base10.toBytes(uint64(v div T.nanoseconds())))
|
||||
res.add(n)
|
||||
v = v mod T.nanoseconds()
|
||||
dec parts
|
||||
if v == 0:
|
||||
return res
|
||||
|
||||
f("w", Week)
|
||||
f("d", Day)
|
||||
f("h", Hour)
|
||||
f("m", Minute)
|
||||
f("s", Second)
|
||||
f("ms", Millisecond)
|
||||
f("us", Microsecond)
|
||||
f("ns", Nanosecond)
|
||||
|
||||
if v >= Week.value:
|
||||
res.add(Base10.toBytes(uint64(v div Week.value)))
|
||||
res.add('w')
|
||||
v = v mod Week.value
|
||||
if v == 0: return res
|
||||
if v >= Day.value:
|
||||
res.add(Base10.toBytes(uint64(v div Day.value)))
|
||||
res.add('d')
|
||||
v = v mod Day.value
|
||||
if v == 0: return res
|
||||
if v >= Hour.value:
|
||||
res.add(Base10.toBytes(uint64(v div Hour.value)))
|
||||
res.add('h')
|
||||
v = v mod Hour.value
|
||||
if v == 0: return res
|
||||
if v >= Minute.value:
|
||||
res.add(Base10.toBytes(uint64(v div Minute.value)))
|
||||
res.add('m')
|
||||
v = v mod Minute.value
|
||||
if v == 0: return res
|
||||
if v >= Second.value:
|
||||
res.add(Base10.toBytes(uint64(v div Second.value)))
|
||||
res.add('s')
|
||||
v = v mod Second.value
|
||||
if v == 0: return res
|
||||
if v >= Millisecond.value:
|
||||
res.add(Base10.toBytes(uint64(v div Millisecond.value)))
|
||||
res.add('m')
|
||||
res.add('s')
|
||||
v = v mod Millisecond.value
|
||||
if v == 0: return res
|
||||
if v >= Microsecond.value:
|
||||
res.add(Base10.toBytes(uint64(v div Microsecond.value)))
|
||||
res.add('u')
|
||||
res.add('s')
|
||||
v = v mod Microsecond.value
|
||||
if v == 0: return res
|
||||
res.add(Base10.toBytes(uint64(v div Nanosecond.value)))
|
||||
res.add('n')
|
||||
res.add('s')
|
||||
res
|
||||
|
||||
func `$`*(a: Duration): string {.inline.} =
|
||||
## Returns string representation of Duration ``a``.
|
||||
a.toString()
|
||||
|
||||
func `$`*(a: Moment): string {.inline.} =
|
||||
## Returns string representation of Moment ``a`` as nanoseconds value.
|
||||
var res = ""
|
||||
|
||||
@ -10,26 +10,30 @@
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[strutils]
|
||||
import results
|
||||
import stew/[base10, byteutils]
|
||||
import ".."/[asyncloop, osdefs, oserrno]
|
||||
import ".."/[config, asyncloop, osdefs, oserrno, handles]
|
||||
|
||||
from std/net import Domain, `==`, IpAddress, IpAddressFamily, parseIpAddress,
|
||||
SockType, Protocol, Port, `$`
|
||||
from std/nativesockets import toInt, `$`
|
||||
|
||||
export Domain, `==`, IpAddress, IpAddressFamily, parseIpAddress, SockType,
|
||||
Protocol, Port, toInt, `$`
|
||||
Protocol, Port, toInt, `$`, results
|
||||
|
||||
const
|
||||
DefaultStreamBufferSize* = 4096 ## Default buffer size for stream
|
||||
## transports
|
||||
DefaultDatagramBufferSize* = 65536 ## Default buffer size for datagram
|
||||
## transports
|
||||
DefaultStreamBufferSize* = chronosTransportDefaultBufferSize
|
||||
## Default buffer size for stream transports
|
||||
DefaultDatagramBufferSize* = 65536
|
||||
## Default buffer size for datagram transports
|
||||
type
|
||||
ServerFlags* = enum
|
||||
## Server's flags
|
||||
ReuseAddr, ReusePort, TcpNoDelay, NoAutoRead, GCUserData, FirstPipe,
|
||||
NoPipeFlash, Broadcast
|
||||
NoPipeFlash, Broadcast, V4Mapped
|
||||
|
||||
DualStackType* {.pure.} = enum
|
||||
Auto, Enabled, Disabled, Default
|
||||
|
||||
AddressFamily* {.pure.} = enum
|
||||
None, IPv4, IPv6, Unix
|
||||
@ -70,12 +74,13 @@ when defined(windows) or defined(nimdoc):
|
||||
udata*: pointer # User-defined pointer
|
||||
flags*: set[ServerFlags] # Flags
|
||||
bufferSize*: int # Size of internal transports' buffer
|
||||
loopFuture*: Future[void] # Server's main Future
|
||||
loopFuture*: Future[void].Raising([]) # Server's main Future
|
||||
domain*: Domain # Current server domain (IPv4 or IPv6)
|
||||
apending*: bool
|
||||
asock*: AsyncFD # Current AcceptEx() socket
|
||||
errorCode*: OSErrorCode # Current error code
|
||||
abuffer*: array[128, byte] # Windows AcceptEx() buffer
|
||||
dualstack*: DualStackType # IPv4/IPv6 dualstack parameters
|
||||
when defined(windows):
|
||||
aovl*: CustomOverlapped # AcceptEx OVERLAPPED structure
|
||||
else:
|
||||
@ -88,8 +93,9 @@ else:
|
||||
udata*: pointer # User-defined pointer
|
||||
flags*: set[ServerFlags] # Flags
|
||||
bufferSize*: int # Size of internal transports' buffer
|
||||
loopFuture*: Future[void] # Server's main Future
|
||||
loopFuture*: Future[void].Raising([]) # Server's main Future
|
||||
errorCode*: OSErrorCode # Current error code
|
||||
dualstack*: DualStackType # IPv4/IPv6 dualstack parameters
|
||||
|
||||
type
|
||||
TransportError* = object of AsyncError
|
||||
@ -108,6 +114,8 @@ type
|
||||
## Transport's capability not supported exception
|
||||
TransportUseClosedError* = object of TransportError
|
||||
## Usage after transport close exception
|
||||
TransportUseEofError* = object of TransportError
|
||||
## Usage after transport half-close exception
|
||||
TransportTooManyError* = object of TransportError
|
||||
## Too many open file descriptors exception
|
||||
TransportAbortedError* = object of TransportError
|
||||
@ -193,8 +201,17 @@ proc `$`*(address: TransportAddress): string =
|
||||
of AddressFamily.None:
|
||||
"None"
|
||||
|
||||
proc toIpAddress*(address: TransportAddress): IpAddress =
|
||||
case address.family
|
||||
of AddressFamily.IPv4:
|
||||
IpAddress(family: IpAddressFamily.IPv4, address_v4: address.address_v4)
|
||||
of AddressFamily.IPv6:
|
||||
IpAddress(family: IpAddressFamily.IPv6, address_v6: address.address_v6)
|
||||
else:
|
||||
raiseAssert "IpAddress do not support address family " & $address.family
|
||||
|
||||
proc toHex*(address: TransportAddress): string =
|
||||
## Returns hexadecimal representation of ``address`.
|
||||
## Returns hexadecimal representation of ``address``.
|
||||
case address.family
|
||||
of AddressFamily.IPv4:
|
||||
"0x" & address.address_v4.toHex()
|
||||
@ -298,6 +315,9 @@ proc getAddrInfo(address: string, port: Port, domain: Domain,
|
||||
raises: [TransportAddressError].} =
|
||||
## We have this one copy of ``getAddrInfo()`` because of AI_V4MAPPED in
|
||||
## ``net.nim:getAddrInfo()``, which is not cross-platform.
|
||||
##
|
||||
## Warning: `ptr AddrInfo` returned by `getAddrInfo()` needs to be freed by
|
||||
## calling `freeAddrInfo()`.
|
||||
var hints: AddrInfo
|
||||
var res: ptr AddrInfo = nil
|
||||
hints.ai_family = toInt(domain)
|
||||
@ -420,6 +440,7 @@ proc resolveTAddress*(address: string, port: Port,
|
||||
if ta notin res:
|
||||
res.add(ta)
|
||||
it = it.ai_next
|
||||
freeAddrInfo(aiList)
|
||||
res
|
||||
|
||||
proc resolveTAddress*(address: string, domain: Domain): seq[TransportAddress] {.
|
||||
@ -558,11 +579,11 @@ template checkClosed*(t: untyped, future: untyped) =
|
||||
|
||||
template checkWriteEof*(t: untyped, future: untyped) =
|
||||
if (WriteEof in (t).state):
|
||||
future.fail(newException(TransportError,
|
||||
future.fail(newException(TransportUseEofError,
|
||||
"Transport connection is already dropped!"))
|
||||
return future
|
||||
|
||||
template getError*(t: untyped): ref CatchableError =
|
||||
template getError*(t: untyped): ref TransportError =
|
||||
var err = (t).error
|
||||
(t).error = nil
|
||||
err
|
||||
@ -585,22 +606,6 @@ proc raiseTransportOsError*(err: OSErrorCode) {.
|
||||
## Raises transport specific OS error.
|
||||
raise getTransportOsError(err)
|
||||
|
||||
type
|
||||
SeqHeader = object
|
||||
length, reserved: int
|
||||
|
||||
proc isLiteral*(s: string): bool {.inline.} =
|
||||
when defined(gcOrc) or defined(gcArc):
|
||||
false
|
||||
else:
|
||||
(cast[ptr SeqHeader](s).reserved and (1 shl (sizeof(int) * 8 - 2))) != 0
|
||||
|
||||
proc isLiteral*[T](s: seq[T]): bool {.inline.} =
|
||||
when defined(gcOrc) or defined(gcArc):
|
||||
false
|
||||
else:
|
||||
(cast[ptr SeqHeader](s).reserved and (1 shl (sizeof(int) * 8 - 2))) != 0
|
||||
|
||||
template getTransportTooManyError*(
|
||||
code = OSErrorCode(0)
|
||||
): ref TransportTooManyError =
|
||||
@ -716,3 +721,97 @@ proc raiseTransportError*(ecode: OSErrorCode) {.
|
||||
raise getTransportTooManyError(ecode)
|
||||
else:
|
||||
raise getTransportOsError(ecode)
|
||||
|
||||
proc isAvailable*(family: AddressFamily): bool =
|
||||
case family
|
||||
of AddressFamily.None:
|
||||
raiseAssert "Invalid address family"
|
||||
of AddressFamily.IPv4:
|
||||
isAvailable(Domain.AF_INET)
|
||||
of AddressFamily.IPv6:
|
||||
isAvailable(Domain.AF_INET6)
|
||||
of AddressFamily.Unix:
|
||||
isAvailable(Domain.AF_UNIX)
|
||||
|
||||
proc getDomain*(socket: AsyncFD): Result[AddressFamily, OSErrorCode] =
|
||||
## Returns address family which is used to create socket ``socket``.
|
||||
##
|
||||
## Note: `chronos` supports only `AF_INET`, `AF_INET6` and `AF_UNIX` sockets.
|
||||
## For all other types of sockets this procedure returns
|
||||
## `EAFNOSUPPORT/WSAEAFNOSUPPORT` error.
|
||||
when defined(windows):
|
||||
let protocolInfo = ? getSockOpt2(socket, cint(osdefs.SOL_SOCKET),
|
||||
cint(osdefs.SO_PROTOCOL_INFOW),
|
||||
WSAPROTOCOL_INFO)
|
||||
if protocolInfo.iAddressFamily == toInt(Domain.AF_INET):
|
||||
ok(AddressFamily.IPv4)
|
||||
elif protocolInfo.iAddressFamily == toInt(Domain.AF_INET6):
|
||||
ok(AddressFamily.IPv6)
|
||||
else:
|
||||
err(WSAEAFNOSUPPORT)
|
||||
else:
|
||||
var
|
||||
saddr = Sockaddr_storage()
|
||||
slen = SockLen(sizeof(saddr))
|
||||
if getsockname(SocketHandle(socket), cast[ptr SockAddr](addr saddr),
|
||||
addr slen) != 0:
|
||||
return err(osLastError())
|
||||
if int(saddr.ss_family) == toInt(Domain.AF_INET):
|
||||
ok(AddressFamily.IPv4)
|
||||
elif int(saddr.ss_family) == toInt(Domain.AF_INET6):
|
||||
ok(AddressFamily.IPv6)
|
||||
elif int(saddr.ss_family) == toInt(Domain.AF_UNIX):
|
||||
ok(AddressFamily.Unix)
|
||||
else:
|
||||
err(EAFNOSUPPORT)
|
||||
|
||||
proc setDualstack*(socket: AsyncFD, family: AddressFamily,
|
||||
flag: DualStackType): Result[void, OSErrorCode] =
|
||||
if family == AddressFamily.IPv6:
|
||||
case flag
|
||||
of DualStackType.Auto:
|
||||
# In case of `Auto` we going to ignore all the errors.
|
||||
discard setDualstack(socket, true)
|
||||
ok()
|
||||
of DualStackType.Enabled:
|
||||
? setDualstack(socket, true)
|
||||
ok()
|
||||
of DualStackType.Disabled:
|
||||
? setDualstack(socket, false)
|
||||
ok()
|
||||
of DualStackType.Default:
|
||||
ok()
|
||||
else:
|
||||
ok()
|
||||
|
||||
proc setDualstack*(socket: AsyncFD,
|
||||
flag: DualStackType): Result[void, OSErrorCode] =
|
||||
let family =
|
||||
case flag
|
||||
of DualStackType.Auto:
|
||||
getDomain(socket).get(AddressFamily.IPv6)
|
||||
else:
|
||||
? getDomain(socket)
|
||||
setDualstack(socket, family, flag)
|
||||
|
||||
proc getAutoAddress*(port: Port): TransportAddress =
|
||||
var res =
|
||||
if isAvailable(AddressFamily.IPv6):
|
||||
AnyAddress6
|
||||
else:
|
||||
AnyAddress
|
||||
res.port = port
|
||||
res
|
||||
|
||||
proc getAutoAddresses*(
|
||||
localPort: Port,
|
||||
remotePort: Port
|
||||
): tuple[local: TransportAddress, remote: TransportAddress] =
|
||||
var (local, remote) =
|
||||
if isAvailable(AddressFamily.IPv6):
|
||||
(AnyAddress6, AnyAddress6)
|
||||
else:
|
||||
(AnyAddress, AnyAddress)
|
||||
local.port = localPort
|
||||
remote.port = remotePort
|
||||
(local, remote)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -52,7 +52,7 @@ proc init*(t: typedesc[IpMask], family: AddressFamily, prefix: int): IpMask =
|
||||
IpMask(family: AddressFamily.IPv4, mask4: 0'u32)
|
||||
elif prefix < 32:
|
||||
let mask = 0xFFFF_FFFF'u32 shl (32 - prefix)
|
||||
IpMask(family: AddressFamily.IPv4, mask4: mask.toBE())
|
||||
IpMask(family: AddressFamily.IPv4, mask4: mask)
|
||||
else:
|
||||
IpMask(family: AddressFamily.IPv4, mask4: 0xFFFF_FFFF'u32)
|
||||
of AddressFamily.IPv6:
|
||||
@ -65,13 +65,13 @@ proc init*(t: typedesc[IpMask], family: AddressFamily, prefix: int): IpMask =
|
||||
if prefix > 64:
|
||||
let mask = 0xFFFF_FFFF_FFFF_FFFF'u64 shl (128 - prefix)
|
||||
IpMask(family: AddressFamily.IPv6,
|
||||
mask6: [0xFFFF_FFFF_FFFF_FFFF'u64, mask.toBE()])
|
||||
mask6: [0xFFFF_FFFF_FFFF_FFFF'u64, mask])
|
||||
elif prefix == 64:
|
||||
IpMask(family: AddressFamily.IPv6,
|
||||
mask6: [0xFFFF_FFFF_FFFF_FFFF'u64, 0'u64])
|
||||
else:
|
||||
let mask = 0xFFFF_FFFF_FFFF_FFFF'u64 shl (64 - prefix)
|
||||
IpMask(family: AddressFamily.IPv6, mask6: [mask.toBE(), 0'u64])
|
||||
IpMask(family: AddressFamily.IPv6, mask6: [mask, 0'u64])
|
||||
else:
|
||||
IpMask(family: family)
|
||||
|
||||
@ -80,11 +80,12 @@ proc init*(t: typedesc[IpMask], netmask: TransportAddress): IpMask =
|
||||
case netmask.family
|
||||
of AddressFamily.IPv4:
|
||||
IpMask(family: AddressFamily.IPv4,
|
||||
mask4: uint32.fromBytes(netmask.address_v4))
|
||||
mask4: uint32.fromBytesBE(netmask.address_v4))
|
||||
of AddressFamily.IPv6:
|
||||
IpMask(family: AddressFamily.IPv6,
|
||||
mask6: [uint64.fromBytes(netmask.address_v6.toOpenArray(0, 7)),
|
||||
uint64.fromBytes(netmask.address_v6.toOpenArray(8, 15))])
|
||||
mask6: [
|
||||
uint64.fromBytesBE(netmask.address_v6.toOpenArray(0, 7)),
|
||||
uint64.fromBytesBE(netmask.address_v6.toOpenArray(8, 15))])
|
||||
else:
|
||||
IpMask(family: netmask.family)
|
||||
|
||||
@ -95,8 +96,7 @@ proc initIp*(t: typedesc[IpMask], netmask: string): IpMask =
|
||||
## If ``netmask`` address string is invalid, result IpMask.family will be
|
||||
## set to ``AddressFamily.None``.
|
||||
try:
|
||||
var ip = parseIpAddress(netmask)
|
||||
var tip = initTAddress(ip, Port(0))
|
||||
let tip = initTAddress(parseIpAddress(netmask), Port(0))
|
||||
t.init(tip)
|
||||
except ValueError:
|
||||
IpMask(family: AddressFamily.None)
|
||||
@ -127,9 +127,9 @@ proc init*(t: typedesc[IpMask], netmask: string): IpMask =
|
||||
elif netmask[offset + i] in hexLowers:
|
||||
v = uint32(ord(netmask[offset + i]) - ord('a') + 10)
|
||||
else:
|
||||
return
|
||||
return IpMask(family: AddressFamily.None)
|
||||
r = (r shl 4) or v
|
||||
res.mask4 = r.toBE()
|
||||
res.mask4 = r
|
||||
res
|
||||
elif length == 32 or length == (2 + 32):
|
||||
## IPv6 mask
|
||||
@ -147,10 +147,10 @@ proc init*(t: typedesc[IpMask], netmask: string): IpMask =
|
||||
elif netmask[offset + i] in hexLowers:
|
||||
v = uint64(ord(netmask[offset + i]) - ord('a') + 10)
|
||||
else:
|
||||
return
|
||||
return IpMask(family: AddressFamily.None)
|
||||
r = (r shl 4) or v
|
||||
offset += 16
|
||||
res.mask6[i] = r.toBE()
|
||||
res.mask6[i] = r
|
||||
res
|
||||
else:
|
||||
IpMask(family: AddressFamily.None)
|
||||
@ -167,8 +167,7 @@ proc toIPv6*(address: TransportAddress): TransportAddress =
|
||||
var address6: array[16, uint8]
|
||||
address6[10] = 0xFF'u8
|
||||
address6[11] = 0xFF'u8
|
||||
let ip4 = uint32.fromBytes(address.address_v4)
|
||||
address6[12 .. 15] = ip4.toBytes()
|
||||
address6[12 .. 15] = toBytesBE(uint32.fromBytesBE(address.address_v4))
|
||||
TransportAddress(family: AddressFamily.IPv6, port: address.port,
|
||||
address_v6: address6)
|
||||
of AddressFamily.IPv6:
|
||||
@ -183,9 +182,10 @@ proc isV4Mapped*(address: TransportAddress): bool =
|
||||
## Procedure returns ``false`` if ``address`` family is IPv4.
|
||||
case address.family
|
||||
of AddressFamily.IPv6:
|
||||
let data0 = uint64.fromBytes(address.address_v6.toOpenArray(0, 7))
|
||||
let data1 = uint16.fromBytes(address.address_v6.toOpenArray(8, 9))
|
||||
let data2 = uint16.fromBytes(address.address_v6.toOpenArray(10, 11))
|
||||
let
|
||||
data0 = uint64.fromBytesBE(address.address_v6.toOpenArray(0, 7))
|
||||
data1 = uint16.fromBytesBE(address.address_v6.toOpenArray(8, 9))
|
||||
data2 = uint16.fromBytesBE(address.address_v6.toOpenArray(10, 11))
|
||||
(data0 == 0x00'u64) and (data1 == 0x00'u16) and (data2 == 0xFFFF'u16)
|
||||
else:
|
||||
false
|
||||
@ -202,9 +202,9 @@ proc toIPv4*(address: TransportAddress): TransportAddress =
|
||||
address
|
||||
of AddressFamily.IPv6:
|
||||
if isV4Mapped(address):
|
||||
let data = uint32.fromBytes(address.address_v6.toOpenArray(12, 15))
|
||||
let data = uint32.fromBytesBE(address.address_v6.toOpenArray(12, 15))
|
||||
TransportAddress(family: AddressFamily.IPv4, port: address.port,
|
||||
address_v4: data.toBytes())
|
||||
address_v4: data.toBytesBE())
|
||||
else:
|
||||
TransportAddress(family: AddressFamily.None)
|
||||
else:
|
||||
@ -230,34 +230,34 @@ proc mask*(a: TransportAddress, m: IpMask): TransportAddress =
|
||||
## In all other cases returned address will have ``AddressFamily.None``.
|
||||
if (a.family == AddressFamily.IPv4) and (m.family == AddressFamily.IPv6):
|
||||
if (m.mask6[0] == 0xFFFF_FFFF_FFFF_FFFF'u64) and
|
||||
(m.mask6[1] and 0xFFFF_FFFF'u64) == 0xFFFF_FFFF'u64:
|
||||
(m.mask6[1] and 0xFFFF_FFFF_0000_0000'u64) == 0xFFFF_FFFF_0000_0000'u64:
|
||||
let
|
||||
mask = uint32((m.mask6[1] shr 32) and 0xFFFF_FFFF'u64)
|
||||
data = uint32.fromBytes(a.address_v4)
|
||||
mask = uint32(m.mask6[1] and 0xFFFF_FFFF'u64)
|
||||
data = uint32.fromBytesBE(a.address_v4)
|
||||
TransportAddress(family: AddressFamily.IPv4, port: a.port,
|
||||
address_v4: (data and mask).toBytes())
|
||||
address_v4: (data and mask).toBytesBE())
|
||||
else:
|
||||
TransportAddress(family: AddressFamily.None)
|
||||
elif (a.family == AddressFamily.IPv6) and (m.family == AddressFamily.IPv4):
|
||||
var ip = a.toIPv4()
|
||||
if ip.family != AddressFamily.IPv4:
|
||||
return TransportAddress(family: AddressFamily.None)
|
||||
let data = uint32.fromBytes(ip.address_v4)
|
||||
ip.address_v4[0 .. 3] = (data and m.mask4).toBytes()
|
||||
let data = uint32.fromBytesBE(ip.address_v4)
|
||||
ip.address_v4[0 .. 3] = (data and m.mask4).toBytesBE()
|
||||
var res = ip.toIPv6()
|
||||
res.port = a.port
|
||||
res
|
||||
elif a.family == AddressFamily.IPv4 and m.family == AddressFamily.IPv4:
|
||||
let data = uint32.fromBytes(a.address_v4)
|
||||
let data = uint32.fromBytesBE(a.address_v4)
|
||||
TransportAddress(family: AddressFamily.IPv4, port: a.port,
|
||||
address_v4: (data and m.mask4).toBytes())
|
||||
address_v4: (data and m.mask4).toBytesBE())
|
||||
elif a.family == AddressFamily.IPv6 and m.family == AddressFamily.IPv6:
|
||||
var address6: array[16, uint8]
|
||||
let
|
||||
data0 = uint64.fromBytes(a.address_v6.toOpenArray(0, 7))
|
||||
data1 = uint64.fromBytes(a.address_v6.toOpenArray(8, 15))
|
||||
address6[0 .. 7] = (data0 and m.mask6[0]).toBytes()
|
||||
address6[8 .. 15] = (data1 and m.mask6[1]).toBytes()
|
||||
data0 = uint64.fromBytesBE(a.address_v6.toOpenArray(0, 7))
|
||||
data1 = uint64.fromBytesBE(a.address_v6.toOpenArray(8, 15))
|
||||
address6[0 .. 7] = (data0 and m.mask6[0]).toBytesBE()
|
||||
address6[8 .. 15] = (data1 and m.mask6[1]).toBytesBE()
|
||||
TransportAddress(family: AddressFamily.IPv6, port: a.port,
|
||||
address_v6: address6)
|
||||
else:
|
||||
@ -272,14 +272,14 @@ proc prefix*(mask: IpMask): int =
|
||||
of AddressFamily.IPv4:
|
||||
var
|
||||
res = 0
|
||||
n = mask.mask4.fromBE()
|
||||
n = mask.mask4
|
||||
while n != 0:
|
||||
if (n and 0x8000_0000'u32) == 0'u32: return -1
|
||||
n = n shl 1
|
||||
inc(res)
|
||||
res
|
||||
of AddressFamily.IPv6:
|
||||
let mask6 = [mask.mask6[0].fromBE(), mask.mask6[1].fromBE()]
|
||||
let mask6 = [mask.mask6[0], mask.mask6[1]]
|
||||
var res = 0
|
||||
if mask6[0] == 0xFFFF_FFFF_FFFF_FFFF'u64:
|
||||
res += 64
|
||||
@ -308,11 +308,11 @@ proc subnetMask*(mask: IpMask): TransportAddress =
|
||||
case mask.family
|
||||
of AddressFamily.IPv4:
|
||||
TransportAddress(family: AddressFamily.IPv4,
|
||||
address_v4: mask.mask4.toBytes())
|
||||
address_v4: mask.mask4.toBytesBE())
|
||||
of AddressFamily.IPv6:
|
||||
var address6: array[16, uint8]
|
||||
address6[0 .. 7] = mask.mask6[0].toBytes()
|
||||
address6[8 .. 15] = mask.mask6[1].toBytes()
|
||||
address6[0 .. 7] = mask.mask6[0].toBytesBE()
|
||||
address6[8 .. 15] = mask.mask6[1].toBytesBE()
|
||||
TransportAddress(family: AddressFamily.IPv6, address_v6: address6)
|
||||
else:
|
||||
TransportAddress(family: mask.family)
|
||||
@ -321,9 +321,10 @@ proc `$`*(mask: IpMask, include0x = false): string =
|
||||
## Returns hexadecimal string representation of IP mask ``mask``.
|
||||
case mask.family
|
||||
of AddressFamily.IPv4:
|
||||
var res = if include0x: "0x" else: ""
|
||||
var n = 32
|
||||
var m = mask.mask4.fromBE()
|
||||
var
|
||||
res = if include0x: "0x" else: ""
|
||||
n = 32
|
||||
m = mask.mask4
|
||||
while n > 0:
|
||||
n -= 4
|
||||
var c = int((m shr n) and 0x0F)
|
||||
@ -333,7 +334,7 @@ proc `$`*(mask: IpMask, include0x = false): string =
|
||||
res.add(chr(ord('A') + (c - 10)))
|
||||
res
|
||||
of AddressFamily.IPv6:
|
||||
let mask6 = [mask.mask6[0].fromBE(), mask.mask6[1].fromBE()]
|
||||
let mask6 = [mask.mask6[0], mask.mask6[1]]
|
||||
var res = if include0x: "0x" else: ""
|
||||
for i in 0 .. 1:
|
||||
var n = 64
|
||||
@ -353,12 +354,11 @@ proc ip*(mask: IpMask): string {.raises: [ValueError].} =
|
||||
## Returns IP address text representation of IP mask ``mask``.
|
||||
case mask.family
|
||||
of AddressFamily.IPv4:
|
||||
var address4: array[4, uint8]
|
||||
copyMem(addr address4[0], unsafeAddr mask.mask4, sizeof(uint32))
|
||||
$IpAddress(family: IpAddressFamily.IPv4, address_v4: address4)
|
||||
$IpAddress(family: IpAddressFamily.IPv4, address_v4: mask.mask4.toBytesBE())
|
||||
of AddressFamily.Ipv6:
|
||||
var address6: array[16, uint8]
|
||||
copyMem(addr address6[0], unsafeAddr mask.mask6[0], 16)
|
||||
address6[0 .. 7] = mask.mask6[0].toBytesBE()
|
||||
address6[8 .. 15] = mask.mask6[1].toBytesBE()
|
||||
$IpAddress(family: IpAddressFamily.IPv6, address_v6: address6)
|
||||
else:
|
||||
raise newException(ValueError, "Invalid mask family type")
|
||||
@ -387,11 +387,12 @@ proc init*(t: typedesc[IpNet], network: string): IpNet {.
|
||||
raises: [TransportAddressError].} =
|
||||
## Initialize IP Network from string representation in format
|
||||
## <address>/<prefix length> or <address>/<netmask address>.
|
||||
var parts = network.rsplit("/", maxsplit = 1)
|
||||
var host, mhost: TransportAddress
|
||||
var ipaddr: IpAddress
|
||||
var mask: IpMask
|
||||
var prefix: int
|
||||
var
|
||||
parts = network.rsplit("/", maxsplit = 1)
|
||||
host, mhost: TransportAddress
|
||||
ipaddr: IpAddress
|
||||
mask: IpMask
|
||||
prefix: int
|
||||
try:
|
||||
ipaddr = parseIpAddress(parts[0])
|
||||
if ipaddr.family == IpAddressFamily.IPv4:
|
||||
@ -428,9 +429,9 @@ proc init*(t: typedesc[IpNet], network: string): IpNet {.
|
||||
raise newException(TransportAddressError,
|
||||
"Incorrect network address!")
|
||||
if prefix == -1:
|
||||
result = t.init(host, mask)
|
||||
t.init(host, mask)
|
||||
else:
|
||||
result = t.init(host, prefix)
|
||||
t.init(host, prefix)
|
||||
except ValueError as exc:
|
||||
raise newException(TransportAddressError, exc.msg)
|
||||
|
||||
@ -461,19 +462,19 @@ proc broadcast*(net: IpNet): TransportAddress =
|
||||
case net.host.family
|
||||
of AddressFamily.IPv4:
|
||||
let
|
||||
host = uint32.fromBytes(net.host.address_v4)
|
||||
host = uint32.fromBytesBE(net.host.address_v4)
|
||||
mask = net.mask.mask4
|
||||
TransportAddress(family: AddressFamily.IPv4,
|
||||
address_v4: (host or (not(mask))).toBytes())
|
||||
address_v4: (host or (not(mask))).toBytesBE())
|
||||
of AddressFamily.IPv6:
|
||||
var address6: array[16, uint8]
|
||||
let
|
||||
host0 = uint64.fromBytes(net.host.address_v6.toOpenArray(0, 7))
|
||||
host1 = uint64.fromBytes(net.host.address_v6.toOpenArray(8, 15))
|
||||
host0 = uint64.fromBytesBE(net.host.address_v6.toOpenArray(0, 7))
|
||||
host1 = uint64.fromBytesBE(net.host.address_v6.toOpenArray(8, 15))
|
||||
data0 = net.mask.mask6[0]
|
||||
data1 = net.mask.mask6[1]
|
||||
address6[0 .. 7] = (host0 or (not(data0))).toBytes()
|
||||
address6[8 .. 15] = (host1 or (not(data1))).toBytes()
|
||||
address6[0 .. 7] = (host0 or (not(data0))).toBytesBE()
|
||||
address6[8 .. 15] = (host1 or (not(data1))).toBytesBE()
|
||||
TransportAddress(family: AddressFamily.IPv6, address_v6: address6)
|
||||
else:
|
||||
TransportAddress(family: AddressFamily.None)
|
||||
@ -496,19 +497,19 @@ proc `and`*(address1, address2: TransportAddress): TransportAddress =
|
||||
case address1.family
|
||||
of AddressFamily.IPv4:
|
||||
let
|
||||
data1 = uint32.fromBytes(address1.address_v4)
|
||||
data2 = uint32.fromBytes(address2.address_v4)
|
||||
data1 = uint32.fromBytesBE(address1.address_v4)
|
||||
data2 = uint32.fromBytesBE(address2.address_v4)
|
||||
TransportAddress(family: AddressFamily.IPv4,
|
||||
address_v4: (data1 and data2).toBytes())
|
||||
address_v4: (data1 and data2).toBytesBE())
|
||||
of AddressFamily.IPv6:
|
||||
var address6: array[16, uint8]
|
||||
let
|
||||
data1 = uint64.fromBytes(address1.address_v6.toOpenArray(0, 7))
|
||||
data2 = uint64.fromBytes(address1.address_v6.toOpenArray(8, 15))
|
||||
data3 = uint64.fromBytes(address2.address_v6.toOpenArray(0, 7))
|
||||
data4 = uint64.fromBytes(address2.address_v6.toOpenArray(8, 15))
|
||||
address6[0 .. 7] = (data1 and data3).toBytes()
|
||||
address6[8 .. 15] = (data2 and data4).toBytes()
|
||||
data1 = uint64.fromBytesBE(address1.address_v6.toOpenArray(0, 7))
|
||||
data2 = uint64.fromBytesBE(address1.address_v6.toOpenArray(8, 15))
|
||||
data3 = uint64.fromBytesBE(address2.address_v6.toOpenArray(0, 7))
|
||||
data4 = uint64.fromBytesBE(address2.address_v6.toOpenArray(8, 15))
|
||||
address6[0 .. 7] = (data1 and data3).toBytesBE()
|
||||
address6[8 .. 15] = (data2 and data4).toBytesBE()
|
||||
TransportAddress(family: AddressFamily.IPv6, address_v6: address6)
|
||||
else:
|
||||
raiseAssert "Invalid address family type"
|
||||
@ -522,19 +523,19 @@ proc `or`*(address1, address2: TransportAddress): TransportAddress =
|
||||
case address1.family
|
||||
of AddressFamily.IPv4:
|
||||
let
|
||||
data1 = uint32.fromBytes(address1.address_v4)
|
||||
data2 = uint32.fromBytes(address2.address_v4)
|
||||
data1 = uint32.fromBytesBE(address1.address_v4)
|
||||
data2 = uint32.fromBytesBE(address2.address_v4)
|
||||
TransportAddress(family: AddressFamily.IPv4,
|
||||
address_v4: (data1 or data2).toBytes())
|
||||
address_v4: (data1 or data2).toBytesBE())
|
||||
of AddressFamily.IPv6:
|
||||
var address6: array[16, uint8]
|
||||
let
|
||||
data1 = uint64.fromBytes(address1.address_v6.toOpenArray(0, 7))
|
||||
data2 = uint64.fromBytes(address1.address_v6.toOpenArray(8, 15))
|
||||
data3 = uint64.fromBytes(address2.address_v6.toOpenArray(0, 7))
|
||||
data4 = uint64.fromBytes(address2.address_v6.toOpenArray(8, 15))
|
||||
address6[0 .. 7] = (data1 or data3).toBytes()
|
||||
address6[8 .. 15] = (data2 or data4).toBytes()
|
||||
data1 = uint64.fromBytesBE(address1.address_v6.toOpenArray(0, 7))
|
||||
data2 = uint64.fromBytesBE(address1.address_v6.toOpenArray(8, 15))
|
||||
data3 = uint64.fromBytesBE(address2.address_v6.toOpenArray(0, 7))
|
||||
data4 = uint64.fromBytesBE(address2.address_v6.toOpenArray(8, 15))
|
||||
address6[0 .. 7] = (data1 or data3).toBytesBE()
|
||||
address6[8 .. 15] = (data2 or data4).toBytesBE()
|
||||
TransportAddress(family: AddressFamily.IPv6, address_v6: address6)
|
||||
else:
|
||||
raiseAssert "Invalid address family type"
|
||||
@ -543,15 +544,15 @@ proc `not`*(address: TransportAddress): TransportAddress =
|
||||
## Bitwise ``not`` operation for ``address``.
|
||||
case address.family
|
||||
of AddressFamily.IPv4:
|
||||
let data = not(uint32.fromBytes(address.address_v4))
|
||||
TransportAddress(family: AddressFamily.IPv4, address_v4: data.toBytes())
|
||||
let data = not(uint32.fromBytesBE(address.address_v4))
|
||||
TransportAddress(family: AddressFamily.IPv4, address_v4: data.toBytesBE())
|
||||
of AddressFamily.IPv6:
|
||||
var address6: array[16, uint8]
|
||||
let
|
||||
data1 = not(uint64.fromBytes(address.address_v6.toOpenArray(0, 7)))
|
||||
data2 = not(uint64.fromBytes(address.address_v6.toOpenArray(8, 15)))
|
||||
address6[0 .. 7] = data1.toBytes()
|
||||
address6[8 .. 15] = data2.toBytes()
|
||||
data1 = not(uint64.fromBytesBE(address.address_v6.toOpenArray(0, 7)))
|
||||
data2 = not(uint64.fromBytesBE(address.address_v6.toOpenArray(8, 15)))
|
||||
address6[0 .. 7] = data1.toBytesBE()
|
||||
address6[8 .. 15] = data2.toBytesBE()
|
||||
TransportAddress(family: AddressFamily.IPv6, address_v6: address6)
|
||||
else:
|
||||
address
|
||||
@ -702,10 +703,10 @@ proc isZero*(address: TransportAddress): bool {.inline.} =
|
||||
## not ``AddressFamily.None``.
|
||||
case address.family
|
||||
of AddressFamily.IPv4:
|
||||
uint32.fromBytes(a4()) == 0'u32
|
||||
uint32.fromBytesBE(a4()) == 0'u32
|
||||
of AddressFamily.IPv6:
|
||||
(uint64.fromBytes(a6.toOpenArray(0, 7)) == 0'u64) and
|
||||
(uint64.fromBytes(a6.toOpenArray(8, 15)) == 0'u64)
|
||||
(uint64.fromBytesBE(a6.toOpenArray(0, 7)) == 0'u64) and
|
||||
(uint64.fromBytesBE(a6.toOpenArray(8, 15)) == 0'u64)
|
||||
of AddressFamily.Unix:
|
||||
len($cast[cstring](unsafeAddr address.address_un[0])) == 0
|
||||
else:
|
||||
@ -804,7 +805,7 @@ proc isLoopback*(address: TransportAddress): bool =
|
||||
of AddressFamily.IPv4:
|
||||
a4[0] == 127'u8
|
||||
of AddressFamily.IPv6:
|
||||
(uint64.fromBytes(a6.toOpenArray(0, 7)) == 0x00'u64) and
|
||||
(uint64.fromBytesBE(a6.toOpenArray(0, 7)) == 0x00'u64) and
|
||||
(uint64.fromBytesBE(a6.toOpenArray(8, 15)) == 0x01'u64)
|
||||
else:
|
||||
false
|
||||
@ -817,10 +818,10 @@ proc isAnyLocal*(address: TransportAddress): bool =
|
||||
## ``IPv6``: ::
|
||||
case address.family
|
||||
of AddressFamily.IPv4:
|
||||
uint32.fromBytes(a4) == 0'u32
|
||||
uint32.fromBytesBE(a4) == 0'u32
|
||||
of AddressFamily.IPv6:
|
||||
(uint64.fromBytes(a6.toOpenArray(0, 7)) == 0x00'u64) and
|
||||
(uint64.fromBytes(a6.toOpenArray(8, 15)) == 0x00'u64)
|
||||
(uint64.fromBytesBE(a6.toOpenArray(0, 7)) == 0x00'u64) and
|
||||
(uint64.fromBytesBE(a6.toOpenArray(8, 15)) == 0x00'u64)
|
||||
else:
|
||||
false
|
||||
|
||||
@ -895,7 +896,7 @@ proc isBroadcast*(address: TransportAddress): bool =
|
||||
## ``IPv4``: 255.255.255.255
|
||||
case address.family
|
||||
of AddressFamily.IPv4:
|
||||
uint32.fromBytes(a4) == 0xFFFF_FFFF'u32
|
||||
uint32.fromBytesBE(a4) == 0xFFFF_FFFF'u32
|
||||
of AddressFamily.IPv6:
|
||||
false
|
||||
else:
|
||||
@ -916,7 +917,7 @@ proc isBenchmarking*(address: TransportAddress): bool =
|
||||
of AddressFamily.IPv6:
|
||||
(uint16.fromBytesBE(a6.toOpenArray(0, 1)) == 0x2001'u16) and
|
||||
(uint16.fromBytesBE(a6.toOpenArray(2, 3)) == 0x02'u16) and
|
||||
(uint16.fromBytes(a6.toOpenArray(4, 5)) == 0x00'u16)
|
||||
(uint16.fromBytesBE(a6.toOpenArray(4, 5)) == 0x00'u16)
|
||||
else:
|
||||
false
|
||||
|
||||
@ -980,9 +981,9 @@ proc isGlobal*(address: TransportAddress): bool =
|
||||
address.isLoopback() or
|
||||
(
|
||||
# IPv4-Mapped `::FFFF:0:0/96`
|
||||
(uint64.fromBytes(a6.toOpenArray(0, 7)) == 0x00'u64) and
|
||||
(uint16.fromBytes(a6.toOpenArray(8, 9)) == 0x00'u16) and
|
||||
(uint16.fromBytes(a6.toOpenArray(10, 11)) == 0xFFFF'u16)
|
||||
(uint64.fromBytesBE(a6.toOpenArray(0, 7)) == 0x00'u64) and
|
||||
(uint16.fromBytesBE(a6.toOpenArray(8, 9)) == 0x00'u16) and
|
||||
(uint16.fromBytesBE(a6.toOpenArray(10, 11)) == 0xFFFF'u16)
|
||||
) or
|
||||
(
|
||||
# IPv4-IPv6 Translation `64:FF9B:1::/48`
|
||||
@ -993,8 +994,8 @@ proc isGlobal*(address: TransportAddress): bool =
|
||||
(
|
||||
# Discard-Only Address Block `100::/64`
|
||||
(uint16.fromBytesBE(a6.toOpenArray(0, 1)) == 0x100'u16) and
|
||||
(uint32.fromBytes(a6.toOpenArray(2, 5)) == 0x00'u32) and
|
||||
(uint16.fromBytes(a6.toOpenArray(6, 7)) == 0x00'u16)
|
||||
(uint32.fromBytesBE(a6.toOpenArray(2, 5)) == 0x00'u32) and
|
||||
(uint16.fromBytesBE(a6.toOpenArray(6, 7)) == 0x00'u16)
|
||||
) or
|
||||
(
|
||||
# IETF Protocol Assignments `2001::/23`
|
||||
@ -1004,15 +1005,15 @@ proc isGlobal*(address: TransportAddress): bool =
|
||||
(
|
||||
# Port Control Protocol Anycast `2001:1::1`
|
||||
(uint32.fromBytesBE(a6.toOpenArray(0, 3)) == 0x20010001'u32) and
|
||||
(uint32.fromBytes(a6.toOpenArray(4, 7)) == 0x00'u32) and
|
||||
(uint32.fromBytes(a6.toOpenArray(8, 11)) == 0x00'u32) and
|
||||
(uint32.fromBytesBE(a6.toOpenArray(4, 7)) == 0x00'u32) and
|
||||
(uint32.fromBytesBE(a6.toOpenArray(8, 11)) == 0x00'u32) and
|
||||
(uint32.fromBytesBE(a6.toOpenArray(12, 15)) == 0x01'u32)
|
||||
) or
|
||||
(
|
||||
# Traversal Using Relays around NAT Anycast `2001:1::2`
|
||||
(uint32.fromBytesBE(a6.toOpenArray(0, 3)) == 0x20010001'u32) and
|
||||
(uint32.fromBytes(a6.toOpenArray(4, 7)) == 0x00'u32) and
|
||||
(uint32.fromBytes(a6.toOpenArray(8, 11)) == 0x00'u32) and
|
||||
(uint32.fromBytesBE(a6.toOpenArray(4, 7)) == 0x00'u32) and
|
||||
(uint32.fromBytesBE(a6.toOpenArray(8, 11)) == 0x00'u32) and
|
||||
(uint32.fromBytesBE(a6.toOpenArray(12, 15)) == 0x02'u32)
|
||||
) or
|
||||
(
|
||||
@ -1025,7 +1026,7 @@ proc isGlobal*(address: TransportAddress): bool =
|
||||
(uint16.fromBytesBE(a6.toOpenArray(0, 1)) == 0x2001'u16) and
|
||||
(uint16.fromBytesBE(a6.toOpenArray(2, 3)) == 0x04'u16) and
|
||||
(uint16.fromBytesBE(a6.toOpenArray(4, 5)) == 0x112'u16) and
|
||||
(uint16.fromBytes(a6.toOpenArray(6, 7)) == 0x00'u16)
|
||||
(uint16.fromBytesBE(a6.toOpenArray(6, 7)) == 0x00'u16)
|
||||
) or
|
||||
(
|
||||
# ORCHIDv2 `2001:20::/28`
|
||||
|
||||
@ -677,10 +677,10 @@ when defined(linux):
|
||||
var msg = cast[ptr NlMsgHeader](addr data[0])
|
||||
var endflag = false
|
||||
while NLMSG_OK(msg, length):
|
||||
if msg.nlmsg_type == NLMSG_ERROR:
|
||||
if msg.nlmsg_type in [uint16(NLMSG_DONE), uint16(NLMSG_ERROR)]:
|
||||
endflag = true
|
||||
break
|
||||
else:
|
||||
elif msg.nlmsg_type == RTM_NEWROUTE:
|
||||
res = processRoute(msg)
|
||||
endflag = true
|
||||
break
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -21,11 +21,12 @@ template asyncTest*(name: string, body: untyped): untyped =
|
||||
|
||||
template checkLeaks*(name: string): untyped =
|
||||
let counter = getTrackerCounter(name)
|
||||
if counter.opened != counter.closed:
|
||||
echo "[" & name & "] opened = ", counter.opened,
|
||||
", closed = ", counter.closed
|
||||
checkpoint:
|
||||
"[" & name & "] opened = " & $counter.opened &
|
||||
", closed = " & $ counter.closed
|
||||
check counter.opened == counter.closed
|
||||
|
||||
template checkLeaks*(): untyped =
|
||||
proc checkLeaks*() =
|
||||
for key in getThreadDispatcher().trackerCounterKeys():
|
||||
checkLeaks(key)
|
||||
GC_fullCollect()
|
||||
|
||||
1
docs/.gitignore
vendored
Normal file
1
docs/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
book
|
||||
20
docs/book.toml
Normal file
20
docs/book.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[book]
|
||||
authors = ["Jacek Sieka"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "Chronos"
|
||||
|
||||
[preprocessor.toc]
|
||||
command = "mdbook-toc"
|
||||
renderer = ["html"]
|
||||
max-level = 2
|
||||
|
||||
[preprocessor.open-on-gh]
|
||||
command = "mdbook-open-on-gh"
|
||||
renderer = ["html"]
|
||||
|
||||
[output.html]
|
||||
git-repository-url = "https://github.com/status-im/nim-chronos/"
|
||||
git-branch = "master"
|
||||
additional-css = ["open-in.css"]
|
||||
21
docs/examples/cancellation.nim
Normal file
21
docs/examples/cancellation.nim
Normal file
@ -0,0 +1,21 @@
|
||||
## Simple cancellation example
|
||||
|
||||
import chronos
|
||||
|
||||
proc someTask() {.async.} = await sleepAsync(10.minutes)
|
||||
|
||||
proc cancellationExample() {.async.} =
|
||||
# Start a task but don't wait for it to finish
|
||||
let future = someTask()
|
||||
future.cancelSoon()
|
||||
# `cancelSoon` schedules but does not wait for the future to get cancelled -
|
||||
# it might still be pending here
|
||||
|
||||
let future2 = someTask() # Start another task concurrently
|
||||
await future2.cancelAndWait()
|
||||
# Using `cancelAndWait`, we can be sure that `future2` is either
|
||||
# complete, failed or cancelled at this point. `future` could still be
|
||||
# pending!
|
||||
assert future2.finished()
|
||||
|
||||
waitFor(cancellationExample())
|
||||
28
docs/examples/discards.nim
Normal file
28
docs/examples/discards.nim
Normal file
@ -0,0 +1,28 @@
|
||||
## The peculiarities of `discard` in `async` procedures
|
||||
import chronos
|
||||
|
||||
proc failingOperation() {.async.} =
|
||||
echo "Raising!"
|
||||
raise (ref ValueError)(msg: "My error")
|
||||
|
||||
proc myApp() {.async.} =
|
||||
# This style of discard causes the `ValueError` to be discarded, hiding the
|
||||
# failure of the operation - avoid!
|
||||
discard failingOperation()
|
||||
|
||||
proc runAsTask(fut: Future[void]): Future[void] {.async: (raises: []).} =
|
||||
# runAsTask uses `raises: []` to ensure at compile-time that no exceptions
|
||||
# escape it!
|
||||
try:
|
||||
await fut
|
||||
except CatchableError as exc:
|
||||
echo "The task failed! ", exc.msg
|
||||
|
||||
# asyncSpawn ensures that errors don't leak unnoticed from tasks without
|
||||
# blocking:
|
||||
asyncSpawn runAsTask(failingOperation())
|
||||
|
||||
# If we didn't catch the exception with `runAsTask`, the program will crash:
|
||||
asyncSpawn failingOperation()
|
||||
|
||||
waitFor myApp()
|
||||
15
docs/examples/httpget.nim
Normal file
15
docs/examples/httpget.nim
Normal file
@ -0,0 +1,15 @@
|
||||
import chronos/apps/http/httpclient
|
||||
|
||||
proc retrievePage*(uri: string): Future[string] {.async.} =
|
||||
# Create a new HTTP session
|
||||
let httpSession = HttpSessionRef.new()
|
||||
try:
|
||||
# Fetch page contents
|
||||
let resp = await httpSession.fetch(parseUri(uri))
|
||||
# Convert response to a string, assuming its encoding matches the terminal!
|
||||
bytesToString(resp.data)
|
||||
finally: # Close the session
|
||||
await noCancel(httpSession.closeWait())
|
||||
|
||||
echo waitFor retrievePage(
|
||||
"https://raw.githubusercontent.com/status-im/nim-chronos/master/README.md")
|
||||
130
docs/examples/middleware.nim
Normal file
130
docs/examples/middleware.nim
Normal file
@ -0,0 +1,130 @@
|
||||
import chronos/apps/http/httpserver
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
proc firstMiddlewareHandler(
|
||||
middleware: HttpServerMiddlewareRef,
|
||||
reqfence: RequestFence,
|
||||
nextHandler: HttpProcessCallback2
|
||||
): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
|
||||
if reqfence.isErr():
|
||||
# Ignore request errors
|
||||
return await nextHandler(reqfence)
|
||||
|
||||
let request = reqfence.get()
|
||||
var headers = request.headers
|
||||
|
||||
if request.uri.path.startsWith("/path/to/hidden/resources"):
|
||||
headers.add("X-Filter", "drop")
|
||||
elif request.uri.path.startsWith("/path/to/blocked/resources"):
|
||||
headers.add("X-Filter", "block")
|
||||
else:
|
||||
headers.add("X-Filter", "pass")
|
||||
|
||||
# Updating request by adding new HTTP header `X-Filter`.
|
||||
let res = request.updateRequest(headers)
|
||||
if res.isErr():
|
||||
# We use default error handler in case of error which will respond with
|
||||
# proper HTTP status code error.
|
||||
return defaultResponse(res.error)
|
||||
|
||||
# Calling next handler.
|
||||
await nextHandler(reqfence)
|
||||
|
||||
proc secondMiddlewareHandler(
|
||||
middleware: HttpServerMiddlewareRef,
|
||||
reqfence: RequestFence,
|
||||
nextHandler: HttpProcessCallback2
|
||||
): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
|
||||
if reqfence.isErr():
|
||||
# Ignore request errors
|
||||
return await nextHandler(reqfence)
|
||||
|
||||
let
|
||||
request = reqfence.get()
|
||||
filtered = request.headers.getString("X-Filter", "pass")
|
||||
|
||||
if filtered == "drop":
|
||||
# Force HTTP server to drop connection with remote peer.
|
||||
dropResponse()
|
||||
elif filtered == "block":
|
||||
# Force HTTP server to respond with HTTP `404 Not Found` error code.
|
||||
codeResponse(Http404)
|
||||
else:
|
||||
# Calling next handler.
|
||||
await nextHandler(reqfence)
|
||||
|
||||
proc thirdMiddlewareHandler(
|
||||
middleware: HttpServerMiddlewareRef,
|
||||
reqfence: RequestFence,
|
||||
nextHandler: HttpProcessCallback2
|
||||
): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
|
||||
if reqfence.isErr():
|
||||
# Ignore request errors
|
||||
return await nextHandler(reqfence)
|
||||
|
||||
let request = reqfence.get()
|
||||
echo "QUERY = [", request.rawPath, "]"
|
||||
echo request.headers
|
||||
try:
|
||||
if request.uri.path == "/path/to/plugin/resources/page1":
|
||||
await request.respond(Http200, "PLUGIN PAGE1")
|
||||
elif request.uri.path == "/path/to/plugin/resources/page2":
|
||||
await request.respond(Http200, "PLUGIN PAGE2")
|
||||
else:
|
||||
# Calling next handler.
|
||||
await nextHandler(reqfence)
|
||||
except HttpWriteError as exc:
|
||||
# We use default error handler if we unable to send response.
|
||||
defaultResponse(exc)
|
||||
|
||||
proc mainHandler(
|
||||
reqfence: RequestFence
|
||||
): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
|
||||
if reqfence.isErr():
|
||||
return defaultResponse()
|
||||
|
||||
let request = reqfence.get()
|
||||
try:
|
||||
if request.uri.path == "/path/to/original/page1":
|
||||
await request.respond(Http200, "ORIGINAL PAGE1")
|
||||
elif request.uri.path == "/path/to/original/page2":
|
||||
await request.respond(Http200, "ORIGINAL PAGE2")
|
||||
else:
|
||||
# Force HTTP server to respond with `404 Not Found` status code.
|
||||
codeResponse(Http404)
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
|
||||
proc middlewareExample() {.async: (raises: []).} =
|
||||
let
|
||||
middlewares = [
|
||||
HttpServerMiddlewareRef(handler: firstMiddlewareHandler),
|
||||
HttpServerMiddlewareRef(handler: secondMiddlewareHandler),
|
||||
HttpServerMiddlewareRef(handler: thirdMiddlewareHandler)
|
||||
]
|
||||
socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
boundAddress =
|
||||
if isAvailable(AddressFamily.IPv6):
|
||||
AnyAddress6
|
||||
else:
|
||||
AnyAddress
|
||||
res = HttpServerRef.new(boundAddress, mainHandler,
|
||||
socketFlags = socketFlags,
|
||||
middlewares = middlewares)
|
||||
|
||||
doAssert(res.isOk(), "Unable to start HTTP server")
|
||||
let server = res.get()
|
||||
server.start()
|
||||
let address = server.instance.localAddress()
|
||||
echo "HTTP server running on ", address
|
||||
try:
|
||||
await server.join()
|
||||
except CancelledError:
|
||||
discard
|
||||
finally:
|
||||
await server.stop()
|
||||
await server.closeWait()
|
||||
|
||||
when isMainModule:
|
||||
waitFor(middlewareExample())
|
||||
1
docs/examples/nim.cfg
Normal file
1
docs/examples/nim.cfg
Normal file
@ -0,0 +1 @@
|
||||
path = "../.."
|
||||
38
docs/examples/signalling.nim
Normal file
38
docs/examples/signalling.nim
Normal file
@ -0,0 +1,38 @@
|
||||
import chronos, chronos/threadsync
|
||||
import os
|
||||
|
||||
type
|
||||
Context = object
|
||||
# Context allocated by `createShared` should contain no garbage-collected
|
||||
# types!
|
||||
signal: ThreadSignalPtr
|
||||
value: int
|
||||
|
||||
proc myThread(ctx: ptr Context) {.thread.} =
|
||||
echo "Doing some work in a thread"
|
||||
sleep(3000)
|
||||
ctx.value = 42
|
||||
echo "Done, firing the signal"
|
||||
discard ctx.signal.fireSync().expect("correctly initialized signal should not fail")
|
||||
|
||||
proc main() {.async.} =
|
||||
let
|
||||
signal = ThreadSignalPtr.new().expect("free file descriptor for signal")
|
||||
context = createShared(Context)
|
||||
context.signal = signal
|
||||
|
||||
var thread: Thread[ptr Context]
|
||||
|
||||
echo "Starting thread"
|
||||
createThread(thread, myThread, context)
|
||||
|
||||
await signal.wait()
|
||||
|
||||
echo "Work done: ", context.value
|
||||
|
||||
joinThread(thread)
|
||||
|
||||
signal.close().expect("closing once works")
|
||||
deallocShared(context)
|
||||
|
||||
waitFor main()
|
||||
25
docs/examples/timeoutcomposed.nim
Normal file
25
docs/examples/timeoutcomposed.nim
Normal file
@ -0,0 +1,25 @@
|
||||
## Single timeout for several operations
|
||||
import chronos
|
||||
|
||||
proc shortTask {.async.} =
|
||||
try:
|
||||
await sleepAsync(1.seconds)
|
||||
except CancelledError as exc:
|
||||
echo "Short task was cancelled!"
|
||||
raise exc # Propagate cancellation to the next operation
|
||||
|
||||
proc composedTimeout() {.async.} =
|
||||
let
|
||||
# Common timout for several sub-tasks
|
||||
timeout = sleepAsync(10.seconds)
|
||||
|
||||
while not timeout.finished():
|
||||
let task = shortTask() # Start a task but don't `await` it
|
||||
if (await race(task, timeout)) == task:
|
||||
echo "Ran one more task"
|
||||
else:
|
||||
# This cancellation may or may not happen as task might have finished
|
||||
# right at the timeout!
|
||||
task.cancelSoon()
|
||||
|
||||
waitFor composedTimeout()
|
||||
20
docs/examples/timeoutsimple.nim
Normal file
20
docs/examples/timeoutsimple.nim
Normal file
@ -0,0 +1,20 @@
|
||||
## Simple timeouts
|
||||
import chronos
|
||||
|
||||
proc longTask {.async.} =
|
||||
try:
|
||||
await sleepAsync(10.minutes)
|
||||
except CancelledError as exc:
|
||||
echo "Long task was cancelled!"
|
||||
raise exc # Propagate cancellation to the next operation
|
||||
|
||||
proc simpleTimeout() {.async.} =
|
||||
let
|
||||
task = longTask() # Start a task but don't `await` it
|
||||
|
||||
if not await task.withTimeout(1.seconds):
|
||||
echo "Timeout reached - withTimeout should have cancelled the task"
|
||||
else:
|
||||
echo "Task completed"
|
||||
|
||||
waitFor simpleTimeout()
|
||||
24
docs/examples/twogets.nim
Normal file
24
docs/examples/twogets.nim
Normal file
@ -0,0 +1,24 @@
|
||||
## Make two http requests concurrently and output the one that wins
|
||||
|
||||
import chronos
|
||||
import ./httpget
|
||||
|
||||
proc twoGets() {.async.} =
|
||||
let
|
||||
futs = @[
|
||||
# Both pages will start downloading concurrently...
|
||||
httpget.retrievePage("https://duckduckgo.com/?q=chronos"),
|
||||
httpget.retrievePage("https://www.google.fr/search?q=chronos")
|
||||
]
|
||||
|
||||
# Wait for at least one request to finish..
|
||||
let winner = await one(futs)
|
||||
# ..and cancel the others since we won't need them
|
||||
for fut in futs:
|
||||
# Trying to cancel an already-finished future is harmless
|
||||
fut.cancelSoon()
|
||||
|
||||
# An exception could be raised here if the winning request failed!
|
||||
echo "Result: ", winner.read()
|
||||
|
||||
waitFor(twoGets())
|
||||
7
docs/open-in.css
Normal file
7
docs/open-in.css
Normal file
@ -0,0 +1,7 @@
|
||||
footer {
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
border-top: 1px solid black;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
16
docs/src/SUMMARY.md
Normal file
16
docs/src/SUMMARY.md
Normal file
@ -0,0 +1,16 @@
|
||||
- [Introduction](./introduction.md)
|
||||
- [Examples](./examples.md)
|
||||
|
||||
# User guide
|
||||
|
||||
- [Core concepts](./concepts.md)
|
||||
- [`async` functions](async_procs.md)
|
||||
- [Errors and exceptions](./error_handling.md)
|
||||
- [Threads](./threads.md)
|
||||
- [Tips, tricks and best practices](./tips.md)
|
||||
- [Porting code to `chronos`](./porting.md)
|
||||
- [HTTP server middleware](./http_server_middleware.md)
|
||||
|
||||
# Developer guide
|
||||
|
||||
- [Updating this book](./book.md)
|
||||
123
docs/src/async_procs.md
Normal file
123
docs/src/async_procs.md
Normal file
@ -0,0 +1,123 @@
|
||||
# Async procedures
|
||||
|
||||
Async procedures are those that interact with `chronos` to cooperatively
|
||||
suspend and resume their execution depending on the completion of other
|
||||
async procedures, timers, tasks on other threads or asynchronous I/O scheduled
|
||||
with the operating system.
|
||||
|
||||
Async procedures are marked with the `{.async.}` pragma and return a `Future`
|
||||
indicating the state of the operation.
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## The `async` pragma
|
||||
|
||||
The `{.async.}` pragma will transform a procedure (or a method) returning a
|
||||
`Future` into a closure iterator. If there is no return type specified,
|
||||
`Future[void]` is returned.
|
||||
|
||||
```nim
|
||||
proc p() {.async.} =
|
||||
await sleepAsync(100.milliseconds)
|
||||
|
||||
echo p().type # prints "Future[system.void]"
|
||||
```
|
||||
|
||||
## `await` keyword
|
||||
|
||||
The `await` keyword operates on `Future` instances typically returned from an
|
||||
`async` procedure.
|
||||
|
||||
Whenever `await` is encountered inside an async procedure, control is given
|
||||
back to the dispatcher for as many steps as it's necessary for the awaited
|
||||
future to complete, fail or be cancelled. `await` calls the
|
||||
equivalent of `Future.read()` on the completed future to return the
|
||||
encapsulated value when the operation finishes.
|
||||
|
||||
```nim
|
||||
proc p1() {.async.} =
|
||||
await sleepAsync(1.seconds)
|
||||
|
||||
proc p2() {.async.} =
|
||||
await sleepAsync(1.seconds)
|
||||
|
||||
proc p3() {.async.} =
|
||||
let
|
||||
fut1 = p1()
|
||||
fut2 = p2()
|
||||
# Just by executing the async procs, both resulting futures entered the
|
||||
# dispatcher queue and their "clocks" started ticking.
|
||||
await fut1
|
||||
await fut2
|
||||
# Only one second passed while awaiting them both, not two.
|
||||
|
||||
waitFor p3()
|
||||
```
|
||||
|
||||
```admonition warning
|
||||
Because `async` procedures are executed concurrently, they are subject to many
|
||||
of the same risks that typically accompany multithreaded programming.
|
||||
|
||||
In particular, if two `async` procedures have access to the same mutable state,
|
||||
the value before and after `await` might not be the same as the order of execution is not guaranteed!
|
||||
```
|
||||
|
||||
## Raw async procedures
|
||||
|
||||
Raw async procedures are those that interact with `chronos` via the `Future`
|
||||
type but whose body does not go through the async transformation.
|
||||
|
||||
Such functions are created by adding `raw: true` to the `async` parameters:
|
||||
|
||||
```nim
|
||||
proc rawAsync(): Future[void] {.async: (raw: true).} =
|
||||
let fut = newFuture[void]("rawAsync")
|
||||
fut.complete()
|
||||
fut
|
||||
```
|
||||
|
||||
Raw functions must not raise exceptions directly - they are implicitly declared
|
||||
as `raises: []` - instead they should store exceptions in the returned `Future`:
|
||||
|
||||
```nim
|
||||
proc rawFailure(): Future[void] {.async: (raw: true).} =
|
||||
let fut = newFuture[void]("rawAsync")
|
||||
fut.fail((ref ValueError)(msg: "Oh no!"))
|
||||
fut
|
||||
```
|
||||
|
||||
Raw procedures can also use checked exceptions:
|
||||
|
||||
```nim
|
||||
proc rawAsyncRaises(): Future[void] {.async: (raw: true, raises: [IOError]).} =
|
||||
let fut = newFuture[void]()
|
||||
assert not (compiles do: fut.fail((ref ValueError)(msg: "uh-uh")))
|
||||
fut.fail((ref IOError)(msg: "IO"))
|
||||
fut
|
||||
```
|
||||
|
||||
## Callbacks and closures
|
||||
|
||||
Callback/closure types are declared using the `async` annotation as usual:
|
||||
|
||||
```nim
|
||||
type MyCallback = proc(): Future[void] {.async.}
|
||||
|
||||
proc runCallback(cb: MyCallback) {.async: (raises: []).} =
|
||||
try:
|
||||
await cb()
|
||||
except CatchableError:
|
||||
discard # handle errors as usual
|
||||
```
|
||||
|
||||
When calling a callback, it is important to remember that it may raise exceptions that need to be handled.
|
||||
|
||||
Checked exceptions can be used to limit the exceptions that a callback can
|
||||
raise:
|
||||
|
||||
```nim
|
||||
type MyEasyCallback = proc(): Future[void] {.async: (raises: []).}
|
||||
|
||||
proc runCallback(cb: MyEasyCallback) {.async: (raises: [])} =
|
||||
await cb()
|
||||
```
|
||||
214
docs/src/concepts.md
Normal file
214
docs/src/concepts.md
Normal file
@ -0,0 +1,214 @@
|
||||
# Concepts
|
||||
|
||||
Async/await is a programming model that relies on cooperative multitasking to
|
||||
coordinate the concurrent execution of procedures, using event notifications
|
||||
from the operating system or other treads to resume execution.
|
||||
|
||||
Code execution happens in a loop that alternates between making progress on
|
||||
tasks and handling events.
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## The dispatcher
|
||||
|
||||
The event handler loop is called a "dispatcher" and a single instance per
|
||||
thread is created, as soon as one is needed.
|
||||
|
||||
Scheduling is done by calling [async procedures](./async_procs.md) that return
|
||||
`Future` objects - each time a procedure is unable to make further
|
||||
progress, for example because it's waiting for some data to arrive, it hands
|
||||
control back to the dispatcher which ensures that the procedure is resumed when
|
||||
ready.
|
||||
|
||||
A single thread, and thus a single dispatcher, is typically able to handle
|
||||
thousands of concurrent in-progress requests.
|
||||
|
||||
## The `Future` type
|
||||
|
||||
`Future` objects encapsulate the outcome of executing an `async` procedure. The
|
||||
`Future` may be `pending` meaning that the outcome is not yet known or
|
||||
`finished` meaning that the return value is available, the operation failed
|
||||
with an exception or was cancelled.
|
||||
|
||||
Inside an async procedure, you can `await` the outcome of another async
|
||||
procedure - if the `Future` representing that operation is still `pending`, a
|
||||
callback representing where to resume execution will be added to it and the
|
||||
dispatcher will be given back control to deal with other tasks.
|
||||
|
||||
When a `Future` is `finished`, all its callbacks are scheduled to be run by
|
||||
the dispatcher, thus continuing any operations that were waiting for an outcome.
|
||||
|
||||
## The `poll` call
|
||||
|
||||
To trigger the processing step of the dispatcher, we need to call `poll()` -
|
||||
either directly or through a wrapper like `runForever()` or `waitFor()`.
|
||||
|
||||
Each call to poll handles any file descriptors, timers and callbacks that are
|
||||
ready to be processed.
|
||||
|
||||
Using `waitFor`, the result of a single asynchronous operation can be obtained:
|
||||
|
||||
```nim
|
||||
proc myApp() {.async.} =
|
||||
echo "Waiting for a second..."
|
||||
await sleepAsync(1.seconds)
|
||||
echo "done!"
|
||||
|
||||
waitFor myApp()
|
||||
```
|
||||
|
||||
It is also possible to keep running the event loop forever using `runForever`:
|
||||
|
||||
```nim
|
||||
proc myApp() {.async.} =
|
||||
while true:
|
||||
await sleepAsync(1.seconds)
|
||||
echo "A bit more than a second passed!"
|
||||
|
||||
let future = myApp()
|
||||
runForever()
|
||||
```
|
||||
|
||||
Such an application never terminates, thus it is rare that applications are
|
||||
structured this way.
|
||||
|
||||
```admonish warning
|
||||
Both `waitFor` and `runForever` call `poll` which offers fine-grained
|
||||
control over the event loop steps.
|
||||
|
||||
Nested calls to `poll` - directly or indirectly via `waitFor` and `runForever`
|
||||
are not allowed.
|
||||
```
|
||||
|
||||
## Cancellation
|
||||
|
||||
Any pending `Future` can be cancelled. This can be used for timeouts, to start
|
||||
multiple parallel operations and cancel the rest as soon as one finishes,
|
||||
to initiate the orderely shutdown of an application etc.
|
||||
|
||||
```nim
|
||||
{{#include ../examples/cancellation.nim}}
|
||||
```
|
||||
|
||||
Even if cancellation is initiated, it is not guaranteed that the operation gets
|
||||
cancelled - the future might still be completed or fail depending on the
|
||||
order of events in the dispatcher and the specifics of the operation.
|
||||
|
||||
If the future indeed gets cancelled, `await` will raise a
|
||||
`CancelledError` as is likely to happen in the following example:
|
||||
|
||||
```nim
|
||||
proc c1 {.async.} =
|
||||
echo "Before sleep"
|
||||
try:
|
||||
await sleepAsync(10.minutes)
|
||||
echo "After sleep" # not reach due to cancellation
|
||||
except CancelledError as exc:
|
||||
echo "We got cancelled!"
|
||||
# `CancelledError` is typically re-raised to notify the caller that the
|
||||
# operation is being cancelled
|
||||
raise exc
|
||||
|
||||
proc c2 {.async.} =
|
||||
await c1()
|
||||
echo "Never reached, since the CancelledError got re-raised"
|
||||
|
||||
let work = c2()
|
||||
waitFor(work.cancelAndWait())
|
||||
```
|
||||
|
||||
The `CancelledError` will now travel up the stack like any other exception.
|
||||
It can be caught for instance to free some resources and is then typically
|
||||
re-raised for the whole chain operations to get cancelled.
|
||||
|
||||
Alternatively, the cancellation request can be translated to a regular outcome
|
||||
of the operation - for example, a `read` operation might return an empty result.
|
||||
|
||||
Cancelling an already-finished `Future` has no effect, as the following example
|
||||
of downloading two web pages concurrently shows:
|
||||
|
||||
```nim
|
||||
{{#include ../examples/twogets.nim}}
|
||||
```
|
||||
|
||||
### Ownership
|
||||
|
||||
When calling a procedure that returns a `Future`, ownership of that `Future` is
|
||||
shared between the callee that created it and the caller that waits for it to be
|
||||
finished.
|
||||
|
||||
The `Future` can be thought of as a single-item channel between a producer and a
|
||||
consumer. The producer creates the `Future` and is responsible for completing or
|
||||
failing it while the caller waits for completion and may `cancel` it.
|
||||
|
||||
Although it is technically possible, callers must not `complete` or `fail`
|
||||
futures and callees or other intermediate observers must not `cancel` them as
|
||||
this may lead to panics and shutdown (ie if the future is completed twice or a
|
||||
cancalletion is not handled by the original caller).
|
||||
|
||||
### `noCancel`
|
||||
|
||||
Certain operations must not be cancelled for semantic reasons. Common scenarios
|
||||
include `closeWait` that releases a resources irrevocably and composed
|
||||
operations whose individual steps should be performed together or not at all.
|
||||
|
||||
In such cases, the `noCancel` modifier to `await` can be used to temporarily
|
||||
disable cancellation propagation, allowing the operation to complete even if
|
||||
the caller initiates a cancellation request:
|
||||
|
||||
```nim
|
||||
proc deepSleep(dur: Duration) {.async.} =
|
||||
# `noCancel` prevents any cancellation request by the caller of `deepSleep`
|
||||
# from reaching `sleepAsync` - even if `deepSleep` is cancelled, its future
|
||||
# will not complete until the sleep finishes.
|
||||
await noCancel sleepAsync(dur)
|
||||
|
||||
let future = deepSleep(10.minutes)
|
||||
|
||||
# This will take ~10 minutes even if we try to cancel the call to `deepSleep`!
|
||||
await cancelAndWait(future)
|
||||
```
|
||||
|
||||
### `join`
|
||||
|
||||
The `join` modifier to `await` allows cancelling an `async` procedure without
|
||||
propagating the cancellation to the awaited operation. This is useful when
|
||||
`await`:ing a `Future` for monitoring purposes, ie when a procedure is not the
|
||||
owner of the future that's being `await`:ed.
|
||||
|
||||
One situation where this happens is when implementing the "observer" pattern,
|
||||
where a helper monitors an operation it did not initiate:
|
||||
|
||||
```nim
|
||||
var tick: Future[void]
|
||||
proc ticker() {.async.} =
|
||||
while true:
|
||||
tick = sleepAsync(1.second)
|
||||
await tick
|
||||
echo "tick!"
|
||||
|
||||
proc tocker() {.async.} =
|
||||
# This operation does not own or implement the operation behind `tick`,
|
||||
# so it should not cancel it when `tocker` is cancelled
|
||||
await join tick
|
||||
echo "tock!"
|
||||
|
||||
let
|
||||
fut = ticker() # `ticker` is now looping and most likely waiting for `tick`
|
||||
fut2 = tocker() # both `ticker` and `tocker` are waiting for `tick`
|
||||
|
||||
# We don't want `tocker` to cancel a future that was created in `ticker`
|
||||
waitFor fut2.cancelAndWait()
|
||||
|
||||
waitFor fut # keeps printing `tick!` every second.
|
||||
```
|
||||
|
||||
## Compile-time configuration
|
||||
|
||||
`chronos` contains several compile-time
|
||||
[configuration options](./chronos/config.nim) enabling stricter compile-time
|
||||
checks and debugging helpers whose runtime cost may be significant.
|
||||
|
||||
Strictness options generally will become default in future chronos releases and
|
||||
allow adapting existing code without changing the new version - see the
|
||||
[`config.nim`](./chronos/config.nim) module for more information.
|
||||
168
docs/src/error_handling.md
Normal file
168
docs/src/error_handling.md
Normal file
@ -0,0 +1,168 @@
|
||||
# Errors and exceptions
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Exceptions
|
||||
|
||||
Exceptions inheriting from [`CatchableError`](https://nim-lang.org/docs/system.html#CatchableError)
|
||||
interrupt execution of an `async` procedure. The exception is placed in the
|
||||
`Future.error` field while changing the status of the `Future` to `Failed`
|
||||
and callbacks are scheduled.
|
||||
|
||||
When a future is read or awaited the exception is re-raised, traversing the
|
||||
`async` execution chain until handled.
|
||||
|
||||
```nim
|
||||
proc p1() {.async.} =
|
||||
await sleepAsync(1.seconds)
|
||||
raise newException(ValueError, "ValueError inherits from CatchableError")
|
||||
|
||||
proc p2() {.async.} =
|
||||
await sleepAsync(1.seconds)
|
||||
|
||||
proc p3() {.async.} =
|
||||
let
|
||||
fut1 = p1()
|
||||
fut2 = p2()
|
||||
await fut1
|
||||
echo "unreachable code here"
|
||||
await fut2
|
||||
|
||||
# `waitFor()` would call `Future.read()` unconditionally, which would raise the
|
||||
# exception in `Future.error`.
|
||||
let fut3 = p3()
|
||||
while not(fut3.finished()):
|
||||
poll()
|
||||
|
||||
echo "fut3.state = ", fut3.state # "Failed"
|
||||
if fut3.failed():
|
||||
echo "p3() failed: ", fut3.error.name, ": ", fut3.error.msg
|
||||
# prints "p3() failed: ValueError: ValueError inherits from CatchableError"
|
||||
```
|
||||
|
||||
You can put the `await` in a `try` block, to deal with that exception sooner:
|
||||
|
||||
```nim
|
||||
proc p3() {.async.} =
|
||||
let
|
||||
fut1 = p1()
|
||||
fut2 = p2()
|
||||
try:
|
||||
await fut1
|
||||
except CachableError:
|
||||
echo "p1() failed: ", fut1.error.name, ": ", fut1.error.msg
|
||||
echo "reachable code here"
|
||||
await fut2
|
||||
```
|
||||
|
||||
Because `chronos` ensures that all exceptions are re-routed to the `Future`,
|
||||
`poll` will not itself raise exceptions.
|
||||
|
||||
`poll` may still panic / raise `Defect` if such are raised in user code due to
|
||||
undefined behavior.
|
||||
|
||||
## Checked exceptions
|
||||
|
||||
By specifying a `raises` list to an async procedure, you can check which
|
||||
exceptions can be raised by it:
|
||||
|
||||
```nim
|
||||
proc p1(): Future[void] {.async: (raises: [IOError]).} =
|
||||
assert not (compiles do: raise newException(ValueError, "uh-uh"))
|
||||
raise newException(IOError, "works") # Or any child of IOError
|
||||
|
||||
proc p2(): Future[void] {.async, (raises: [IOError]).} =
|
||||
await p1() # Works, because await knows that p1
|
||||
# can only raise IOError
|
||||
```
|
||||
|
||||
Under the hood, the return type of `p1` will be rewritten to an internal type
|
||||
which will convey raises informations to `await`.
|
||||
|
||||
```admonition note
|
||||
Most `async` include `CancelledError` in the list of `raises`, indicating that
|
||||
the operation they implement might get cancelled resulting in neither value nor
|
||||
error!
|
||||
```
|
||||
|
||||
When using checked exceptions, the `Future` type is modified to include
|
||||
`raises` information - it can be constructed with the `Raising` helper:
|
||||
|
||||
```nim
|
||||
# Create a variable of the type that will be returned by a an async function
|
||||
# raising `[CancelledError]`:
|
||||
var fut: Future[int].Raising([CancelledError])
|
||||
```
|
||||
|
||||
```admonition note
|
||||
`Raising` creates a specialization of `InternalRaisesFuture` type - as the name
|
||||
suggests, this is an internal type whose implementation details are likely to
|
||||
change in future `chronos` versions.
|
||||
```
|
||||
|
||||
## The `Exception` type
|
||||
|
||||
Exceptions deriving from `Exception` are not caught by default as these may
|
||||
include `Defect` and other forms undefined or uncatchable behavior.
|
||||
|
||||
Because exception effect tracking is turned on for `async` functions, this may
|
||||
sometimes lead to compile errors around forward declarations, methods and
|
||||
closures as Nim conservatively asssumes that any `Exception` might be raised
|
||||
from those.
|
||||
|
||||
Make sure to explicitly annotate these with `{.raises.}`:
|
||||
|
||||
```nim
|
||||
# Forward declarations need to explicitly include a raises list:
|
||||
proc myfunction() {.raises: [ValueError].}
|
||||
|
||||
# ... as do `proc` types
|
||||
type MyClosure = proc() {.raises: [ValueError].}
|
||||
|
||||
proc myfunction() =
|
||||
raise (ref ValueError)(msg: "Implementation here")
|
||||
|
||||
let closure: MyClosure = myfunction
|
||||
```
|
||||
## Compatibility modes
|
||||
|
||||
**Individual functions.** For compatibility, `async` functions can be instructed
|
||||
to handle `Exception` as well, specifying `handleException: true`. Any
|
||||
`Exception` that is not a `Defect` and not a `CatchableError` will then be
|
||||
caught and remapped to `AsyncExceptionError`:
|
||||
|
||||
```nim
|
||||
proc raiseException() {.async: (handleException: true, raises: [AsyncExceptionError]).} =
|
||||
raise (ref Exception)(msg: "Raising Exception is UB")
|
||||
|
||||
proc callRaiseException() {.async: (raises: []).} =
|
||||
try:
|
||||
await raiseException()
|
||||
except AsyncExceptionError as exc:
|
||||
# The original Exception is available from the `parent` field
|
||||
echo exc.parent.msg
|
||||
```
|
||||
|
||||
**Global flag.** This mode can be enabled globally with
|
||||
`-d:chronosHandleException` as a help when porting code to `chronos`. The
|
||||
behavior in this case will be that:
|
||||
|
||||
1. old-style functions annotated with plain `async` will behave as if they had
|
||||
been annotated with `async: (handleException: true)`.
|
||||
|
||||
This is functionally equivalent to
|
||||
`async: (handleException: true, raises: [CatchableError])` and will, as
|
||||
before, remap any `Exception` that is not `Defect` into
|
||||
`AsyncExceptionError`, while also allowing any `CatchableError` (including
|
||||
`AsyncExceptionError`) to get through without compilation errors.
|
||||
|
||||
2. New-style functions with `async: (raises: [...])` annotations or their own
|
||||
`handleException` annotations will not be affected.
|
||||
|
||||
The rationale here is to allow one to incrementally introduce exception
|
||||
annotations and get compiler feedback while not requiring that every bit of
|
||||
legacy code is updated at once.
|
||||
|
||||
This should be used sparingly and with care, however, as global configuration
|
||||
settings may interfere with libraries that use `chronos` leading to unexpected
|
||||
behavior.
|
||||
23
docs/src/examples.md
Normal file
23
docs/src/examples.md
Normal file
@ -0,0 +1,23 @@
|
||||
# Examples
|
||||
|
||||
Examples are available in the [`docs/examples/`](https://github.com/status-im/nim-chronos/tree/master/docs/examples/) folder.
|
||||
|
||||
## Basic concepts
|
||||
|
||||
* [cancellation](https://github.com/status-im/nim-chronos/tree/master/docs/examples/cancellation.nim) - Cancellation primer
|
||||
* [timeoutsimple](https://github.com/status-im/nim-chronos/tree/master/docs/examples/timeoutsimple.nim) - Simple timeouts
|
||||
* [timeoutcomposed](https://github.com/status-im/nim-chronos/tree/master/docs/examples/examples/timeoutcomposed.nim) - Shared timeout of multiple tasks
|
||||
|
||||
## Threads
|
||||
|
||||
* [signalling](https://github.com/status-im/nim-chronos/tree/master/docs/examples/signalling.nim) - Cross-thread signalling
|
||||
|
||||
## TCP
|
||||
|
||||
* [tcpserver](https://github.com/status-im/nim-chronos/tree/master/docs/examples/tcpserver.nim) - Simple TCP/IP v4/v6 echo server
|
||||
|
||||
## HTTP
|
||||
|
||||
* [httpget](https://github.com/status-im/nim-chronos/tree/master/docs/examples/httpget.nim) - Downloading a web page using the http client
|
||||
* [twogets](https://github.com/status-im/nim-chronos/tree/master/docs/examples/twogets.nim) - Download two pages concurrently
|
||||
* [middleware](https://github.com/status-im/nim-chronos/tree/master/docs/examples/middleware.nim) - Deploy multiple HTTP server middlewares
|
||||
19
docs/src/getting_started.md
Normal file
19
docs/src/getting_started.md
Normal file
@ -0,0 +1,19 @@
|
||||
## Getting started
|
||||
|
||||
Install `chronos` using `nimble`:
|
||||
|
||||
```text
|
||||
nimble install chronos
|
||||
```
|
||||
|
||||
or add a dependency to your `.nimble` file:
|
||||
|
||||
```text
|
||||
requires "chronos"
|
||||
```
|
||||
|
||||
and start using it:
|
||||
|
||||
```nim
|
||||
{{#include ../examples/httpget.nim}}
|
||||
```
|
||||
102
docs/src/http_server_middleware.md
Normal file
102
docs/src/http_server_middleware.md
Normal file
@ -0,0 +1,102 @@
|
||||
## HTTP server middleware
|
||||
|
||||
Chronos provides a powerful mechanism for customizing HTTP request handlers via
|
||||
middlewares.
|
||||
|
||||
A middleware is a coroutine that can modify, block or filter HTTP request.
|
||||
|
||||
Single HTTP server could support unlimited number of middlewares, but you need to consider that each request in worst case could go through all the middlewares, and therefore a huge number of middlewares can have a significant impact on HTTP server performance.
|
||||
|
||||
Order of middlewares is also important: right after HTTP server has received request, it will be sent to the first middleware in list, and each middleware will be responsible for passing control to other middlewares. Therefore, when building a list, it would be a good idea to place the request handlers at the end of the list, while keeping the middleware that could block or modify the request at the beginning of the list.
|
||||
|
||||
Middleware could also modify HTTP server request, and these changes will be visible to all handlers (either middlewares or the original request handler). This can be done using the following helpers:
|
||||
|
||||
```nim
|
||||
proc updateRequest*(request: HttpRequestRef, scheme: string, meth: HttpMethod,
|
||||
version: HttpVersion, requestUri: string,
|
||||
headers: HttpTable): HttpResultMessage[void]
|
||||
|
||||
proc updateRequest*(request: HttpRequestRef, meth: HttpMethod,
|
||||
requestUri: string,
|
||||
headers: HttpTable): HttpResultMessage[void]
|
||||
|
||||
proc updateRequest*(request: HttpRequestRef, requestUri: string,
|
||||
headers: HttpTable): HttpResultMessage[void]
|
||||
|
||||
proc updateRequest*(request: HttpRequestRef,
|
||||
requestUri: string): HttpResultMessage[void]
|
||||
|
||||
proc updateRequest*(request: HttpRequestRef,
|
||||
headers: HttpTable): HttpResultMessage[void]
|
||||
```
|
||||
|
||||
As you can see all the HTTP request parameters could be modified: request method, version, request path and request headers.
|
||||
|
||||
Middleware could also use helpers to obtain more information about remote and local addresses of request's connection (this could be helpful when you need to do some IP address filtering).
|
||||
|
||||
```nim
|
||||
proc remote*(request: HttpRequestRef): Opt[TransportAddress]
|
||||
## Returns remote address of HTTP request's connection.
|
||||
proc local*(request: HttpRequestRef): Opt[TransportAddress] =
|
||||
## Returns local address of HTTP request's connection.
|
||||
```
|
||||
|
||||
Every middleware is the coroutine which looks like this:
|
||||
|
||||
```nim
|
||||
proc middlewareHandler(
|
||||
middleware: HttpServerMiddlewareRef,
|
||||
reqfence: RequestFence,
|
||||
nextHandler: HttpProcessCallback2
|
||||
): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
|
||||
```
|
||||
|
||||
Where `middleware` argument is the object which could hold some specific values, `reqfence` is HTTP request which is enclosed with HTTP server error information and `nextHandler` is reference to next request handler, it could be either middleware handler or the original request processing callback handler.
|
||||
|
||||
```nim
|
||||
await nextHandler(reqfence)
|
||||
```
|
||||
|
||||
You should perform await for the response from the `nextHandler(reqfence)`. Usually you should call next handler when you dont want to handle request or you dont know how to handle it, for example:
|
||||
|
||||
```nim
|
||||
proc middlewareHandler(
|
||||
middleware: HttpServerMiddlewareRef,
|
||||
reqfence: RequestFence,
|
||||
nextHandler: HttpProcessCallback2
|
||||
): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
|
||||
if reqfence.isErr():
|
||||
# We dont know or do not want to handle failed requests, so we call next handler.
|
||||
return await nextHandler(reqfence)
|
||||
let request = reqfence.get()
|
||||
if request.uri.path == "/path/we/able/to/respond":
|
||||
try:
|
||||
# Sending some response.
|
||||
await request.respond(Http200, "TEST")
|
||||
except HttpWriteError as exc:
|
||||
# We could also return default response for exception or other types of error.
|
||||
defaultResponse(exc)
|
||||
elif request.uri.path == "/path/for/rewrite":
|
||||
# We going to modify request object for this request, next handler will receive it with different request path.
|
||||
let res = request.updateRequest("/path/to/new/location")
|
||||
if res.isErr():
|
||||
return defaultResponse(res.error)
|
||||
await nextHandler(reqfence)
|
||||
elif request.uri.path == "/restricted/path":
|
||||
if request.remote().isNone():
|
||||
# We can't obtain remote address, so we force HTTP server to respond with `401 Unauthorized` status code.
|
||||
return codeResponse(Http401)
|
||||
if $(request.remote().get()).startsWith("127.0.0.1"):
|
||||
# Remote peer's address starts with "127.0.0.1", sending proper response.
|
||||
await request.respond(Http200, "AUTHORIZED")
|
||||
else:
|
||||
# Force HTTP server to respond with `403 Forbidden` status code.
|
||||
codeResponse(Http403)
|
||||
elif request.uri.path == "/blackhole":
|
||||
# Force HTTP server to drop connection with remote peer.
|
||||
dropResponse()
|
||||
else:
|
||||
# All other requests should be handled by somebody else.
|
||||
await nextHandler(reqfence)
|
||||
```
|
||||
|
||||
50
docs/src/introduction.md
Normal file
50
docs/src/introduction.md
Normal file
@ -0,0 +1,50 @@
|
||||
# Introduction
|
||||
|
||||
Chronos implements the [async/await](https://en.wikipedia.org/wiki/Async/await)
|
||||
paradigm in a self-contained library using macro and closure iterator
|
||||
transformation features provided by Nim.
|
||||
|
||||
Features include:
|
||||
|
||||
* Asynchronous socket and process I/O
|
||||
* HTTP client / server with SSL/TLS support out of the box (no OpenSSL needed)
|
||||
* Synchronization primitivies like queues, events and locks
|
||||
* [Cancellation](./concepts.md#cancellation)
|
||||
* Efficient dispatch pipeline with excellent multi-platform support
|
||||
* Exception [effect support](./guide.md#error-handling)
|
||||
|
||||
## Installation
|
||||
|
||||
Install `chronos` using `nimble`:
|
||||
|
||||
```text
|
||||
nimble install chronos
|
||||
```
|
||||
|
||||
or add a dependency to your `.nimble` file:
|
||||
|
||||
```text
|
||||
requires "chronos"
|
||||
```
|
||||
|
||||
and start using it:
|
||||
|
||||
```nim
|
||||
{{#include ../examples/httpget.nim}}
|
||||
```
|
||||
|
||||
There are more [examples](./examples.md) throughout the manual!
|
||||
|
||||
## Platform support
|
||||
|
||||
Several platforms are supported, with different backend [options](./concepts.md#compile-time-configuration):
|
||||
|
||||
* Windows: [`IOCP`](https://learn.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports)
|
||||
* Linux: [`epoll`](https://en.wikipedia.org/wiki/Epoll) / `poll`
|
||||
* OSX / BSD: [`kqueue`](https://en.wikipedia.org/wiki/Kqueue) / `poll`
|
||||
* Android / Emscripten / posix: `poll`
|
||||
|
||||
## API documentation
|
||||
|
||||
This guide covers basic usage of chronos - for details, see the
|
||||
[API reference](./api/chronos.html).
|
||||
59
docs/src/porting.md
Normal file
59
docs/src/porting.md
Normal file
@ -0,0 +1,59 @@
|
||||
# Porting code to `chronos` v4
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
Thanks to its macro support, Nim allows `async`/`await` to be implemented in
|
||||
libraries with only minimal support from the language - as such, multiple
|
||||
`async` libraries exist, including `chronos` and `asyncdispatch`, and more may
|
||||
come to be developed in the futures.
|
||||
|
||||
## Chronos v3
|
||||
|
||||
Chronos v4 introduces new features for IPv6, exception effects, a stand-alone
|
||||
`Future` type as well as several other changes - when upgrading from chronos v3,
|
||||
here are several things to consider:
|
||||
|
||||
* Exception handling is now strict by default - see the [error handling](./error_handling.md)
|
||||
chapter for how to deal with `raises` effects
|
||||
* `AsyncEventBus` was removed - use `AsyncEventQueue` instead
|
||||
* `Future.value` and `Future.error` panic when accessed in the wrong state
|
||||
* `Future.read` and `Future.readError` raise `FutureError` instead of
|
||||
`ValueError` when accessed in the wrong state
|
||||
|
||||
## `asyncdispatch`
|
||||
|
||||
Code written for `asyncdispatch` and `chronos` looks similar but there are
|
||||
several differences to be aware of:
|
||||
|
||||
* `chronos` has its own dispatch loop - you can typically not mix `chronos` and
|
||||
`asyncdispatch` in the same thread
|
||||
* `import chronos` instead of `import asyncdispatch`
|
||||
* cleanup is important - make sure to use `closeWait` to release any resources
|
||||
you're using or file descriptor and other leaks will ensue
|
||||
* cancellation support means that `CancelledError` may be raised from most
|
||||
`{.async.}` functions
|
||||
* Calling `yield` directly in tasks is not supported - instead, use `awaitne`.
|
||||
* `asyncSpawn` is used instead of `asyncCheck` - note that exceptions raised
|
||||
in tasks that are `asyncSpawn`:ed cause panic
|
||||
|
||||
## Supporting multiple backends
|
||||
|
||||
Libraries built on top of `async`/`await` may wish to support multiple async
|
||||
backends - the best way to do so is to create separate modules for each backend
|
||||
that may be imported side-by-side - see [nim-metrics](https://github.com/status-im/nim-metrics/blob/master/metrics/)
|
||||
for an example.
|
||||
|
||||
An alternative way is to select backend using a global compile flag - this
|
||||
method makes it diffucult to compose applications that use both backends as may
|
||||
happen with transitive dependencies, but may be appropriate in some cases -
|
||||
libraries choosing this path should call the flag `asyncBackend`, allowing
|
||||
applications to choose the backend with `-d:asyncBackend=<backend_name>`.
|
||||
|
||||
Known `async` backends include:
|
||||
|
||||
* `chronos` - this library (`-d:asyncBackend=chronos`)
|
||||
* `asyncdispatch` the standard library `asyncdispatch` [module](https://nim-lang.org/docs/asyncdispatch.html) (`-d:asyncBackend=asyncdispatch`)
|
||||
* `none` - ``-d:asyncBackend=none`` - disable ``async`` support completely
|
||||
|
||||
``none`` can be used when a library supports both a synchronous and
|
||||
asynchronous API, to disable the latter.
|
||||
18
docs/src/threads.md
Normal file
18
docs/src/threads.md
Normal file
@ -0,0 +1,18 @@
|
||||
# Threads
|
||||
|
||||
While the cooperative [`async`](./concepts.md) model offers an efficient model
|
||||
for dealing with many tasks that often are blocked on I/O, it is not suitable
|
||||
for long-running computations that would prevent concurrent tasks from progressing.
|
||||
|
||||
Multithreading offers a way to offload heavy computations to be executed in
|
||||
parallel with the async work, or, in cases where a single event loop gets
|
||||
overloaded, to manage multiple event loops in parallel.
|
||||
|
||||
For interaction between threads, the `ThreadSignalPtr` type (found in the
|
||||
(`chronos/threadsync`)(https://github.com/status-im/nim-chronos/blob/master/chronos/threadsync.nim)
|
||||
module) is used - both to wait for notifications coming from other threads and
|
||||
to notify other threads of progress from within an async procedure.
|
||||
|
||||
```nim
|
||||
{{#include ../examples/signalling.nim}}
|
||||
```
|
||||
34
docs/src/tips.md
Normal file
34
docs/src/tips.md
Normal file
@ -0,0 +1,34 @@
|
||||
# Tips, tricks and best practices
|
||||
|
||||
## Timeouts
|
||||
|
||||
To prevent a single task from taking too long, `withTimeout` can be used:
|
||||
|
||||
```nim
|
||||
{{#include ../examples/timeoutsimple.nim}}
|
||||
```
|
||||
|
||||
When several tasks should share a single timeout, a common timer can be created
|
||||
with `sleepAsync`:
|
||||
|
||||
```nim
|
||||
{{#include ../examples/timeoutcomposed.nim}}
|
||||
```
|
||||
|
||||
## `discard`
|
||||
|
||||
When calling an asynchronous procedure without `await`, the operation is started
|
||||
but its result is not processed until corresponding `Future` is `read`.
|
||||
|
||||
It is therefore important to never `discard` futures directly - instead, one
|
||||
can discard the result of awaiting the future or use `asyncSpawn` to monitor
|
||||
the outcome of the future as if it were running in a separate thread.
|
||||
|
||||
Similar to threads, tasks managed by `asyncSpawn` may causes the application to
|
||||
crash if any exceptions leak out of it - use
|
||||
[checked exceptions](./error_handling.md#checked-exceptions) to avoid this
|
||||
problem.
|
||||
|
||||
```nim
|
||||
{{#include ../examples/discards.nim}}
|
||||
```
|
||||
@ -5,10 +5,22 @@
|
||||
# Licensed under either of
|
||||
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
||||
# MIT license (LICENSE-MIT)
|
||||
import testmacro, testsync, testsoon, testtime, testfut, testsignal,
|
||||
testaddress, testdatagram, teststream, testserver, testbugs, testnet,
|
||||
testasyncstream, testhttpserver, testshttpserver, testhttpclient,
|
||||
testproc, testratelimit, testfutures, testthreadsync
|
||||
import ".."/chronos/config
|
||||
|
||||
# Must be imported last to check for Pending futures
|
||||
import testutils
|
||||
when (chronosEventEngine in ["epoll", "kqueue"]) or defined(windows):
|
||||
import testmacro, testsync, testsoon, testtime, testfut, testsignal,
|
||||
testaddress, testdatagram, teststream, testserver, testbugs, testnet,
|
||||
testasyncstream, testhttpserver, testshttpserver, testhttpclient,
|
||||
testproc, testratelimit, testfutures, testthreadsync
|
||||
|
||||
# Must be imported last to check for Pending futures
|
||||
import testutils
|
||||
elif chronosEventEngine == "poll":
|
||||
# `poll` engine do not support signals and processes
|
||||
import testmacro, testsync, testsoon, testtime, testfut, testaddress,
|
||||
testdatagram, teststream, testserver, testbugs, testnet,
|
||||
testasyncstream, testhttpserver, testshttpserver, testhttpclient,
|
||||
testratelimit, testfutures, testthreadsync
|
||||
|
||||
# Must be imported last to check for Pending futures
|
||||
import testutils
|
||||
|
||||
63
tests/testasyncstream.c
Normal file
63
tests/testasyncstream.c
Normal file
@ -0,0 +1,63 @@
|
||||
#include <brssl.h>
|
||||
|
||||
// This is the X509TrustAnchor for the SelfSignedRsaCert above
|
||||
// Generate by doing the following:
|
||||
// 1. Compile `brssl` from BearSSL
|
||||
// 2. Run `brssl ta filewithSelfSignedRsaCert.pem`
|
||||
// 3. Paste the output in the emit block below
|
||||
// 4. Rename `TAs` to `SelfSignedTAs`
|
||||
|
||||
static const unsigned char TA0_DN[] = {
|
||||
0x30, 0x5F, 0x31, 0x0B, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13,
|
||||
0x02, 0x41, 0x55, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x08,
|
||||
0x0C, 0x0A, 0x53, 0x6F, 0x6D, 0x65, 0x2D, 0x53, 0x74, 0x61, 0x74, 0x65,
|
||||
0x31, 0x21, 0x30, 0x1F, 0x06, 0x03, 0x55, 0x04, 0x0A, 0x0C, 0x18, 0x49,
|
||||
0x6E, 0x74, 0x65, 0x72, 0x6E, 0x65, 0x74, 0x20, 0x57, 0x69, 0x64, 0x67,
|
||||
0x69, 0x74, 0x73, 0x20, 0x50, 0x74, 0x79, 0x20, 0x4C, 0x74, 0x64, 0x31,
|
||||
0x18, 0x30, 0x16, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, 0x0F, 0x31, 0x32,
|
||||
0x37, 0x2E, 0x30, 0x2E, 0x30, 0x2E, 0x31, 0x3A, 0x34, 0x33, 0x38, 0x30,
|
||||
0x38
|
||||
};
|
||||
|
||||
static const unsigned char TA0_RSA_N[] = {
|
||||
0xA7, 0xEE, 0xD5, 0xC6, 0x2C, 0xA3, 0x08, 0x33, 0x33, 0x86, 0xB5, 0x5C,
|
||||
0xD4, 0x8B, 0x16, 0xB1, 0xD7, 0xF7, 0xED, 0x95, 0x22, 0xDC, 0xA4, 0x40,
|
||||
0x24, 0x64, 0xC3, 0x91, 0xBA, 0x20, 0x82, 0x9D, 0x88, 0xED, 0x20, 0x98,
|
||||
0x46, 0x65, 0xDC, 0xD1, 0x15, 0x90, 0xBC, 0x7C, 0x19, 0x5F, 0x00, 0x96,
|
||||
0x69, 0x2C, 0x80, 0x0E, 0x7D, 0x7D, 0x8B, 0xD9, 0xFD, 0x49, 0x66, 0xEC,
|
||||
0x29, 0xC0, 0x39, 0x0E, 0x22, 0xF3, 0x6A, 0x28, 0xC0, 0x6B, 0x97, 0x93,
|
||||
0x2F, 0x92, 0x5E, 0x5A, 0xCC, 0xF4, 0xF4, 0xAE, 0xD9, 0xE3, 0xBB, 0x0A,
|
||||
0xDC, 0xA8, 0xDE, 0x4D, 0x16, 0xD6, 0xE6, 0x64, 0xF2, 0x85, 0x62, 0xF6,
|
||||
0xE3, 0x7B, 0x1D, 0x9A, 0x5C, 0x6A, 0xA3, 0x97, 0x93, 0x16, 0x9D, 0x02,
|
||||
0x2C, 0xFD, 0x90, 0x3E, 0xF8, 0x35, 0x44, 0x5E, 0x66, 0x8D, 0xF6, 0x80,
|
||||
0xF1, 0x71, 0x9B, 0x2F, 0x44, 0xC0, 0xCA, 0x7E, 0xB1, 0x90, 0x7F, 0xD8,
|
||||
0x8B, 0x7A, 0x85, 0x4B, 0xE3, 0xB1, 0xB1, 0xF4, 0xAA, 0x6A, 0x36, 0xA0,
|
||||
0xFF, 0x24, 0xB2, 0x27, 0xE0, 0xBA, 0x62, 0x7A, 0xE9, 0x95, 0xC9, 0x88,
|
||||
0x9D, 0x9B, 0xAB, 0xA4, 0x4C, 0xEA, 0x87, 0x46, 0xFA, 0xD6, 0x9B, 0x7E,
|
||||
0xB2, 0xE9, 0x5B, 0xCA, 0x5B, 0x84, 0xC4, 0xF7, 0xB4, 0xC7, 0x69, 0xC5,
|
||||
0x0B, 0x9A, 0x47, 0x9A, 0x86, 0xD4, 0xDF, 0xF3, 0x30, 0xC9, 0x6D, 0xB8,
|
||||
0x78, 0x10, 0xEF, 0xA0, 0x89, 0xF8, 0x30, 0x80, 0x9D, 0x96, 0x05, 0x44,
|
||||
0xB4, 0xFB, 0x98, 0x4C, 0x71, 0x6B, 0xBC, 0xD7, 0x5D, 0x66, 0x5E, 0x66,
|
||||
0xA7, 0x94, 0xE5, 0x65, 0x72, 0x85, 0xBC, 0x7C, 0x7F, 0x11, 0x98, 0xF8,
|
||||
0xCB, 0xD5, 0xE2, 0xB5, 0x67, 0x78, 0xF7, 0x49, 0x51, 0xC4, 0x7F, 0xBA,
|
||||
0x16, 0x66, 0xD2, 0x15, 0x5B, 0x98, 0x06, 0x03, 0x48, 0xD0, 0x9D, 0xF0,
|
||||
0x38, 0x2B, 0x9D, 0x51
|
||||
};
|
||||
|
||||
static const unsigned char TA0_RSA_E[] = {
|
||||
0x01, 0x00, 0x01
|
||||
};
|
||||
|
||||
const br_x509_trust_anchor SelfSignedTAs[1] = {
|
||||
{
|
||||
{ (unsigned char *)TA0_DN, sizeof TA0_DN },
|
||||
BR_X509_TA_CA,
|
||||
{
|
||||
BR_KEYTYPE_RSA,
|
||||
{ .rsa = {
|
||||
(unsigned char *)TA0_RSA_N, sizeof TA0_RSA_N,
|
||||
(unsigned char *)TA0_RSA_E, sizeof TA0_RSA_E,
|
||||
} }
|
||||
}
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@ -14,23 +14,26 @@ suite "Asynchronous issues test suite":
|
||||
const HELLO_PORT = 45679
|
||||
const TEST_MSG = "testmsg"
|
||||
const MSG_LEN = TEST_MSG.len()
|
||||
const TestsCount = 500
|
||||
const TestsCount = 100
|
||||
|
||||
type
|
||||
CustomData = ref object
|
||||
test: string
|
||||
|
||||
proc udp4DataAvailable(transp: DatagramTransport,
|
||||
remote: TransportAddress) {.async, gcsafe.} =
|
||||
var udata = getUserData[CustomData](transp)
|
||||
var expect = TEST_MSG
|
||||
var data: seq[byte]
|
||||
var datalen: int
|
||||
transp.peekMessage(data, datalen)
|
||||
if udata.test == "CHECK" and datalen == MSG_LEN and
|
||||
equalMem(addr data[0], addr expect[0], datalen):
|
||||
udata.test = "OK"
|
||||
transp.close()
|
||||
remote: TransportAddress) {.async: (raises: []).} =
|
||||
try:
|
||||
var udata = getUserData[CustomData](transp)
|
||||
var expect = TEST_MSG
|
||||
var data: seq[byte]
|
||||
var datalen: int
|
||||
transp.peekMessage(data, datalen)
|
||||
if udata.test == "CHECK" and datalen == MSG_LEN and
|
||||
equalMem(addr data[0], addr expect[0], datalen):
|
||||
udata.test = "OK"
|
||||
transp.close()
|
||||
except CatchableError as exc:
|
||||
raiseAssert exc.msg
|
||||
|
||||
proc issue6(): Future[bool] {.async.} =
|
||||
var myself = initTAddress("127.0.0.1:" & $HELLO_PORT)
|
||||
@ -132,6 +135,16 @@ suite "Asynchronous issues test suite":
|
||||
await server.closeWait()
|
||||
return true
|
||||
|
||||
proc testOrDeadlock(): Future[bool] {.async.} =
|
||||
proc f(): Future[void] {.async.} =
|
||||
await sleepAsync(2.seconds) or sleepAsync(1.seconds)
|
||||
let fx = f()
|
||||
try:
|
||||
await fx.cancelAndWait().wait(2.seconds)
|
||||
except AsyncTimeoutError:
|
||||
return false
|
||||
true
|
||||
|
||||
test "Issue #6":
|
||||
check waitFor(issue6()) == true
|
||||
|
||||
@ -149,3 +162,6 @@ suite "Asynchronous issues test suite":
|
||||
|
||||
test "IndexError crash test":
|
||||
check waitFor(testIndexError()) == true
|
||||
|
||||
test "`or` deadlock [#516] test":
|
||||
check waitFor(testOrDeadlock()) == true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1441
tests/testfut.nim
1441
tests/testfut.nim
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,7 @@ import std/[strutils, sha1]
|
||||
import ".."/chronos/unittest2/asynctests
|
||||
import ".."/chronos,
|
||||
".."/chronos/apps/http/[httpserver, shttpserver, httpclient]
|
||||
import stew/base10
|
||||
import stew/[byteutils, base10]
|
||||
|
||||
{.used.}
|
||||
|
||||
@ -74,6 +74,8 @@ N8r5CwGcIX/XPC3lKazzbZ8baA==
|
||||
"""
|
||||
|
||||
suite "HTTP client testing suite":
|
||||
teardown:
|
||||
checkLeaks()
|
||||
|
||||
type
|
||||
TestResponseTuple = tuple[status: int, data: string, count: int]
|
||||
@ -85,7 +87,8 @@ suite "HTTP client testing suite":
|
||||
res
|
||||
|
||||
proc createServer(address: TransportAddress,
|
||||
process: HttpProcessCallback, secure: bool): HttpServerRef =
|
||||
process: HttpProcessCallback2,
|
||||
secure: bool): HttpServerRef =
|
||||
let
|
||||
socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
serverFlags = {HttpServerFlags.Http11Pipeline}
|
||||
@ -128,18 +131,24 @@ suite "HTTP client testing suite":
|
||||
(MethodPatch, "/test/patch")
|
||||
]
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
case request.uri.path
|
||||
of "/test/get", "/test/post", "/test/head", "/test/put",
|
||||
"/test/delete", "/test/trace", "/test/options", "/test/connect",
|
||||
"/test/patch", "/test/error":
|
||||
return await request.respond(Http200, request.uri.path)
|
||||
try:
|
||||
await request.respond(Http200, request.uri.path)
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
return await request.respond(Http404, "Page not found")
|
||||
try:
|
||||
await request.respond(Http404, "Page not found")
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
var server = createServer(initTAddress("127.0.0.1:0"), process, secure)
|
||||
server.start()
|
||||
@ -157,7 +166,7 @@ suite "HTTP client testing suite":
|
||||
var req = HttpClientRequestRef.new(session, ha, item[0])
|
||||
let response = await fetch(req)
|
||||
if response.status == 200:
|
||||
let data = cast[string](response.data)
|
||||
let data = string.fromBytes(response.data)
|
||||
if data == item[1]:
|
||||
inc(counter)
|
||||
await req.closeWait()
|
||||
@ -173,7 +182,7 @@ suite "HTTP client testing suite":
|
||||
var req = HttpClientRequestRef.new(session, ha, item[0])
|
||||
let response = await fetch(req)
|
||||
if response.status == 200:
|
||||
let data = cast[string](response.data)
|
||||
let data = string.fromBytes(response.data)
|
||||
if data == item[1]:
|
||||
inc(counter)
|
||||
await req.closeWait()
|
||||
@ -187,15 +196,15 @@ suite "HTTP client testing suite":
|
||||
let ResponseTests = [
|
||||
(MethodGet, "/test/short_size_response", 65600, 1024,
|
||||
"SHORTSIZERESPONSE"),
|
||||
(MethodGet, "/test/long_size_response", 262400, 1024,
|
||||
(MethodGet, "/test/long_size_response", 131200, 1024,
|
||||
"LONGSIZERESPONSE"),
|
||||
(MethodGet, "/test/short_chunked_response", 65600, 1024,
|
||||
"SHORTCHUNKRESPONSE"),
|
||||
(MethodGet, "/test/long_chunked_response", 262400, 1024,
|
||||
(MethodGet, "/test/long_chunked_response", 131200, 1024,
|
||||
"LONGCHUNKRESPONSE")
|
||||
]
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
case request.uri.path
|
||||
@ -203,46 +212,58 @@ suite "HTTP client testing suite":
|
||||
var response = request.getResponse()
|
||||
var data = createBigMessage(ResponseTests[0][4], ResponseTests[0][2])
|
||||
response.status = Http200
|
||||
await response.sendBody(data)
|
||||
return response
|
||||
try:
|
||||
await response.sendBody(data)
|
||||
except HttpWriteError as exc:
|
||||
return defaultResponse(exc)
|
||||
response
|
||||
of "/test/long_size_response":
|
||||
var response = request.getResponse()
|
||||
var data = createBigMessage(ResponseTests[1][4], ResponseTests[1][2])
|
||||
response.status = Http200
|
||||
await response.sendBody(data)
|
||||
return response
|
||||
try:
|
||||
await response.sendBody(data)
|
||||
except HttpWriteError as exc:
|
||||
return defaultResponse(exc)
|
||||
response
|
||||
of "/test/short_chunked_response":
|
||||
var response = request.getResponse()
|
||||
var data = createBigMessage(ResponseTests[2][4], ResponseTests[2][2])
|
||||
response.status = Http200
|
||||
await response.prepare()
|
||||
var offset = 0
|
||||
while true:
|
||||
if len(data) == offset:
|
||||
break
|
||||
let toWrite = min(1024, len(data) - offset)
|
||||
await response.sendChunk(addr data[offset], toWrite)
|
||||
offset = offset + toWrite
|
||||
await response.finish()
|
||||
return response
|
||||
try:
|
||||
await response.prepare()
|
||||
var offset = 0
|
||||
while true:
|
||||
if len(data) == offset:
|
||||
break
|
||||
let toWrite = min(1024, len(data) - offset)
|
||||
await response.sendChunk(addr data[offset], toWrite)
|
||||
offset = offset + toWrite
|
||||
await response.finish()
|
||||
except HttpWriteError as exc:
|
||||
return defaultResponse(exc)
|
||||
response
|
||||
of "/test/long_chunked_response":
|
||||
var response = request.getResponse()
|
||||
var data = createBigMessage(ResponseTests[3][4], ResponseTests[3][2])
|
||||
response.status = Http200
|
||||
await response.prepare()
|
||||
var offset = 0
|
||||
while true:
|
||||
if len(data) == offset:
|
||||
break
|
||||
let toWrite = min(1024, len(data) - offset)
|
||||
await response.sendChunk(addr data[offset], toWrite)
|
||||
offset = offset + toWrite
|
||||
await response.finish()
|
||||
return response
|
||||
try:
|
||||
await response.prepare()
|
||||
var offset = 0
|
||||
while true:
|
||||
if len(data) == offset:
|
||||
break
|
||||
let toWrite = min(1024, len(data) - offset)
|
||||
await response.sendChunk(addr data[offset], toWrite)
|
||||
offset = offset + toWrite
|
||||
await response.finish()
|
||||
except HttpWriteError as exc:
|
||||
return defaultResponse(exc)
|
||||
response
|
||||
else:
|
||||
return await request.respond(Http404, "Page not found")
|
||||
defaultResponse()
|
||||
else:
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
var server = createServer(initTAddress("127.0.0.1:0"), process, secure)
|
||||
server.start()
|
||||
@ -311,21 +332,26 @@ suite "HTTP client testing suite":
|
||||
(MethodPost, "/test/big_request", 262400)
|
||||
]
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
case request.uri.path
|
||||
of "/test/big_request":
|
||||
if request.hasBody():
|
||||
let body = await request.getBody()
|
||||
let digest = $secureHash(cast[string](body))
|
||||
return await request.respond(Http200, digest)
|
||||
else:
|
||||
return await request.respond(Http400, "Missing content body")
|
||||
try:
|
||||
if request.hasBody():
|
||||
let body = await request.getBody()
|
||||
let digest = $secureHash(string.fromBytes(body))
|
||||
await request.respond(Http200, digest)
|
||||
else:
|
||||
await request.respond(Http400, "Missing content body")
|
||||
except HttpProtocolError as exc:
|
||||
defaultResponse(exc)
|
||||
except HttpTransportError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
return await request.respond(Http404, "Page not found")
|
||||
defaultResponse()
|
||||
else:
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
var server = createServer(initTAddress("127.0.0.1:0"), process, secure)
|
||||
server.start()
|
||||
@ -348,7 +374,7 @@ suite "HTTP client testing suite":
|
||||
session, ha, item[0], headers = headers
|
||||
)
|
||||
|
||||
var expectDigest = $secureHash(cast[string](data))
|
||||
var expectDigest = $secureHash(string.fromBytes(data))
|
||||
# Sending big request by 1024bytes long chunks
|
||||
var writer = await open(request)
|
||||
var offset = 0
|
||||
@ -364,7 +390,7 @@ suite "HTTP client testing suite":
|
||||
|
||||
if response.status == 200:
|
||||
var res = await response.getBodyBytes()
|
||||
if cast[string](res) == expectDigest:
|
||||
if string.fromBytes(res) == expectDigest:
|
||||
inc(counter)
|
||||
await response.closeWait()
|
||||
await request.closeWait()
|
||||
@ -381,21 +407,27 @@ suite "HTTP client testing suite":
|
||||
(MethodPost, "/test/big_chunk_request", 262400)
|
||||
]
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
case request.uri.path
|
||||
of "/test/big_chunk_request":
|
||||
if request.hasBody():
|
||||
let body = await request.getBody()
|
||||
let digest = $secureHash(cast[string](body))
|
||||
return await request.respond(Http200, digest)
|
||||
else:
|
||||
return await request.respond(Http400, "Missing content body")
|
||||
try:
|
||||
if request.hasBody():
|
||||
let
|
||||
body = await request.getBody()
|
||||
digest = $secureHash(string.fromBytes(body))
|
||||
await request.respond(Http200, digest)
|
||||
else:
|
||||
await request.respond(Http400, "Missing content body")
|
||||
except HttpProtocolError as exc:
|
||||
defaultResponse(exc)
|
||||
except HttpTransportError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
return await request.respond(Http404, "Page not found")
|
||||
defaultResponse()
|
||||
else:
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
var server = createServer(initTAddress("127.0.0.1:0"), process, secure)
|
||||
server.start()
|
||||
@ -418,7 +450,7 @@ suite "HTTP client testing suite":
|
||||
session, ha, item[0], headers = headers
|
||||
)
|
||||
|
||||
var expectDigest = $secureHash(cast[string](data))
|
||||
var expectDigest = $secureHash(string.fromBytes(data))
|
||||
# Sending big request by 1024bytes long chunks
|
||||
var writer = await open(request)
|
||||
var offset = 0
|
||||
@ -434,7 +466,7 @@ suite "HTTP client testing suite":
|
||||
|
||||
if response.status == 200:
|
||||
var res = await response.getBodyBytes()
|
||||
if cast[string](res) == expectDigest:
|
||||
if string.fromBytes(res) == expectDigest:
|
||||
inc(counter)
|
||||
await response.closeWait()
|
||||
await request.closeWait()
|
||||
@ -455,23 +487,28 @@ suite "HTTP client testing suite":
|
||||
]
|
||||
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
case request.uri.path
|
||||
of "/test/post/urlencoded_size", "/test/post/urlencoded_chunked":
|
||||
if request.hasBody():
|
||||
var postTable = await request.post()
|
||||
let body = postTable.getString("field1") & ":" &
|
||||
postTable.getString("field2") & ":" &
|
||||
postTable.getString("field3")
|
||||
return await request.respond(Http200, body)
|
||||
else:
|
||||
return await request.respond(Http400, "Missing content body")
|
||||
try:
|
||||
if request.hasBody():
|
||||
var postTable = await request.post()
|
||||
let body = postTable.getString("field1") & ":" &
|
||||
postTable.getString("field2") & ":" &
|
||||
postTable.getString("field3")
|
||||
await request.respond(Http200, body)
|
||||
else:
|
||||
await request.respond(Http400, "Missing content body")
|
||||
except HttpTransportError as exc:
|
||||
defaultResponse(exc)
|
||||
except HttpProtocolError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
return await request.respond(Http404, "Page not found")
|
||||
defaultResponse()
|
||||
else:
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
var server = createServer(initTAddress("127.0.0.1:0"), process, secure)
|
||||
server.start()
|
||||
@ -491,12 +528,12 @@ suite "HTTP client testing suite":
|
||||
]
|
||||
var request = HttpClientRequestRef.new(
|
||||
session, ha, MethodPost, headers = headers,
|
||||
body = cast[seq[byte]](PostRequests[0][1]))
|
||||
body = PostRequests[0][1].toBytes())
|
||||
var response = await send(request)
|
||||
|
||||
if response.status == 200:
|
||||
var res = await response.getBodyBytes()
|
||||
if cast[string](res) == PostRequests[0][2]:
|
||||
if string.fromBytes(res) == PostRequests[0][2]:
|
||||
inc(counter)
|
||||
await response.closeWait()
|
||||
await request.closeWait()
|
||||
@ -532,7 +569,7 @@ suite "HTTP client testing suite":
|
||||
var response = await request.finish()
|
||||
if response.status == 200:
|
||||
var res = await response.getBodyBytes()
|
||||
if cast[string](res) == PostRequests[1][2]:
|
||||
if string.fromBytes(res) == PostRequests[1][2]:
|
||||
inc(counter)
|
||||
await response.closeWait()
|
||||
await request.closeWait()
|
||||
@ -554,23 +591,28 @@ suite "HTTP client testing suite":
|
||||
]
|
||||
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
case request.uri.path
|
||||
of "/test/post/multipart_size", "/test/post/multipart_chunked":
|
||||
if request.hasBody():
|
||||
var postTable = await request.post()
|
||||
let body = postTable.getString("field1") & ":" &
|
||||
postTable.getString("field2") & ":" &
|
||||
postTable.getString("field3")
|
||||
return await request.respond(Http200, body)
|
||||
else:
|
||||
return await request.respond(Http400, "Missing content body")
|
||||
try:
|
||||
if request.hasBody():
|
||||
var postTable = await request.post()
|
||||
let body = postTable.getString("field1") & ":" &
|
||||
postTable.getString("field2") & ":" &
|
||||
postTable.getString("field3")
|
||||
await request.respond(Http200, body)
|
||||
else:
|
||||
await request.respond(Http400, "Missing content body")
|
||||
except HttpProtocolError as exc:
|
||||
defaultResponse(exc)
|
||||
except HttpTransportError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
return await request.respond(Http404, "Page not found")
|
||||
defaultResponse()
|
||||
else:
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
var server = createServer(initTAddress("127.0.0.1:0"), process, secure)
|
||||
server.start()
|
||||
@ -601,7 +643,7 @@ suite "HTTP client testing suite":
|
||||
var response = await send(request)
|
||||
if response.status == 200:
|
||||
var res = await response.getBodyBytes()
|
||||
if cast[string](res) == PostRequests[0][3]:
|
||||
if string.fromBytes(res) == PostRequests[0][3]:
|
||||
inc(counter)
|
||||
await response.closeWait()
|
||||
await request.closeWait()
|
||||
@ -634,7 +676,7 @@ suite "HTTP client testing suite":
|
||||
let response = await request.finish()
|
||||
if response.status == 200:
|
||||
var res = await response.getBodyBytes()
|
||||
if cast[string](res) == PostRequests[1][3]:
|
||||
if string.fromBytes(res) == PostRequests[1][3]:
|
||||
inc(counter)
|
||||
await response.closeWait()
|
||||
await request.closeWait()
|
||||
@ -649,26 +691,29 @@ suite "HTTP client testing suite":
|
||||
var lastAddress: Uri
|
||||
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
case request.uri.path
|
||||
of "/":
|
||||
return await request.redirect(Http302, "/redirect/1")
|
||||
of "/redirect/1":
|
||||
return await request.redirect(Http302, "/next/redirect/2")
|
||||
of "/next/redirect/2":
|
||||
return await request.redirect(Http302, "redirect/3")
|
||||
of "/next/redirect/redirect/3":
|
||||
return await request.redirect(Http302, "next/redirect/4")
|
||||
of "/next/redirect/redirect/next/redirect/4":
|
||||
return await request.redirect(Http302, lastAddress)
|
||||
of "/final/5":
|
||||
return await request.respond(Http200, "ok-5")
|
||||
else:
|
||||
return await request.respond(Http404, "Page not found")
|
||||
try:
|
||||
case request.uri.path
|
||||
of "/":
|
||||
await request.redirect(Http302, "/redirect/1")
|
||||
of "/redirect/1":
|
||||
await request.redirect(Http302, "/next/redirect/2")
|
||||
of "/next/redirect/2":
|
||||
await request.redirect(Http302, "redirect/3")
|
||||
of "/next/redirect/redirect/3":
|
||||
await request.redirect(Http302, "next/redirect/4")
|
||||
of "/next/redirect/redirect/next/redirect/4":
|
||||
await request.redirect(Http302, lastAddress)
|
||||
of "/final/5":
|
||||
await request.respond(Http200, "ok-5")
|
||||
else:
|
||||
await request.respond(Http404, "Page not found")
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
var server = createServer(initTAddress("127.0.0.1:0"), process, secure)
|
||||
server.start()
|
||||
@ -704,6 +749,107 @@ suite "HTTP client testing suite":
|
||||
await server.closeWait()
|
||||
return "redirect-" & $res
|
||||
|
||||
proc testSendCancelLeaksTest(secure: bool): Future[bool] {.async.} =
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
defaultResponse()
|
||||
|
||||
var server = createServer(initTAddress("127.0.0.1:0"), process, secure)
|
||||
server.start()
|
||||
let address = server.instance.localAddress()
|
||||
|
||||
let ha =
|
||||
if secure:
|
||||
getAddress(address, HttpClientScheme.Secure, "/")
|
||||
else:
|
||||
getAddress(address, HttpClientScheme.NonSecure, "/")
|
||||
|
||||
var counter = 0
|
||||
while true:
|
||||
let
|
||||
session = createSession(secure)
|
||||
request = HttpClientRequestRef.new(session, ha, MethodGet)
|
||||
requestFut = request.send()
|
||||
|
||||
if counter > 0:
|
||||
await stepsAsync(counter)
|
||||
let exitLoop =
|
||||
if not(requestFut.finished()):
|
||||
await cancelAndWait(requestFut)
|
||||
doAssert(cancelled(requestFut) or completed(requestFut),
|
||||
"Future should be Cancelled or Completed at this point")
|
||||
if requestFut.completed():
|
||||
let response = await requestFut
|
||||
await response.closeWait()
|
||||
|
||||
inc(counter)
|
||||
false
|
||||
else:
|
||||
let response = await requestFut
|
||||
await response.closeWait()
|
||||
true
|
||||
|
||||
await request.closeWait()
|
||||
await session.closeWait()
|
||||
|
||||
if exitLoop:
|
||||
break
|
||||
|
||||
await server.stop()
|
||||
await server.closeWait()
|
||||
return true
|
||||
|
||||
proc testOpenCancelLeaksTest(secure: bool): Future[bool] {.async.} =
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
defaultResponse()
|
||||
|
||||
var server = createServer(initTAddress("127.0.0.1:0"), process, secure)
|
||||
server.start()
|
||||
let address = server.instance.localAddress()
|
||||
|
||||
let ha =
|
||||
if secure:
|
||||
getAddress(address, HttpClientScheme.Secure, "/")
|
||||
else:
|
||||
getAddress(address, HttpClientScheme.NonSecure, "/")
|
||||
|
||||
var counter = 0
|
||||
while true:
|
||||
let
|
||||
session = createSession(secure)
|
||||
request = HttpClientRequestRef.new(session, ha, MethodPost)
|
||||
bodyFut = request.open()
|
||||
|
||||
if counter > 0:
|
||||
await stepsAsync(counter)
|
||||
let exitLoop =
|
||||
if not(bodyFut.finished()):
|
||||
await cancelAndWait(bodyFut)
|
||||
doAssert(cancelled(bodyFut) or completed(bodyFut),
|
||||
"Future should be Cancelled or Completed at this point")
|
||||
|
||||
if bodyFut.completed():
|
||||
let bodyWriter = await bodyFut
|
||||
await bodyWriter.closeWait()
|
||||
|
||||
inc(counter)
|
||||
false
|
||||
else:
|
||||
let bodyWriter = await bodyFut
|
||||
await bodyWriter.closeWait()
|
||||
true
|
||||
|
||||
await request.closeWait()
|
||||
await session.closeWait()
|
||||
|
||||
if exitLoop:
|
||||
break
|
||||
|
||||
await server.stop()
|
||||
await server.closeWait()
|
||||
return true
|
||||
|
||||
# proc testBasicAuthorization(): Future[bool] {.async.} =
|
||||
# let session = HttpSessionRef.new({HttpClientFlag.NoVerifyHost},
|
||||
# maxRedirections = 10)
|
||||
@ -766,20 +912,24 @@ suite "HTTP client testing suite":
|
||||
return @[(data1.status, data1.data.bytesToString(), count),
|
||||
(data2.status, data2.data.bytesToString(), count)]
|
||||
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
case request.uri.path
|
||||
of "/keep":
|
||||
let headers = HttpTable.init([("connection", "keep-alive")])
|
||||
return await request.respond(Http200, "ok", headers = headers)
|
||||
of "/drop":
|
||||
let headers = HttpTable.init([("connection", "close")])
|
||||
return await request.respond(Http200, "ok", headers = headers)
|
||||
else:
|
||||
return await request.respond(Http404, "Page not found")
|
||||
try:
|
||||
case request.uri.path
|
||||
of "/keep":
|
||||
let headers = HttpTable.init([("connection", "keep-alive")])
|
||||
await request.respond(Http200, "ok", headers = headers)
|
||||
of "/drop":
|
||||
let headers = HttpTable.init([("connection", "close")])
|
||||
await request.respond(Http200, "ok", headers = headers)
|
||||
else:
|
||||
await request.respond(Http404, "Page not found")
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
var server = createServer(initTAddress("127.0.0.1:0"), process, false)
|
||||
server.start()
|
||||
@ -901,16 +1051,20 @@ suite "HTTP client testing suite":
|
||||
await request.closeWait()
|
||||
return (data.status, data.data.bytesToString(), 0)
|
||||
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
case request.uri.path
|
||||
of "/test":
|
||||
return await request.respond(Http200, "ok")
|
||||
else:
|
||||
return await request.respond(Http404, "Page not found")
|
||||
try:
|
||||
case request.uri.path
|
||||
of "/test":
|
||||
await request.respond(Http200, "ok")
|
||||
else:
|
||||
await request.respond(Http404, "Page not found")
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
var server = createServer(initTAddress("127.0.0.1:0"), process, false)
|
||||
server.start()
|
||||
@ -960,19 +1114,23 @@ suite "HTTP client testing suite":
|
||||
await request.closeWait()
|
||||
return (data.status, data.data.bytesToString(), 0)
|
||||
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
case request.uri.path
|
||||
of "/test":
|
||||
return await request.respond(Http200, "ok")
|
||||
of "/keep-test":
|
||||
let headers = HttpTable.init([("Connection", "keep-alive")])
|
||||
return await request.respond(Http200, "not-alive", headers)
|
||||
else:
|
||||
return await request.respond(Http404, "Page not found")
|
||||
try:
|
||||
case request.uri.path
|
||||
of "/test":
|
||||
await request.respond(Http200, "ok")
|
||||
of "/keep-test":
|
||||
let headers = HttpTable.init([("Connection", "keep-alive")])
|
||||
await request.respond(Http200, "not-alive", headers)
|
||||
else:
|
||||
await request.respond(Http404, "Page not found")
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
var server = createServer(initTAddress("127.0.0.1:0"), process, false)
|
||||
server.start()
|
||||
@ -1075,58 +1233,62 @@ suite "HTTP client testing suite":
|
||||
return false
|
||||
true
|
||||
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
if request.uri.path.startsWith("/test/single/"):
|
||||
let index =
|
||||
block:
|
||||
var res = -1
|
||||
for index, value in SingleGoodTests.pairs():
|
||||
if value[0] == request.uri.path:
|
||||
res = index
|
||||
break
|
||||
res
|
||||
if index < 0:
|
||||
return await request.respond(Http404, "Page not found")
|
||||
var response = request.getResponse()
|
||||
response.status = Http200
|
||||
await response.sendBody(SingleGoodTests[index][1])
|
||||
return response
|
||||
elif request.uri.path.startsWith("/test/multiple/"):
|
||||
let index =
|
||||
block:
|
||||
var res = -1
|
||||
for index, value in MultipleGoodTests.pairs():
|
||||
if value[0] == request.uri.path:
|
||||
res = index
|
||||
break
|
||||
res
|
||||
if index < 0:
|
||||
return await request.respond(Http404, "Page not found")
|
||||
var response = request.getResponse()
|
||||
response.status = Http200
|
||||
await response.sendBody(MultipleGoodTests[index][1])
|
||||
return response
|
||||
elif request.uri.path.startsWith("/test/overflow/"):
|
||||
let index =
|
||||
block:
|
||||
var res = -1
|
||||
for index, value in OverflowTests.pairs():
|
||||
if value[0] == request.uri.path:
|
||||
res = index
|
||||
break
|
||||
res
|
||||
if index < 0:
|
||||
return await request.respond(Http404, "Page not found")
|
||||
var response = request.getResponse()
|
||||
response.status = Http200
|
||||
await response.sendBody(OverflowTests[index][1])
|
||||
return response
|
||||
else:
|
||||
return await request.respond(Http404, "Page not found")
|
||||
try:
|
||||
if request.uri.path.startsWith("/test/single/"):
|
||||
let index =
|
||||
block:
|
||||
var res = -1
|
||||
for index, value in SingleGoodTests.pairs():
|
||||
if value[0] == request.uri.path:
|
||||
res = index
|
||||
break
|
||||
res
|
||||
if index < 0:
|
||||
return await request.respond(Http404, "Page not found")
|
||||
var response = request.getResponse()
|
||||
response.status = Http200
|
||||
await response.sendBody(SingleGoodTests[index][1])
|
||||
response
|
||||
elif request.uri.path.startsWith("/test/multiple/"):
|
||||
let index =
|
||||
block:
|
||||
var res = -1
|
||||
for index, value in MultipleGoodTests.pairs():
|
||||
if value[0] == request.uri.path:
|
||||
res = index
|
||||
break
|
||||
res
|
||||
if index < 0:
|
||||
return await request.respond(Http404, "Page not found")
|
||||
var response = request.getResponse()
|
||||
response.status = Http200
|
||||
await response.sendBody(MultipleGoodTests[index][1])
|
||||
response
|
||||
elif request.uri.path.startsWith("/test/overflow/"):
|
||||
let index =
|
||||
block:
|
||||
var res = -1
|
||||
for index, value in OverflowTests.pairs():
|
||||
if value[0] == request.uri.path:
|
||||
res = index
|
||||
break
|
||||
res
|
||||
if index < 0:
|
||||
return await request.respond(Http404, "Page not found")
|
||||
var response = request.getResponse()
|
||||
response.status = Http200
|
||||
await response.sendBody(OverflowTests[index][1])
|
||||
response
|
||||
else:
|
||||
defaultResponse()
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
var server = createServer(initTAddress("127.0.0.1:0"), process, secure)
|
||||
server.start()
|
||||
@ -1243,6 +1405,18 @@ suite "HTTP client testing suite":
|
||||
test "HTTP(S) client maximum redirections test":
|
||||
check waitFor(testRequestRedirectTest(true, 4)) == "redirect-true"
|
||||
|
||||
test "HTTP send() cancellation leaks test":
|
||||
check waitFor(testSendCancelLeaksTest(false)) == true
|
||||
|
||||
test "HTTP(S) send() cancellation leaks test":
|
||||
check waitFor(testSendCancelLeaksTest(true)) == true
|
||||
|
||||
test "HTTP open() cancellation leaks test":
|
||||
check waitFor(testOpenCancelLeaksTest(false)) == true
|
||||
|
||||
test "HTTP(S) open() cancellation leaks test":
|
||||
check waitFor(testOpenCancelLeaksTest(true)) == true
|
||||
|
||||
test "HTTPS basic authorization test":
|
||||
skip()
|
||||
# This test disabled because remote service is pretty flaky and fails pretty
|
||||
@ -1262,5 +1436,145 @@ suite "HTTP client testing suite":
|
||||
test "HTTP client server-sent events test":
|
||||
check waitFor(testServerSentEvents(false)) == true
|
||||
|
||||
test "Leaks test":
|
||||
checkLeaks()
|
||||
test "HTTP getHttpAddress() test":
|
||||
block:
|
||||
# HTTP client supports only `http` and `https` schemes in URL.
|
||||
let res = getHttpAddress("ftp://ftp.scene.org")
|
||||
check:
|
||||
res.isErr()
|
||||
res.error == HttpAddressErrorType.InvalidUrlScheme
|
||||
res.error.isCriticalError()
|
||||
block:
|
||||
# HTTP URL default ports and custom ports test
|
||||
let
|
||||
res1 = getHttpAddress("http://www.google.com")
|
||||
res2 = getHttpAddress("https://www.google.com")
|
||||
res3 = getHttpAddress("http://www.google.com:35000")
|
||||
res4 = getHttpAddress("https://www.google.com:25000")
|
||||
check:
|
||||
res1.isOk()
|
||||
res2.isOk()
|
||||
res3.isOk()
|
||||
res4.isOk()
|
||||
res1.get().port == 80
|
||||
res2.get().port == 443
|
||||
res3.get().port == 35000
|
||||
res4.get().port == 25000
|
||||
block:
|
||||
# HTTP URL invalid port values test
|
||||
let
|
||||
res1 = getHttpAddress("http://www.google.com:-80")
|
||||
res2 = getHttpAddress("http://www.google.com:0")
|
||||
res3 = getHttpAddress("http://www.google.com:65536")
|
||||
res4 = getHttpAddress("http://www.google.com:65537")
|
||||
res5 = getHttpAddress("https://www.google.com:-443")
|
||||
res6 = getHttpAddress("https://www.google.com:0")
|
||||
res7 = getHttpAddress("https://www.google.com:65536")
|
||||
res8 = getHttpAddress("https://www.google.com:65537")
|
||||
check:
|
||||
res1.isErr() and res1.error == HttpAddressErrorType.InvalidPortNumber
|
||||
res1.error.isCriticalError()
|
||||
res2.isOk()
|
||||
res2.get().port == 0
|
||||
res3.isErr() and res3.error == HttpAddressErrorType.InvalidPortNumber
|
||||
res3.error.isCriticalError()
|
||||
res4.isErr() and res4.error == HttpAddressErrorType.InvalidPortNumber
|
||||
res4.error.isCriticalError()
|
||||
res5.isErr() and res5.error == HttpAddressErrorType.InvalidPortNumber
|
||||
res5.error.isCriticalError()
|
||||
res6.isOk()
|
||||
res6.get().port == 0
|
||||
res7.isErr() and res7.error == HttpAddressErrorType.InvalidPortNumber
|
||||
res7.error.isCriticalError()
|
||||
res8.isErr() and res8.error == HttpAddressErrorType.InvalidPortNumber
|
||||
res8.error.isCriticalError()
|
||||
block:
|
||||
# HTTP URL missing hostname
|
||||
let
|
||||
res1 = getHttpAddress("http://")
|
||||
res2 = getHttpAddress("https://")
|
||||
check:
|
||||
res1.isErr() and res1.error == HttpAddressErrorType.MissingHostname
|
||||
res1.error.isCriticalError()
|
||||
res2.isErr() and res2.error == HttpAddressErrorType.MissingHostname
|
||||
res2.error.isCriticalError()
|
||||
block:
|
||||
# No resolution flags and incorrect URL
|
||||
let
|
||||
flags = {HttpClientFlag.NoInet4Resolution,
|
||||
HttpClientFlag.NoInet6Resolution}
|
||||
res1 = getHttpAddress("http://256.256.256.256", flags)
|
||||
res2 = getHttpAddress(
|
||||
"http://[FFFFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF]", flags)
|
||||
check:
|
||||
res1.isErr() and res1.error == HttpAddressErrorType.InvalidIpHostname
|
||||
res1.error.isCriticalError()
|
||||
res2.isErr() and res2.error == HttpAddressErrorType.InvalidIpHostname
|
||||
res2.error.isCriticalError()
|
||||
block:
|
||||
# Resolution of non-existent hostname
|
||||
let res = getHttpAddress("http://eYr6bdBo.com")
|
||||
check:
|
||||
res.isErr() and res.error == HttpAddressErrorType.NameLookupFailed
|
||||
res.error.isRecoverableError()
|
||||
not(res.error.isCriticalError())
|
||||
|
||||
asyncTest "HTTPS response headers buffer size test":
|
||||
const HeadersSize = HttpMaxHeadersSize
|
||||
let expectValue =
|
||||
string.fromBytes(createBigMessage("HEADERSTEST", HeadersSize))
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
try:
|
||||
case request.uri.path
|
||||
of "/test":
|
||||
let headers = HttpTable.init([("big-header", expectValue)])
|
||||
await request.respond(Http200, "ok", headers)
|
||||
else:
|
||||
await request.respond(Http404, "Page not found")
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
defaultResponse()
|
||||
|
||||
var server = createServer(initTAddress("127.0.0.1:0"), process, false)
|
||||
server.start()
|
||||
let
|
||||
address = server.instance.localAddress()
|
||||
ha = getAddress(address, HttpClientScheme.NonSecure, "/test")
|
||||
session = HttpSessionRef.new()
|
||||
let
|
||||
req1 = HttpClientRequestRef.new(session, ha)
|
||||
req2 =
|
||||
HttpClientRequestRef.new(session, ha,
|
||||
maxResponseHeadersSize = HttpMaxHeadersSize * 2)
|
||||
res1 =
|
||||
try:
|
||||
let res {.used.} = await send(req1)
|
||||
await closeWait(req1)
|
||||
await closeWait(res)
|
||||
false
|
||||
except HttpReadError:
|
||||
true
|
||||
except HttpError:
|
||||
await closeWait(req1)
|
||||
false
|
||||
except CancelledError:
|
||||
await closeWait(req1)
|
||||
false
|
||||
|
||||
res2 = await send(req2)
|
||||
|
||||
check:
|
||||
res1 == true
|
||||
res2.status == 200
|
||||
res2.headers.getString("big-header") == expectValue
|
||||
|
||||
await req1.closeWait()
|
||||
await req2.closeWait()
|
||||
await res2.closeWait()
|
||||
await session.closeWait()
|
||||
await server.stop()
|
||||
await server.closeWait()
|
||||
|
||||
@ -7,21 +7,35 @@
|
||||
# MIT license (LICENSE-MIT)
|
||||
import std/[strutils, algorithm]
|
||||
import ".."/chronos/unittest2/asynctests,
|
||||
".."/chronos, ".."/chronos/apps/http/httpserver,
|
||||
".."/chronos/apps/http/httpcommon,
|
||||
".."/chronos/apps/http/httpdebug
|
||||
".."/chronos,
|
||||
".."/chronos/apps/http/[httpserver, httpcommon, httpdebug]
|
||||
import stew/base10
|
||||
|
||||
{.used.}
|
||||
|
||||
# Trouble finding this if defined near its use for `data2.sorted`, etc. likely
|
||||
# related to "generic sandwich" issues. If any test ever wants to `sort` a
|
||||
# `seq[(string, seq[string]]` differently, they may need to re-work that test.
|
||||
proc `<`(a, b: (string, seq[string])): bool = a[0] < b[0]
|
||||
|
||||
suite "HTTP server testing suite":
|
||||
teardown:
|
||||
checkLeaks()
|
||||
|
||||
type
|
||||
TooBigTest = enum
|
||||
GetBodyTest, ConsumeBodyTest, PostUrlTest, PostMultipartTest
|
||||
TestHttpResponse = object
|
||||
status: int
|
||||
headers: HttpTable
|
||||
data: string
|
||||
|
||||
FirstMiddlewareRef = ref object of HttpServerMiddlewareRef
|
||||
someInteger: int
|
||||
|
||||
SecondMiddlewareRef = ref object of HttpServerMiddlewareRef
|
||||
someString: string
|
||||
|
||||
proc httpClient(address: TransportAddress,
|
||||
data: string): Future[string] {.async.} =
|
||||
var transp: StreamTransport
|
||||
@ -51,7 +65,7 @@ suite "HTTP server testing suite":
|
||||
zeroMem(addr buffer[0], len(buffer))
|
||||
await transp.readExactly(addr buffer[0], length)
|
||||
let data = bytesToString(buffer.toOpenArray(0, length - 1))
|
||||
let headers =
|
||||
let (status, headers) =
|
||||
block:
|
||||
let resp = parseResponse(hdata, false)
|
||||
if resp.failed():
|
||||
@ -59,13 +73,43 @@ suite "HTTP server testing suite":
|
||||
var res = HttpTable.init()
|
||||
for key, value in resp.headers(hdata):
|
||||
res.add(key, value)
|
||||
res
|
||||
return TestHttpResponse(headers: headers, data: data)
|
||||
(resp.code, res)
|
||||
TestHttpResponse(status: status, headers: headers, data: data)
|
||||
|
||||
proc httpClient3(address: TransportAddress,
|
||||
data: string): Future[TestHttpResponse] {.async.} =
|
||||
var
|
||||
transp: StreamTransport
|
||||
buffer = newSeq[byte](4096)
|
||||
sep = @[0x0D'u8, 0x0A'u8, 0x0D'u8, 0x0A'u8]
|
||||
try:
|
||||
transp = await connect(address)
|
||||
if len(data) > 0:
|
||||
let wres = await transp.write(data)
|
||||
if wres != len(data):
|
||||
raise newException(ValueError, "Unable to write full request")
|
||||
let hres = await transp.readUntil(addr buffer[0], len(buffer), sep)
|
||||
var hdata = @buffer
|
||||
hdata.setLen(hres)
|
||||
var rres = bytesToString(await transp.read())
|
||||
let (status, headers) =
|
||||
block:
|
||||
let resp = parseResponse(hdata, false)
|
||||
if resp.failed():
|
||||
raise newException(ValueError, "Unable to decode response headers")
|
||||
var res = HttpTable.init()
|
||||
for key, value in resp.headers(hdata):
|
||||
res.add(key, value)
|
||||
(resp.code, res)
|
||||
TestHttpResponse(status: status, headers: headers, data: rres)
|
||||
finally:
|
||||
if not(isNil(transp)):
|
||||
await closeWait(transp)
|
||||
|
||||
proc testTooBigBodyChunked(operation: TooBigTest): Future[bool] {.async.} =
|
||||
var serverRes = false
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
try:
|
||||
@ -78,13 +122,15 @@ suite "HTTP server testing suite":
|
||||
let ptable {.used.} = await request.post()
|
||||
of PostMultipartTest:
|
||||
let ptable {.used.} = await request.post()
|
||||
except HttpCriticalError as exc:
|
||||
defaultResponse()
|
||||
except HttpTransportError as exc:
|
||||
defaultResponse(exc)
|
||||
except HttpProtocolError as exc:
|
||||
if exc.code == Http413:
|
||||
serverRes = true
|
||||
# Reraising exception, because processor should properly handle it.
|
||||
raise exc
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
|
||||
@ -129,14 +175,17 @@ suite "HTTP server testing suite":
|
||||
proc testTimeout(): Future[bool] {.async.} =
|
||||
var serverRes = false
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
return await request.respond(Http200, "TEST_OK", HttpTable.init())
|
||||
try:
|
||||
await request.respond(Http200, "TEST_OK", HttpTable.init())
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
if r.error.kind == HttpServerError.TimeoutError:
|
||||
serverRes = true
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
let res = HttpServerRef.new(initTAddress("127.0.0.1:0"),
|
||||
@ -159,14 +208,17 @@ suite "HTTP server testing suite":
|
||||
proc testEmpty(): Future[bool] {.async.} =
|
||||
var serverRes = false
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
return await request.respond(Http200, "TEST_OK", HttpTable.init())
|
||||
try:
|
||||
await request.respond(Http200, "TEST_OK", HttpTable.init())
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
if r.error.kind == HttpServerError.CriticalError:
|
||||
if r.error.kind == HttpServerError.ProtocolError:
|
||||
serverRes = true
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
let res = HttpServerRef.new(initTAddress("127.0.0.1:0"),
|
||||
@ -189,14 +241,17 @@ suite "HTTP server testing suite":
|
||||
proc testTooBig(): Future[bool] {.async.} =
|
||||
var serverRes = false
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
return await request.respond(Http200, "TEST_OK", HttpTable.init())
|
||||
try:
|
||||
await request.respond(Http200, "TEST_OK", HttpTable.init())
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
if r.error.error == HttpServerError.CriticalError:
|
||||
if r.error.error == HttpServerError.ProtocolError:
|
||||
serverRes = true
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
|
||||
@ -220,13 +275,11 @@ suite "HTTP server testing suite":
|
||||
proc testTooBigBody(): Future[bool] {.async.} =
|
||||
var serverRes = false
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
if r.isOk():
|
||||
discard
|
||||
else:
|
||||
if r.error.error == HttpServerError.CriticalError:
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isErr():
|
||||
if r.error.error == HttpServerError.ProtocolError:
|
||||
serverRes = true
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
|
||||
@ -267,7 +320,7 @@ suite "HTTP server testing suite":
|
||||
proc testQuery(): Future[bool] {.async.} =
|
||||
var serverRes = false
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
var kres = newSeq[string]()
|
||||
@ -275,11 +328,14 @@ suite "HTTP server testing suite":
|
||||
kres.add(k & ":" & v)
|
||||
sort(kres)
|
||||
serverRes = true
|
||||
return await request.respond(Http200, "TEST_OK:" & kres.join(":"),
|
||||
HttpTable.init())
|
||||
try:
|
||||
await request.respond(Http200, "TEST_OK:" & kres.join(":"),
|
||||
HttpTable.init())
|
||||
except HttpWriteError as exc:
|
||||
serverRes = false
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
serverRes = false
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
|
||||
@ -297,10 +353,9 @@ suite "HTTP server testing suite":
|
||||
"GET /?a=%D0%9F&%D0%A4=%D0%91&b=%D0%A6&c=%D0%AE HTTP/1.0\r\n\r\n")
|
||||
await server.stop()
|
||||
await server.closeWait()
|
||||
let r = serverRes and
|
||||
(data1.find("TEST_OK:a:1:a:2:b:3:c:4") >= 0) and
|
||||
(data2.find("TEST_OK:a:П:b:Ц:c:Ю:Ф:Б") >= 0)
|
||||
return r
|
||||
serverRes and
|
||||
(data1.find("TEST_OK:a:1:a:2:b:3:c:4") >= 0) and
|
||||
(data2.find("TEST_OK:a:П:b:Ц:c:Ю:Ф:Б") >= 0)
|
||||
|
||||
check waitFor(testQuery()) == true
|
||||
|
||||
@ -308,7 +363,7 @@ suite "HTTP server testing suite":
|
||||
proc testHeaders(): Future[bool] {.async.} =
|
||||
var serverRes = false
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
var kres = newSeq[string]()
|
||||
@ -316,11 +371,14 @@ suite "HTTP server testing suite":
|
||||
kres.add(k & ":" & v)
|
||||
sort(kres)
|
||||
serverRes = true
|
||||
return await request.respond(Http200, "TEST_OK:" & kres.join(":"),
|
||||
HttpTable.init())
|
||||
try:
|
||||
await request.respond(Http200, "TEST_OK:" & kres.join(":"),
|
||||
HttpTable.init())
|
||||
except HttpWriteError as exc:
|
||||
serverRes = false
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
serverRes = false
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
|
||||
@ -352,21 +410,30 @@ suite "HTTP server testing suite":
|
||||
proc testPostUrl(): Future[bool] {.async.} =
|
||||
var serverRes = false
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
var kres = newSeq[string]()
|
||||
let request = r.get()
|
||||
if request.meth in PostMethods:
|
||||
let post = await request.post()
|
||||
let post =
|
||||
try:
|
||||
await request.post()
|
||||
except HttpProtocolError as exc:
|
||||
return defaultResponse(exc)
|
||||
except HttpTransportError as exc:
|
||||
return defaultResponse(exc)
|
||||
for k, v in post.stringItems():
|
||||
kres.add(k & ":" & v)
|
||||
sort(kres)
|
||||
serverRes = true
|
||||
return await request.respond(Http200, "TEST_OK:" & kres.join(":"),
|
||||
HttpTable.init())
|
||||
serverRes = true
|
||||
try:
|
||||
await request.respond(Http200, "TEST_OK:" & kres.join(":"),
|
||||
HttpTable.init())
|
||||
except HttpWriteError as exc:
|
||||
serverRes = false
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
serverRes = false
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
|
||||
@ -396,21 +463,30 @@ suite "HTTP server testing suite":
|
||||
proc testPostUrl2(): Future[bool] {.async.} =
|
||||
var serverRes = false
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
var kres = newSeq[string]()
|
||||
let request = r.get()
|
||||
if request.meth in PostMethods:
|
||||
let post = await request.post()
|
||||
let post =
|
||||
try:
|
||||
await request.post()
|
||||
except HttpProtocolError as exc:
|
||||
return defaultResponse(exc)
|
||||
except HttpTransportError as exc:
|
||||
return defaultResponse(exc)
|
||||
for k, v in post.stringItems():
|
||||
kres.add(k & ":" & v)
|
||||
sort(kres)
|
||||
serverRes = true
|
||||
return await request.respond(Http200, "TEST_OK:" & kres.join(":"),
|
||||
HttpTable.init())
|
||||
serverRes = true
|
||||
try:
|
||||
await request.respond(Http200, "TEST_OK:" & kres.join(":"),
|
||||
HttpTable.init())
|
||||
except HttpWriteError as exc:
|
||||
serverRes = false
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
serverRes = false
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
|
||||
@ -441,21 +517,30 @@ suite "HTTP server testing suite":
|
||||
proc testPostMultipart(): Future[bool] {.async.} =
|
||||
var serverRes = false
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
var kres = newSeq[string]()
|
||||
let request = r.get()
|
||||
if request.meth in PostMethods:
|
||||
let post = await request.post()
|
||||
let post =
|
||||
try:
|
||||
await request.post()
|
||||
except HttpProtocolError as exc:
|
||||
return defaultResponse(exc)
|
||||
except HttpTransportError as exc:
|
||||
return defaultResponse(exc)
|
||||
for k, v in post.stringItems():
|
||||
kres.add(k & ":" & v)
|
||||
sort(kres)
|
||||
serverRes = true
|
||||
return await request.respond(Http200, "TEST_OK:" & kres.join(":"),
|
||||
HttpTable.init())
|
||||
serverRes = true
|
||||
try:
|
||||
await request.respond(Http200, "TEST_OK:" & kres.join(":"),
|
||||
HttpTable.init())
|
||||
except HttpWriteError as exc:
|
||||
serverRes = false
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
serverRes = false
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
|
||||
@ -497,21 +582,31 @@ suite "HTTP server testing suite":
|
||||
proc testPostMultipart2(): Future[bool] {.async.} =
|
||||
var serverRes = false
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
var kres = newSeq[string]()
|
||||
let request = r.get()
|
||||
if request.meth in PostMethods:
|
||||
let post = await request.post()
|
||||
let post =
|
||||
try:
|
||||
await request.post()
|
||||
except HttpProtocolError as exc:
|
||||
return defaultResponse(exc)
|
||||
except HttpTransportError as exc:
|
||||
return defaultResponse(exc)
|
||||
for k, v in post.stringItems():
|
||||
kres.add(k & ":" & v)
|
||||
sort(kres)
|
||||
serverRes = true
|
||||
return await request.respond(Http200, "TEST_OK:" & kres.join(":"),
|
||||
HttpTable.init())
|
||||
try:
|
||||
await request.respond(Http200, "TEST_OK:" & kres.join(":"),
|
||||
HttpTable.init())
|
||||
except HttpWriteError as exc:
|
||||
serverRes = false
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
serverRes = false
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
|
||||
@ -566,16 +661,20 @@ suite "HTTP server testing suite":
|
||||
var eventContinue = newAsyncEvent()
|
||||
var count = 0
|
||||
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
inc(count)
|
||||
if count == ClientsCount:
|
||||
eventWait.fire()
|
||||
await eventContinue.wait()
|
||||
return await request.respond(Http404, "", HttpTable.init())
|
||||
try:
|
||||
await request.respond(Http404, "", HttpTable.init())
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
|
||||
@ -752,11 +851,11 @@ suite "HTTP server testing suite":
|
||||
for key, value in table1.items(true):
|
||||
data2.add((key, value))
|
||||
|
||||
check:
|
||||
data1 == @[("Header2", "value2"), ("Header2", "VALUE3"),
|
||||
("Header1", "value1")]
|
||||
data2 == @[("Header2", @["value2", "VALUE3"]),
|
||||
("Header1", @["value1"])]
|
||||
check: # .sorted to not depend upon hash(key)-order
|
||||
data1.sorted == sorted(@[("Header2", "value2"), ("Header2", "VALUE3"),
|
||||
("Header1", "value1")])
|
||||
data2.sorted == sorted(@[("Header2", @["value2", "VALUE3"]),
|
||||
("Header1", @["value1"])])
|
||||
|
||||
table1.set("header2", "value4")
|
||||
check:
|
||||
@ -1230,23 +1329,26 @@ suite "HTTP server testing suite":
|
||||
proc testPostMultipart2(): Future[bool] {.async.} =
|
||||
var serverRes = false
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
let response = request.getResponse()
|
||||
await response.prepareSSE()
|
||||
await response.send("event: event1\r\ndata: data1\r\n\r\n")
|
||||
await response.send("event: event2\r\ndata: data2\r\n\r\n")
|
||||
await response.sendEvent("event3", "data3")
|
||||
await response.sendEvent("event4", "data4")
|
||||
await response.send("data: data5\r\n\r\n")
|
||||
await response.sendEvent("", "data6")
|
||||
await response.finish()
|
||||
serverRes = true
|
||||
return response
|
||||
try:
|
||||
await response.prepareSSE()
|
||||
await response.send("event: event1\r\ndata: data1\r\n\r\n")
|
||||
await response.send("event: event2\r\ndata: data2\r\n\r\n")
|
||||
await response.sendEvent("event3", "data3")
|
||||
await response.sendEvent("event4", "data4")
|
||||
await response.send("data: data5\r\n\r\n")
|
||||
await response.sendEvent("", "data6")
|
||||
await response.finish()
|
||||
serverRes = true
|
||||
response
|
||||
except HttpWriteError as exc:
|
||||
serverRes = false
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
serverRes = false
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
|
||||
@ -1305,12 +1407,16 @@ suite "HTTP server testing suite":
|
||||
{}, false, "close")
|
||||
]
|
||||
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
return await request.respond(Http200, "TEST_OK", HttpTable.init())
|
||||
try:
|
||||
await request.respond(Http200, "TEST_OK", HttpTable.init())
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
for test in TestMessages:
|
||||
let
|
||||
@ -1327,44 +1433,47 @@ suite "HTTP server testing suite":
|
||||
|
||||
server.start()
|
||||
var transp: StreamTransport
|
||||
try:
|
||||
transp = await connect(address)
|
||||
block:
|
||||
let response = await transp.httpClient2(test[0], 7)
|
||||
check:
|
||||
response.data == "TEST_OK"
|
||||
response.headers.getString("connection") == test[3]
|
||||
# We do this sleeping here just because we running both server and
|
||||
# client in single process, so when we received response from server
|
||||
# it does not mean that connection has been immediately closed - it
|
||||
# takes some more calls, so we trying to get this calls happens.
|
||||
await sleepAsync(50.milliseconds)
|
||||
let connectionStillAvailable =
|
||||
try:
|
||||
let response {.used.} = await transp.httpClient2(test[0], 7)
|
||||
true
|
||||
except CatchableError:
|
||||
false
|
||||
|
||||
check connectionStillAvailable == test[2]
|
||||
transp = await connect(address)
|
||||
block:
|
||||
let response = await transp.httpClient2(test[0], 7)
|
||||
check:
|
||||
response.data == "TEST_OK"
|
||||
response.headers.getString("connection") == test[3]
|
||||
# We do this sleeping here just because we running both server and
|
||||
# client in single process, so when we received response from server
|
||||
# it does not mean that connection has been immediately closed - it
|
||||
# takes some more calls, so we trying to get this calls happens.
|
||||
await sleepAsync(50.milliseconds)
|
||||
let connectionStillAvailable =
|
||||
try:
|
||||
let response {.used.} = await transp.httpClient2(test[0], 7)
|
||||
true
|
||||
except CatchableError:
|
||||
false
|
||||
|
||||
finally:
|
||||
if not(isNil(transp)):
|
||||
await transp.closeWait()
|
||||
await server.stop()
|
||||
await server.closeWait()
|
||||
check connectionStillAvailable == test[2]
|
||||
|
||||
if not(isNil(transp)):
|
||||
await transp.closeWait()
|
||||
await server.stop()
|
||||
await server.closeWait()
|
||||
|
||||
asyncTest "HTTP debug tests":
|
||||
const
|
||||
TestsCount = 10
|
||||
TestRequest = "GET / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n"
|
||||
TestRequest = "GET /httpdebug HTTP/1.1\r\nConnection: keep-alive\r\n\r\n"
|
||||
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
return await request.respond(Http200, "TEST_OK", HttpTable.init())
|
||||
try:
|
||||
await request.respond(Http200, "TEST_OK", HttpTable.init())
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
proc client(address: TransportAddress,
|
||||
data: string): Future[StreamTransport] {.async.} =
|
||||
@ -1401,31 +1510,320 @@ suite "HTTP server testing suite":
|
||||
info.flags == {HttpServerFlags.Http11Pipeline}
|
||||
info.socketFlags == socketFlags
|
||||
|
||||
var clientFutures: seq[Future[StreamTransport]]
|
||||
for i in 0 ..< TestsCount:
|
||||
clientFutures.add(client(address, TestRequest))
|
||||
await allFutures(clientFutures)
|
||||
|
||||
let connections = server.getConnections()
|
||||
check len(connections) == TestsCount
|
||||
let currentTime = Moment.now()
|
||||
for index, connection in connections.pairs():
|
||||
let transp = clientFutures[index].read()
|
||||
check:
|
||||
connection.remoteAddress.get() == transp.localAddress()
|
||||
connection.localAddress.get() == transp.remoteAddress()
|
||||
connection.connectionType == ConnectionType.NonSecure
|
||||
connection.connectionState == ConnectionState.Alive
|
||||
connection.query.get("") == "/httpdebug"
|
||||
(currentTime - connection.createMoment.get()) != ZeroDuration
|
||||
(currentTime - connection.acceptMoment) != ZeroDuration
|
||||
var pending: seq[Future[void]]
|
||||
for transpFut in clientFutures:
|
||||
pending.add(closeWait(transpFut.read()))
|
||||
await allFutures(pending)
|
||||
await server.stop()
|
||||
await server.closeWait()
|
||||
|
||||
asyncTest "HTTP middleware request filtering test":
|
||||
proc init(t: typedesc[FirstMiddlewareRef],
|
||||
data: int): HttpServerMiddlewareRef =
|
||||
proc shandler(
|
||||
middleware: HttpServerMiddlewareRef,
|
||||
reqfence: RequestFence,
|
||||
nextHandler: HttpProcessCallback2
|
||||
): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
|
||||
let mw = FirstMiddlewareRef(middleware)
|
||||
if reqfence.isErr():
|
||||
# Our handler is not supposed to handle request errors, so we
|
||||
# call next handler in sequence which could process errors.
|
||||
return await nextHandler(reqfence)
|
||||
|
||||
let request = reqfence.get()
|
||||
if request.uri.path == "/first":
|
||||
# This is request we are waiting for, so we going to process it.
|
||||
try:
|
||||
await request.respond(Http200, $mw.someInteger)
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
# We know nothing about request's URI, so we pass this request to the
|
||||
# next handler which could process such request.
|
||||
await nextHandler(reqfence)
|
||||
|
||||
HttpServerMiddlewareRef(
|
||||
FirstMiddlewareRef(someInteger: data, handler: shandler))
|
||||
|
||||
proc init(t: typedesc[SecondMiddlewareRef],
|
||||
data: string): HttpServerMiddlewareRef =
|
||||
proc shandler(
|
||||
middleware: HttpServerMiddlewareRef,
|
||||
reqfence: RequestFence,
|
||||
nextHandler: HttpProcessCallback2
|
||||
): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
|
||||
let mw = SecondMiddlewareRef(middleware)
|
||||
if reqfence.isErr():
|
||||
# Our handler is not supposed to handle request errors, so we
|
||||
# call next handler in sequence which could process errors.
|
||||
return await nextHandler(reqfence)
|
||||
|
||||
let request = reqfence.get()
|
||||
|
||||
if request.uri.path == "/second":
|
||||
# This is request we are waiting for, so we going to process it.
|
||||
try:
|
||||
await request.respond(Http200, mw.someString)
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
# We know nothing about request's URI, so we pass this request to the
|
||||
# next handler which could process such request.
|
||||
await nextHandler(reqfence)
|
||||
|
||||
HttpServerMiddlewareRef(
|
||||
SecondMiddlewareRef(someString: data, handler: shandler))
|
||||
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
if request.uri.path == "/test":
|
||||
try:
|
||||
await request.respond(Http200, "ORIGIN")
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
defaultResponse()
|
||||
else:
|
||||
defaultResponse()
|
||||
|
||||
let
|
||||
middlewares = [FirstMiddlewareRef.init(655370),
|
||||
SecondMiddlewareRef.init("SECOND")]
|
||||
socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
|
||||
socketFlags = socketFlags,
|
||||
middlewares = middlewares)
|
||||
check res.isOk()
|
||||
|
||||
let server = res.get()
|
||||
server.start()
|
||||
let
|
||||
address = server.instance.localAddress()
|
||||
req1 = "GET /test HTTP/1.1\r\n\r\n"
|
||||
req2 = "GET /first HTTP/1.1\r\n\r\n"
|
||||
req3 = "GET /second HTTP/1.1\r\n\r\n"
|
||||
req4 = "GET /noway HTTP/1.1\r\n\r\n"
|
||||
resp1 = await httpClient3(address, req1)
|
||||
resp2 = await httpClient3(address, req2)
|
||||
resp3 = await httpClient3(address, req3)
|
||||
resp4 = await httpClient3(address, req4)
|
||||
|
||||
check:
|
||||
resp1.status == 200
|
||||
resp1.data == "ORIGIN"
|
||||
resp2.status == 200
|
||||
resp2.data == "655370"
|
||||
resp3.status == 200
|
||||
resp3.data == "SECOND"
|
||||
resp4.status == 404
|
||||
|
||||
await server.stop()
|
||||
await server.closeWait()
|
||||
|
||||
asyncTest "HTTP middleware request modification test":
|
||||
proc init(t: typedesc[FirstMiddlewareRef],
|
||||
data: int): HttpServerMiddlewareRef =
|
||||
proc shandler(
|
||||
middleware: HttpServerMiddlewareRef,
|
||||
reqfence: RequestFence,
|
||||
nextHandler: HttpProcessCallback2
|
||||
): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
|
||||
let mw = FirstMiddlewareRef(middleware)
|
||||
if reqfence.isErr():
|
||||
# Our handler is not supposed to handle request errors, so we
|
||||
# call next handler in sequence which could process errors.
|
||||
return await nextHandler(reqfence)
|
||||
|
||||
let
|
||||
request = reqfence.get()
|
||||
modifiedUri = "/modified/" & $mw.someInteger & request.rawPath
|
||||
var modifiedHeaders = request.headers
|
||||
modifiedHeaders.add("X-Modified", "test-value")
|
||||
|
||||
let res = request.updateRequest(modifiedUri, modifiedHeaders)
|
||||
if res.isErr():
|
||||
return defaultResponse(res.error)
|
||||
|
||||
# We sending modified request to the next handler.
|
||||
await nextHandler(reqfence)
|
||||
|
||||
HttpServerMiddlewareRef(
|
||||
FirstMiddlewareRef(someInteger: data, handler: shandler))
|
||||
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
try:
|
||||
await request.respond(Http200, request.rawPath & ":" &
|
||||
request.headers.getString("x-modified"))
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
defaultResponse()
|
||||
|
||||
let
|
||||
middlewares = [FirstMiddlewareRef.init(655370)]
|
||||
socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
|
||||
socketFlags = socketFlags,
|
||||
middlewares = middlewares)
|
||||
check res.isOk()
|
||||
|
||||
let server = res.get()
|
||||
server.start()
|
||||
let
|
||||
address = server.instance.localAddress()
|
||||
req1 = "GET /test HTTP/1.1\r\n\r\n"
|
||||
req2 = "GET /first HTTP/1.1\r\n\r\n"
|
||||
req3 = "GET /second HTTP/1.1\r\n\r\n"
|
||||
req4 = "GET /noway HTTP/1.1\r\n\r\n"
|
||||
resp1 = await httpClient3(address, req1)
|
||||
resp2 = await httpClient3(address, req2)
|
||||
resp3 = await httpClient3(address, req3)
|
||||
resp4 = await httpClient3(address, req4)
|
||||
|
||||
check:
|
||||
resp1.status == 200
|
||||
resp1.data == "/modified/655370/test:test-value"
|
||||
resp2.status == 200
|
||||
resp2.data == "/modified/655370/first:test-value"
|
||||
resp3.status == 200
|
||||
resp3.data == "/modified/655370/second:test-value"
|
||||
resp4.status == 200
|
||||
resp4.data == "/modified/655370/noway:test-value"
|
||||
|
||||
await server.stop()
|
||||
await server.closeWait()
|
||||
|
||||
asyncTest "HTTP middleware request blocking test":
|
||||
proc init(t: typedesc[FirstMiddlewareRef],
|
||||
data: int): HttpServerMiddlewareRef =
|
||||
proc shandler(
|
||||
middleware: HttpServerMiddlewareRef,
|
||||
reqfence: RequestFence,
|
||||
nextHandler: HttpProcessCallback2
|
||||
): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
|
||||
if reqfence.isErr():
|
||||
# Our handler is not supposed to handle request errors, so we
|
||||
# call next handler in sequence which could process errors.
|
||||
return await nextHandler(reqfence)
|
||||
|
||||
let request = reqfence.get()
|
||||
if request.uri.path == "/first":
|
||||
# Blocking request by disconnecting remote peer.
|
||||
dropResponse()
|
||||
elif request.uri.path == "/second":
|
||||
# Blocking request by sending HTTP error message with 401 code.
|
||||
codeResponse(Http401)
|
||||
else:
|
||||
# Allow all other requests to be processed by next handler.
|
||||
await nextHandler(reqfence)
|
||||
|
||||
HttpServerMiddlewareRef(
|
||||
FirstMiddlewareRef(someInteger: data, handler: shandler))
|
||||
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
try:
|
||||
await request.respond(Http200, "ORIGIN")
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
defaultResponse()
|
||||
|
||||
let
|
||||
middlewares = [FirstMiddlewareRef.init(655370)]
|
||||
socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
|
||||
socketFlags = socketFlags,
|
||||
middlewares = middlewares)
|
||||
check res.isOk()
|
||||
|
||||
let server = res.get()
|
||||
server.start()
|
||||
let
|
||||
address = server.instance.localAddress()
|
||||
req1 = "GET /test HTTP/1.1\r\n\r\n"
|
||||
req2 = "GET /first HTTP/1.1\r\n\r\n"
|
||||
req3 = "GET /second HTTP/1.1\r\n\r\n"
|
||||
resp1 = await httpClient3(address, req1)
|
||||
resp3 = await httpClient3(address, req3)
|
||||
|
||||
check:
|
||||
resp1.status == 200
|
||||
resp1.data == "ORIGIN"
|
||||
resp3.status == 401
|
||||
|
||||
let checked =
|
||||
try:
|
||||
let res {.used.} = await httpClient3(address, req2)
|
||||
false
|
||||
except TransportIncompleteError:
|
||||
true
|
||||
|
||||
check:
|
||||
checked == true
|
||||
|
||||
await server.stop()
|
||||
await server.closeWait()
|
||||
|
||||
asyncTest "HTTP server - baseUri value test":
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
defaultResponse()
|
||||
|
||||
let
|
||||
expectUri2 = "http://www.chronos-test.com/"
|
||||
address = initTAddress("127.0.0.1:0")
|
||||
socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
res1 = HttpServerRef.new(address, process,
|
||||
socketFlags = socketFlags)
|
||||
res2 = HttpServerRef.new(address, process,
|
||||
socketFlags = socketFlags,
|
||||
serverUri = parseUri(expectUri2))
|
||||
check:
|
||||
res1.isOk == true
|
||||
res2.isOk == true
|
||||
|
||||
let
|
||||
server1 = res1.get()
|
||||
server2 = res2.get()
|
||||
|
||||
try:
|
||||
var clientFutures: seq[Future[StreamTransport]]
|
||||
for i in 0 ..< TestsCount:
|
||||
clientFutures.add(client(address, TestRequest))
|
||||
await allFutures(clientFutures)
|
||||
|
||||
let connections = server.getConnections()
|
||||
check len(connections) == TestsCount
|
||||
let currentTime = Moment.now()
|
||||
for index, connection in connections.pairs():
|
||||
let transp = clientFutures[index].read()
|
||||
check:
|
||||
connection.remoteAddress.get() == transp.localAddress()
|
||||
connection.localAddress.get() == transp.remoteAddress()
|
||||
connection.connectionType == ConnectionType.NonSecure
|
||||
connection.connectionState == ConnectionState.Alive
|
||||
(currentTime - connection.createMoment.get()) != ZeroDuration
|
||||
(currentTime - connection.acceptMoment) != ZeroDuration
|
||||
var pending: seq[Future[void]]
|
||||
for transpFut in clientFutures:
|
||||
pending.add(closeWait(transpFut.read()))
|
||||
await allFutures(pending)
|
||||
server1.start()
|
||||
server2.start()
|
||||
let
|
||||
localAddress = server1.instance.localAddress()
|
||||
expectUri1 = "http://127.0.0.1:" & $localAddress.port & "/"
|
||||
check:
|
||||
server1.baseUri == parseUri(expectUri1)
|
||||
server2.baseUri == parseUri(expectUri2)
|
||||
finally:
|
||||
await server.stop()
|
||||
await server.closeWait()
|
||||
|
||||
test "Leaks test":
|
||||
checkLeaks()
|
||||
await server1.stop()
|
||||
await server1.closeWait()
|
||||
await server2.stop()
|
||||
await server2.closeWait()
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
import std/[macros, strutils]
|
||||
import unittest2
|
||||
import ../chronos
|
||||
import ../chronos/config
|
||||
|
||||
{.used.}
|
||||
|
||||
@ -94,6 +95,11 @@ proc testAwaitne(): Future[bool] {.async.} =
|
||||
|
||||
return true
|
||||
|
||||
template returner =
|
||||
# can't use `return 5`
|
||||
result = 5
|
||||
return
|
||||
|
||||
suite "Macro transformations test suite":
|
||||
test "`await` command test":
|
||||
check waitFor(testAwait()) == true
|
||||
@ -136,6 +142,151 @@ suite "Macro transformations test suite":
|
||||
check:
|
||||
waitFor(gen(int)) == default(int)
|
||||
|
||||
test "Nested return":
|
||||
proc nr: Future[int] {.async.} =
|
||||
return
|
||||
if 1 == 1:
|
||||
return 42
|
||||
else:
|
||||
33
|
||||
|
||||
check waitFor(nr()) == 42
|
||||
|
||||
# There are a few unreacheable statements to ensure that we don't regress in
|
||||
# generated code
|
||||
{.push warning[UnreachableCode]: off.}
|
||||
|
||||
suite "Macro transformations - completions":
|
||||
test "Run closure to completion on return": # issue #415
|
||||
var x = 0
|
||||
proc test415 {.async.} =
|
||||
try:
|
||||
return
|
||||
finally:
|
||||
await sleepAsync(1.milliseconds)
|
||||
x = 5
|
||||
waitFor(test415())
|
||||
check: x == 5
|
||||
|
||||
test "Run closure to completion on defer":
|
||||
var x = 0
|
||||
proc testDefer {.async.} =
|
||||
defer:
|
||||
await sleepAsync(1.milliseconds)
|
||||
x = 5
|
||||
return
|
||||
waitFor(testDefer())
|
||||
check: x == 5
|
||||
|
||||
test "Run closure to completion with exceptions":
|
||||
var x = 0
|
||||
proc testExceptionHandling {.async.} =
|
||||
try:
|
||||
return
|
||||
finally:
|
||||
try:
|
||||
await sleepAsync(1.milliseconds)
|
||||
raise newException(ValueError, "")
|
||||
except ValueError:
|
||||
await sleepAsync(1.milliseconds)
|
||||
await sleepAsync(1.milliseconds)
|
||||
x = 5
|
||||
waitFor(testExceptionHandling())
|
||||
check: x == 5
|
||||
|
||||
test "Correct return value when updating result after return":
|
||||
proc testWeirdCase: int =
|
||||
try: return 33
|
||||
finally: result = 55
|
||||
proc testWeirdCaseAsync: Future[int] {.async.} =
|
||||
try:
|
||||
await sleepAsync(1.milliseconds)
|
||||
return 33
|
||||
finally: result = 55
|
||||
|
||||
check:
|
||||
testWeirdCase() == waitFor(testWeirdCaseAsync())
|
||||
testWeirdCase() == 55
|
||||
|
||||
test "Correct return value with result assignment in defer":
|
||||
proc testWeirdCase: int =
|
||||
defer:
|
||||
result = 55
|
||||
result = 33
|
||||
proc testWeirdCaseAsync: Future[int] {.async.} =
|
||||
defer:
|
||||
result = 55
|
||||
await sleepAsync(1.milliseconds)
|
||||
return 33
|
||||
|
||||
check:
|
||||
testWeirdCase() == waitFor(testWeirdCaseAsync())
|
||||
testWeirdCase() == 55
|
||||
|
||||
test "Generic & finally calling async":
|
||||
proc testGeneric(T: type): Future[T] {.async.} =
|
||||
try:
|
||||
try:
|
||||
await sleepAsync(1.milliseconds)
|
||||
return
|
||||
finally:
|
||||
await sleepAsync(1.milliseconds)
|
||||
await sleepAsync(1.milliseconds)
|
||||
result = 11
|
||||
finally:
|
||||
await sleepAsync(1.milliseconds)
|
||||
await sleepAsync(1.milliseconds)
|
||||
result = 12
|
||||
check waitFor(testGeneric(int)) == 12
|
||||
|
||||
proc testFinallyCallsAsync(T: type): Future[T] {.async.} =
|
||||
try:
|
||||
await sleepAsync(1.milliseconds)
|
||||
return
|
||||
finally:
|
||||
result = await testGeneric(T)
|
||||
check waitFor(testFinallyCallsAsync(int)) == 12
|
||||
|
||||
test "templates returning":
|
||||
proc testReturner: Future[int] {.async.} =
|
||||
returner
|
||||
doAssert false
|
||||
check waitFor(testReturner()) == 5
|
||||
|
||||
proc testReturner2: Future[int] {.async.} =
|
||||
template returner2 =
|
||||
return 6
|
||||
returner2
|
||||
doAssert false
|
||||
check waitFor(testReturner2()) == 6
|
||||
|
||||
test "raising defects":
|
||||
proc raiser {.async.} =
|
||||
# sleeping to make sure our caller is the poll loop
|
||||
await sleepAsync(0.milliseconds)
|
||||
raise newException(Defect, "uh-oh")
|
||||
|
||||
let fut = raiser()
|
||||
expect(Defect): waitFor(fut)
|
||||
check not fut.completed()
|
||||
fut.complete()
|
||||
|
||||
test "return result":
|
||||
proc returnResult: Future[int] {.async.} =
|
||||
var result: int
|
||||
result = 12
|
||||
return result
|
||||
check waitFor(returnResult()) == 12
|
||||
|
||||
test "async in async":
|
||||
proc asyncInAsync: Future[int] {.async.} =
|
||||
proc a2: Future[int] {.async.} =
|
||||
result = 12
|
||||
result = await a2()
|
||||
check waitFor(asyncInAsync()) == 12
|
||||
{.pop.}
|
||||
|
||||
suite "Macro transformations - implicit returns":
|
||||
test "Implicit return":
|
||||
proc implicit(): Future[int] {.async.} =
|
||||
42
|
||||
@ -232,3 +383,244 @@ suite "Closure iterator's exception transformation issues":
|
||||
|
||||
waitFor(x())
|
||||
|
||||
suite "Exceptions tracking":
|
||||
template checkNotCompiles(body: untyped) =
|
||||
check (not compiles(body))
|
||||
test "Can raise valid exception":
|
||||
proc test1 {.async.} = raise newException(ValueError, "hey")
|
||||
proc test2 {.async: (raises: [ValueError]).} = raise newException(ValueError, "hey")
|
||||
proc test3 {.async: (raises: [IOError, ValueError]).} =
|
||||
if 1 == 2:
|
||||
raise newException(ValueError, "hey")
|
||||
else:
|
||||
raise newException(IOError, "hey")
|
||||
|
||||
proc test4 {.async: (raises: []), used.} = raise newException(Defect, "hey")
|
||||
proc test5 {.async: (raises: []).} = discard
|
||||
proc test6 {.async: (raises: []).} = await test5()
|
||||
|
||||
expect(ValueError): waitFor test1()
|
||||
expect(ValueError): waitFor test2()
|
||||
expect(IOError): waitFor test3()
|
||||
waitFor test6()
|
||||
|
||||
test "Cannot raise invalid exception":
|
||||
checkNotCompiles:
|
||||
proc test3 {.async: (raises: [IOError]).} = raise newException(ValueError, "hey")
|
||||
|
||||
test "Explicit return in non-raising proc":
|
||||
proc test(): Future[int] {.async: (raises: []).} = return 12
|
||||
check:
|
||||
waitFor(test()) == 12
|
||||
|
||||
test "Non-raising compatibility":
|
||||
proc test1 {.async: (raises: [ValueError]).} = raise newException(ValueError, "hey")
|
||||
let testVar: Future[void] = test1()
|
||||
|
||||
proc test2 {.async.} = raise newException(ValueError, "hey")
|
||||
let testVar2: proc: Future[void] = test2
|
||||
|
||||
# Doesn't work unfortunately
|
||||
#let testVar3: proc: Future[void] = test1
|
||||
|
||||
test "Cannot store invalid future types":
|
||||
proc test1 {.async: (raises: [ValueError]).} = raise newException(ValueError, "hey")
|
||||
proc test2 {.async: (raises: [IOError]).} = raise newException(IOError, "hey")
|
||||
|
||||
var a = test1()
|
||||
checkNotCompiles:
|
||||
a = test2()
|
||||
|
||||
test "Await raises the correct types":
|
||||
proc test1 {.async: (raises: [ValueError]).} = raise newException(ValueError, "hey")
|
||||
proc test2 {.async: (raises: [ValueError, CancelledError]).} = await test1()
|
||||
checkNotCompiles:
|
||||
proc test3 {.async: (raises: [CancelledError]).} = await test1()
|
||||
|
||||
test "Can create callbacks":
|
||||
proc test1 {.async: (raises: [ValueError]).} = raise newException(ValueError, "hey")
|
||||
let callback: proc() {.async: (raises: [ValueError]).} = test1
|
||||
|
||||
test "Can return values":
|
||||
proc test1: Future[int] {.async: (raises: [ValueError]).} =
|
||||
if 1 == 0: raise newException(ValueError, "hey")
|
||||
return 12
|
||||
proc test2: Future[int] {.async: (raises: [ValueError, IOError, CancelledError]).} =
|
||||
return await test1()
|
||||
|
||||
checkNotCompiles:
|
||||
proc test3: Future[int] {.async: (raises: [CancelledError]).} = await test1()
|
||||
|
||||
check waitFor(test2()) == 12
|
||||
|
||||
test "Manual tracking":
|
||||
proc test1: Future[int] {.async: (raw: true, raises: [ValueError]).} =
|
||||
result = newFuture[int]()
|
||||
result.complete(12)
|
||||
check waitFor(test1()) == 12
|
||||
|
||||
proc test2: Future[int] {.async: (raw: true, raises: [IOError, OSError]).} =
|
||||
checkNotCompiles:
|
||||
result.fail(newException(ValueError, "fail"))
|
||||
|
||||
result = newFuture[int]()
|
||||
result.fail(newException(IOError, "fail"))
|
||||
|
||||
proc test3: Future[void] {.async: (raw: true, raises: []).} =
|
||||
result = newFuture[void]()
|
||||
checkNotCompiles:
|
||||
result.fail(newException(ValueError, "fail"))
|
||||
result.complete()
|
||||
# Inheritance
|
||||
proc test4: Future[void] {.async: (raw: true, raises: [CatchableError]).} =
|
||||
result = newFuture[void]()
|
||||
result.fail(newException(IOError, "fail"))
|
||||
|
||||
check:
|
||||
waitFor(test1()) == 12
|
||||
expect(IOError):
|
||||
discard waitFor(test2())
|
||||
|
||||
waitFor(test3())
|
||||
expect(IOError):
|
||||
waitFor(test4())
|
||||
|
||||
test "or errors":
|
||||
proc testit {.async: (raises: [ValueError]).} =
|
||||
raise (ref ValueError)()
|
||||
|
||||
proc testit2 {.async: (raises: [IOError]).} =
|
||||
raise (ref IOError)()
|
||||
|
||||
proc test {.async: (raises: [CancelledError, ValueError, IOError]).} =
|
||||
await testit() or testit2()
|
||||
|
||||
proc noraises() {.raises: [].} =
|
||||
expect(ValueError):
|
||||
try:
|
||||
let f = test()
|
||||
waitFor(f)
|
||||
except IOError:
|
||||
doAssert false
|
||||
|
||||
noraises()
|
||||
|
||||
test "Wait errors":
|
||||
proc testit {.async: (raises: [ValueError]).} =
|
||||
raise newException(ValueError, "hey")
|
||||
|
||||
proc test {.async: (raises: [ValueError, AsyncTimeoutError, CancelledError]).} =
|
||||
await wait(testit(), 1000.milliseconds)
|
||||
|
||||
proc noraises() {.raises: [].} =
|
||||
try:
|
||||
expect(ValueError): waitFor(test())
|
||||
except CancelledError: doAssert false
|
||||
except AsyncTimeoutError: doAssert false
|
||||
|
||||
noraises()
|
||||
|
||||
test "Nocancel errors with raises":
|
||||
proc testit {.async: (raises: [ValueError, CancelledError]).} =
|
||||
await sleepAsync(5.milliseconds)
|
||||
raise (ref ValueError)()
|
||||
|
||||
proc test {.async: (raises: [ValueError]).} =
|
||||
await noCancel testit()
|
||||
|
||||
proc noraises() {.raises: [].} =
|
||||
expect(ValueError):
|
||||
let f = test()
|
||||
waitFor(f.cancelAndWait())
|
||||
waitFor(f)
|
||||
|
||||
noraises()
|
||||
|
||||
test "Nocancel with no errors":
|
||||
proc testit {.async: (raises: [CancelledError]).} =
|
||||
await sleepAsync(5.milliseconds)
|
||||
|
||||
proc test {.async: (raises: []).} =
|
||||
await noCancel testit()
|
||||
|
||||
proc noraises() {.raises: [].} =
|
||||
let f = test()
|
||||
waitFor(f.cancelAndWait())
|
||||
waitFor(f)
|
||||
|
||||
noraises()
|
||||
|
||||
test "Nocancel errors without raises":
|
||||
proc testit {.async.} =
|
||||
await sleepAsync(5.milliseconds)
|
||||
raise (ref ValueError)()
|
||||
|
||||
proc test {.async.} =
|
||||
await noCancel testit()
|
||||
|
||||
proc noraises() =
|
||||
expect(ValueError):
|
||||
let f = test()
|
||||
waitFor(f.cancelAndWait())
|
||||
waitFor(f)
|
||||
|
||||
noraises()
|
||||
|
||||
test "Defect on wrong exception type at runtime":
|
||||
{.push warning[User]: off}
|
||||
let f = InternalRaisesFuture[void, (ValueError,)]()
|
||||
expect(Defect): f.fail((ref CatchableError)())
|
||||
{.pop.}
|
||||
check: not f.finished()
|
||||
|
||||
expect(Defect): f.fail((ref CatchableError)(), warn = false)
|
||||
check: not f.finished()
|
||||
|
||||
test "handleException behavior":
|
||||
proc raiseException() {.
|
||||
async: (handleException: true, raises: [AsyncExceptionError]).} =
|
||||
raise (ref Exception)(msg: "Raising Exception is UB and support for it may change in the future")
|
||||
|
||||
proc callCatchAll() {.async: (raises: []).} =
|
||||
expect(AsyncExceptionError):
|
||||
await raiseException()
|
||||
|
||||
waitFor(callCatchAll())
|
||||
|
||||
test "Global handleException does not override local annotations":
|
||||
when chronosHandleException:
|
||||
proc unnanotated() {.async.} = raise (ref CatchableError)()
|
||||
|
||||
checkNotCompiles:
|
||||
proc annotated() {.async: (raises: [ValueError]).} =
|
||||
raise (ref CatchableError)()
|
||||
|
||||
checkNotCompiles:
|
||||
proc noHandleException() {.async: (handleException: false).} =
|
||||
raise (ref Exception)()
|
||||
else:
|
||||
skip()
|
||||
|
||||
test "Results compatibility":
|
||||
proc returnOk(): Future[Result[int, string]] {.async: (raises: []).} =
|
||||
ok(42)
|
||||
|
||||
proc returnErr(): Future[Result[int, string]] {.async: (raises: []).} =
|
||||
err("failed")
|
||||
|
||||
proc testit(): Future[Result[void, string]] {.async: (raises: []).} =
|
||||
let
|
||||
v = await returnOk()
|
||||
|
||||
check:
|
||||
v.isOk() and v.value() == 42
|
||||
|
||||
let
|
||||
vok = ?v
|
||||
check:
|
||||
vok == 42
|
||||
|
||||
discard ?await returnErr()
|
||||
|
||||
check:
|
||||
waitFor(testit()).error() == "failed"
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
IF /I "%1" == "STDIN" (
|
||||
GOTO :STDINTEST
|
||||
) ELSE IF /I "%1" == "TIMEOUT1" (
|
||||
GOTO :TIMEOUTTEST1
|
||||
) ELSE IF /I "%1" == "TIMEOUT2" (
|
||||
GOTO :TIMEOUTTEST2
|
||||
) ELSE IF /I "%1" == "TIMEOUT10" (
|
||||
@ -19,6 +21,10 @@ SET /P "INPUTDATA="
|
||||
ECHO STDIN DATA: %INPUTDATA%
|
||||
EXIT 0
|
||||
|
||||
:TIMEOUTTEST1
|
||||
ping -n 1 127.0.0.1 > NUL
|
||||
EXIT 1
|
||||
|
||||
:TIMEOUTTEST2
|
||||
ping -n 2 127.0.0.1 > NUL
|
||||
EXIT 2
|
||||
@ -28,7 +34,7 @@ ping -n 10 127.0.0.1 > NUL
|
||||
EXIT 0
|
||||
|
||||
:BIGDATA
|
||||
FOR /L %%G IN (1, 1, 400000) DO ECHO ALICEWASBEGINNINGTOGETVERYTIREDOFSITTINGBYHERSISTERONTHEBANKANDO
|
||||
FOR /L %%G IN (1, 1, 100000) DO ECHO ALICEWASBEGINNINGTOGETVERYTIREDOFSITTINGBYHERSISTERONTHEBANKANDO
|
||||
EXIT 0
|
||||
|
||||
:ENVTEST
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
import std/os
|
||||
import stew/[base10, byteutils]
|
||||
import ".."/chronos/unittest2/asynctests
|
||||
import ".."/chronos/asyncproc
|
||||
|
||||
when defined(posix):
|
||||
from ".."/chronos/osdefs import SIGKILL
|
||||
@ -15,6 +16,9 @@ when defined(posix):
|
||||
when defined(nimHasUsed): {.used.}
|
||||
|
||||
suite "Asynchronous process management test suite":
|
||||
teardown:
|
||||
checkLeaks()
|
||||
|
||||
const OutputTests =
|
||||
when defined(windows):
|
||||
[
|
||||
@ -96,7 +100,11 @@ suite "Asynchronous process management test suite":
|
||||
|
||||
let
|
||||
options = {AsyncProcessOption.EvalCommand}
|
||||
command = "exit 1"
|
||||
command =
|
||||
when defined(windows):
|
||||
"tests\\testproc.bat timeout1"
|
||||
else:
|
||||
"tests/testproc.sh timeout1"
|
||||
|
||||
process = await startProcess(command, options = options)
|
||||
|
||||
@ -201,31 +209,34 @@ suite "Asynchronous process management test suite":
|
||||
await process.closeWait()
|
||||
|
||||
asyncTest "Capture big amount of bytes from STDOUT stream test":
|
||||
let options = {AsyncProcessOption.EvalCommand}
|
||||
let command =
|
||||
when defined(windows):
|
||||
"tests\\testproc.bat bigdata"
|
||||
else:
|
||||
"tests/testproc.sh bigdata"
|
||||
let expect =
|
||||
when defined(windows):
|
||||
400_000 * (64 + 2)
|
||||
else:
|
||||
400_000 * (64 + 1)
|
||||
let process = await startProcess(command, options = options,
|
||||
stdoutHandle = AsyncProcess.Pipe,
|
||||
stderrHandle = AsyncProcess.Pipe)
|
||||
try:
|
||||
let outBytesFut = process.stdoutStream.read()
|
||||
let errBytesFut = process.stderrStream.read()
|
||||
let res = await process.waitForExit(InfiniteDuration)
|
||||
await allFutures(outBytesFut, errBytesFut)
|
||||
check:
|
||||
res == 0
|
||||
len(outBytesFut.read()) == expect
|
||||
len(errBytesFut.read()) == 0
|
||||
finally:
|
||||
await process.closeWait()
|
||||
when sizeof(int) == 4:
|
||||
skip()
|
||||
else:
|
||||
let options = {AsyncProcessOption.EvalCommand}
|
||||
let command =
|
||||
when defined(windows):
|
||||
"tests\\testproc.bat bigdata"
|
||||
else:
|
||||
"tests/testproc.sh bigdata"
|
||||
let expect =
|
||||
when defined(windows):
|
||||
100_000 * (64 + 2)
|
||||
else:
|
||||
100_000 * (64 + 1)
|
||||
let process = await startProcess(command, options = options,
|
||||
stdoutHandle = AsyncProcess.Pipe,
|
||||
stderrHandle = AsyncProcess.Pipe)
|
||||
try:
|
||||
let outBytesFut = process.stdoutStream.read()
|
||||
let errBytesFut = process.stderrStream.read()
|
||||
let res = await process.waitForExit(InfiniteDuration)
|
||||
await allFutures(outBytesFut, errBytesFut)
|
||||
check:
|
||||
res == 0
|
||||
len(outBytesFut.read()) == expect
|
||||
len(errBytesFut.read()) == 0
|
||||
finally:
|
||||
await process.closeWait()
|
||||
|
||||
asyncTest "Long-waiting waitForExit() test":
|
||||
let command =
|
||||
@ -407,11 +418,54 @@ suite "Asynchronous process management test suite":
|
||||
finally:
|
||||
await process.closeWait()
|
||||
|
||||
asyncTest "killAndWaitForExit() test":
|
||||
let command =
|
||||
when defined(windows):
|
||||
("tests\\testproc.bat", "timeout10", 0)
|
||||
else:
|
||||
("tests/testproc.sh", "timeout10", 128 + int(SIGKILL))
|
||||
let process = await startProcess(command[0], arguments = @[command[1]])
|
||||
try:
|
||||
let exitCode = await process.killAndWaitForExit(10.seconds)
|
||||
check exitCode == command[2]
|
||||
finally:
|
||||
await process.closeWait()
|
||||
|
||||
asyncTest "terminateAndWaitForExit() test":
|
||||
let command =
|
||||
when defined(windows):
|
||||
("tests\\testproc.bat", "timeout10", 0)
|
||||
else:
|
||||
("tests/testproc.sh", "timeout10", 128 + int(SIGTERM))
|
||||
let process = await startProcess(command[0], arguments = @[command[1]])
|
||||
try:
|
||||
let exitCode = await process.terminateAndWaitForExit(10.seconds)
|
||||
check exitCode == command[2]
|
||||
finally:
|
||||
await process.closeWait()
|
||||
|
||||
asyncTest "terminateAndWaitForExit() timeout test":
|
||||
when defined(windows):
|
||||
skip()
|
||||
else:
|
||||
let
|
||||
command = ("tests/testproc.sh", "noterm", 128 + int(SIGKILL))
|
||||
process = await startProcess(command[0], arguments = @[command[1]])
|
||||
# We should wait here to allow `bash` execute `trap` command, otherwise
|
||||
# our test script will be killed with SIGTERM. Increase this timeout
|
||||
# if test become flaky.
|
||||
await sleepAsync(1.seconds)
|
||||
try:
|
||||
expect AsyncProcessTimeoutError:
|
||||
let exitCode {.used.} =
|
||||
await process.terminateAndWaitForExit(1.seconds)
|
||||
let exitCode = await process.killAndWaitForExit(10.seconds)
|
||||
check exitCode == command[2]
|
||||
finally:
|
||||
await process.closeWait()
|
||||
|
||||
test "File descriptors leaks test":
|
||||
when defined(windows):
|
||||
skip()
|
||||
else:
|
||||
check getCurrentFD() == markFD
|
||||
|
||||
test "Leaks test":
|
||||
checkLeaks()
|
||||
|
||||
@ -3,18 +3,26 @@
|
||||
if [ "$1" == "stdin" ]; then
|
||||
read -r inputdata
|
||||
echo "STDIN DATA: $inputdata"
|
||||
elif [ "$1" == "timeout1" ]; then
|
||||
sleep 1
|
||||
exit 1
|
||||
elif [ "$1" == "timeout2" ]; then
|
||||
sleep 2
|
||||
exit 2
|
||||
elif [ "$1" == "timeout10" ]; then
|
||||
sleep 10
|
||||
elif [ "$1" == "bigdata" ]; then
|
||||
for i in {1..400000}
|
||||
for i in {1..100000}
|
||||
do
|
||||
echo "ALICEWASBEGINNINGTOGETVERYTIREDOFSITTINGBYHERSISTERONTHEBANKANDO"
|
||||
done
|
||||
elif [ "$1" == "envtest" ]; then
|
||||
echo "$CHRONOSASYNC"
|
||||
elif [ "$1" == "noterm" ]; then
|
||||
trap -- '' SIGTERM
|
||||
while true; do
|
||||
sleep 1
|
||||
done
|
||||
else
|
||||
echo "arguments missing"
|
||||
fi
|
||||
|
||||
@ -49,7 +49,7 @@ suite "Token Bucket":
|
||||
# Consume 10* the budget cap
|
||||
let beforeStart = Moment.now()
|
||||
waitFor(bucket.consume(1000).wait(5.seconds))
|
||||
check Moment.now() - beforeStart in 900.milliseconds .. 1500.milliseconds
|
||||
check Moment.now() - beforeStart in 900.milliseconds .. 2200.milliseconds
|
||||
|
||||
test "Sync manual replenish":
|
||||
var bucket = TokenBucket.new(1000, 0.seconds)
|
||||
@ -96,7 +96,7 @@ suite "Token Bucket":
|
||||
futBlocker.finished == false
|
||||
fut2.finished == false
|
||||
|
||||
futBlocker.cancel()
|
||||
futBlocker.cancelSoon()
|
||||
waitFor(fut2.wait(10.milliseconds))
|
||||
|
||||
test "Very long replenish":
|
||||
@ -117,9 +117,14 @@ suite "Token Bucket":
|
||||
check bucket.tryConsume(1, fakeNow) == true
|
||||
|
||||
test "Short replenish":
|
||||
var bucket = TokenBucket.new(15000, 1.milliseconds)
|
||||
let start = Moment.now()
|
||||
check bucket.tryConsume(15000, start)
|
||||
check bucket.tryConsume(1, start) == false
|
||||
skip()
|
||||
# TODO (cheatfate): This test was disabled, because it continuosly fails in
|
||||
# Github Actions Windows x64 CI when using Nim 1.6.14 version.
|
||||
# Unable to reproduce failure locally.
|
||||
|
||||
check bucket.tryConsume(15000, start + 1.milliseconds) == true
|
||||
# var bucket = TokenBucket.new(15000, 1.milliseconds)
|
||||
# let start = Moment.now()
|
||||
# check bucket.tryConsume(15000, start)
|
||||
# check bucket.tryConsume(1, start) == false
|
||||
|
||||
# check bucket.tryConsume(15000, start + 1.milliseconds) == true
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
# Licensed under either of
|
||||
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
||||
# MIT license (LICENSE-MIT)
|
||||
import unittest2
|
||||
import ../chronos
|
||||
|
||||
import ../chronos/unittest2/asynctests
|
||||
|
||||
{.used.}
|
||||
|
||||
@ -23,30 +23,40 @@ suite "Server's test suite":
|
||||
CustomData = ref object
|
||||
test: string
|
||||
|
||||
teardown:
|
||||
checkLeaks()
|
||||
|
||||
proc serveStreamClient(server: StreamServer,
|
||||
transp: StreamTransport) {.async.} =
|
||||
transp: StreamTransport) {.async: (raises: []).} =
|
||||
discard
|
||||
|
||||
proc serveCustomStreamClient(server: StreamServer,
|
||||
transp: StreamTransport) {.async.} =
|
||||
var cserver = cast[CustomServer](server)
|
||||
var ctransp = cast[CustomTransport](transp)
|
||||
cserver.test1 = "CONNECTION"
|
||||
cserver.test2 = ctransp.test
|
||||
cserver.test3 = await transp.readLine()
|
||||
var answer = "ANSWER\r\n"
|
||||
discard await transp.write(answer)
|
||||
transp.close()
|
||||
await transp.join()
|
||||
transp: StreamTransport) {.async: (raises: []).} =
|
||||
try:
|
||||
var cserver = cast[CustomServer](server)
|
||||
var ctransp = cast[CustomTransport](transp)
|
||||
cserver.test1 = "CONNECTION"
|
||||
cserver.test2 = ctransp.test
|
||||
cserver.test3 = await transp.readLine()
|
||||
var answer = "ANSWER\r\n"
|
||||
discard await transp.write(answer)
|
||||
transp.close()
|
||||
await transp.join()
|
||||
except CatchableError as exc:
|
||||
raiseAssert exc.msg
|
||||
|
||||
|
||||
proc serveUdataStreamClient(server: StreamServer,
|
||||
transp: StreamTransport) {.async.} =
|
||||
var udata = getUserData[CustomData](server)
|
||||
var line = await transp.readLine()
|
||||
var msg = line & udata.test & "\r\n"
|
||||
discard await transp.write(msg)
|
||||
transp.close()
|
||||
await transp.join()
|
||||
transp: StreamTransport) {.async: (raises: []).} =
|
||||
try:
|
||||
var udata = getUserData[CustomData](server)
|
||||
var line = await transp.readLine()
|
||||
var msg = line & udata.test & "\r\n"
|
||||
discard await transp.write(msg)
|
||||
transp.close()
|
||||
await transp.join()
|
||||
except CatchableError as exc:
|
||||
raiseAssert exc.msg
|
||||
|
||||
proc customServerTransport(server: StreamServer,
|
||||
fd: AsyncFD): StreamTransport =
|
||||
@ -54,37 +64,47 @@ suite "Server's test suite":
|
||||
transp.test = "CUSTOM"
|
||||
result = cast[StreamTransport](transp)
|
||||
|
||||
proc test1(): bool =
|
||||
asyncTest "Stream Server start/stop test":
|
||||
var ta = initTAddress("127.0.0.1:31354")
|
||||
var server1 = createStreamServer(ta, serveStreamClient, {ReuseAddr})
|
||||
server1.start()
|
||||
server1.stop()
|
||||
server1.close()
|
||||
waitFor server1.join()
|
||||
await server1.join()
|
||||
|
||||
var server2 = createStreamServer(ta, serveStreamClient, {ReuseAddr})
|
||||
server2.start()
|
||||
server2.stop()
|
||||
server2.close()
|
||||
waitFor server2.join()
|
||||
result = true
|
||||
await server2.join()
|
||||
|
||||
proc test5(): bool =
|
||||
var ta = initTAddress("127.0.0.1:31354")
|
||||
asyncTest "Stream Server stop without start test":
|
||||
var ta = initTAddress("127.0.0.1:0")
|
||||
var server1 = createStreamServer(ta, serveStreamClient, {ReuseAddr})
|
||||
ta = server1.localAddress()
|
||||
server1.stop()
|
||||
server1.close()
|
||||
waitFor server1.join()
|
||||
|
||||
await server1.join()
|
||||
var server2 = createStreamServer(ta, serveStreamClient, {ReuseAddr})
|
||||
server2.stop()
|
||||
server2.close()
|
||||
waitFor server2.join()
|
||||
result = true
|
||||
await server2.join()
|
||||
|
||||
asyncTest "Stream Server inherited object test":
|
||||
var server = CustomServer()
|
||||
server.test1 = "TEST"
|
||||
var ta = initTAddress("127.0.0.1:0")
|
||||
var pserver = createStreamServer(ta, serveCustomStreamClient, {ReuseAddr},
|
||||
child = server,
|
||||
init = customServerTransport)
|
||||
check:
|
||||
pserver == server
|
||||
|
||||
proc client1(server: CustomServer, ta: TransportAddress) {.async.} =
|
||||
var transp = CustomTransport()
|
||||
transp.test = "CLIENT"
|
||||
server.start()
|
||||
var ptransp = await connect(ta, child = transp)
|
||||
var ptransp = await connect(server.localAddress(), child = transp)
|
||||
var etransp = cast[CustomTransport](ptransp)
|
||||
doAssert(etransp.test == "CLIENT")
|
||||
var msg = "TEST\r\n"
|
||||
@ -96,44 +116,48 @@ suite "Server's test suite":
|
||||
server.close()
|
||||
await server.join()
|
||||
|
||||
proc client2(server: StreamServer,
|
||||
ta: TransportAddress): Future[bool] {.async.} =
|
||||
check:
|
||||
server.test1 == "CONNECTION"
|
||||
server.test2 == "CUSTOM"
|
||||
|
||||
asyncTest "StreamServer[T] test":
|
||||
var co = CustomData()
|
||||
co.test = "CUSTOMDATA"
|
||||
var ta = initTAddress("127.0.0.1:0")
|
||||
var server = createStreamServer(ta, serveUdataStreamClient, {ReuseAddr},
|
||||
udata = co)
|
||||
|
||||
server.start()
|
||||
var transp = await connect(ta)
|
||||
var transp = await connect(server.localAddress())
|
||||
var msg = "TEST\r\n"
|
||||
discard await transp.write(msg)
|
||||
var line = await transp.readLine()
|
||||
result = (line == "TESTCUSTOMDATA")
|
||||
check:
|
||||
line == "TESTCUSTOMDATA"
|
||||
transp.close()
|
||||
server.stop()
|
||||
server.close()
|
||||
await server.join()
|
||||
|
||||
proc test3(): bool =
|
||||
var server = CustomServer()
|
||||
server.test1 = "TEST"
|
||||
var ta = initTAddress("127.0.0.1:31354")
|
||||
var pserver = createStreamServer(ta, serveCustomStreamClient, {ReuseAddr},
|
||||
child = cast[StreamServer](server),
|
||||
init = customServerTransport)
|
||||
doAssert(not isNil(pserver))
|
||||
waitFor client1(server, ta)
|
||||
result = (server.test1 == "CONNECTION") and (server.test2 == "CUSTOM")
|
||||
asyncTest "Backlog and connect cancellation":
|
||||
var ta = initTAddress("127.0.0.1:0")
|
||||
var server1 = createStreamServer(ta, serveStreamClient, {ReuseAddr}, backlog = 1)
|
||||
ta = server1.localAddress()
|
||||
|
||||
proc test4(): bool =
|
||||
var co = CustomData()
|
||||
co.test = "CUSTOMDATA"
|
||||
var ta = initTAddress("127.0.0.1:31354")
|
||||
var server = createStreamServer(ta, serveUdataStreamClient, {ReuseAddr},
|
||||
udata = co)
|
||||
result = waitFor client2(server, ta)
|
||||
var clients: seq[Future[StreamTransport]]
|
||||
for i in 0..<10:
|
||||
clients.add(connect(server1.localAddress))
|
||||
|
||||
# Check for leaks in cancellation / connect when server is not accepting
|
||||
for c in clients:
|
||||
if not c.finished:
|
||||
await c.cancelAndWait()
|
||||
else:
|
||||
# The backlog connection "should" end up here
|
||||
try:
|
||||
await c.read().closeWait()
|
||||
except CatchableError:
|
||||
discard
|
||||
|
||||
test "Stream Server start/stop test":
|
||||
check test1() == true
|
||||
test "Stream Server stop without start test":
|
||||
check test5() == true
|
||||
test "Stream Server inherited object test":
|
||||
check test3() == true
|
||||
test "StreamServer[T] test":
|
||||
check test4() == true
|
||||
server1.close()
|
||||
await server1.join()
|
||||
|
||||
@ -7,7 +7,8 @@
|
||||
# MIT license (LICENSE-MIT)
|
||||
import std/strutils
|
||||
import ".."/chronos/unittest2/asynctests
|
||||
import ".."/chronos, ".."/chronos/apps/http/shttpserver
|
||||
import ".."/chronos,
|
||||
".."/chronos/apps/http/shttpserver
|
||||
import stew/base10
|
||||
|
||||
{.used.}
|
||||
@ -74,6 +75,8 @@ N8r5CwGcIX/XPC3lKazzbZ8baA==
|
||||
|
||||
|
||||
suite "Secure HTTP server testing suite":
|
||||
teardown:
|
||||
checkLeaks()
|
||||
|
||||
proc httpsClient(address: TransportAddress,
|
||||
data: string, flags = {NoVerifyHost, NoVerifyServerName}
|
||||
@ -107,15 +110,18 @@ suite "Secure HTTP server testing suite":
|
||||
proc testHTTPS(address: TransportAddress): Future[bool] {.async.} =
|
||||
var serverRes = false
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
serverRes = true
|
||||
return await request.respond(Http200, "TEST_OK:" & $request.meth,
|
||||
HttpTable.init())
|
||||
try:
|
||||
await request.respond(Http200, "TEST_OK:" & $request.meth,
|
||||
HttpTable.init())
|
||||
except HttpWriteError as exc:
|
||||
serverRes = false
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
serverRes = false
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
let serverFlags = {Secure}
|
||||
@ -145,16 +151,18 @@ suite "Secure HTTP server testing suite":
|
||||
var serverRes = false
|
||||
var testFut = newFuture[void]()
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async.} =
|
||||
async: (raises: [CancelledError]).} =
|
||||
if r.isOk():
|
||||
let request = r.get()
|
||||
serverRes = false
|
||||
return await request.respond(Http200, "TEST_OK:" & $request.meth,
|
||||
HttpTable.init())
|
||||
try:
|
||||
await request.respond(Http200, "TEST_OK:" & $request.meth,
|
||||
HttpTable.init())
|
||||
except HttpWriteError as exc:
|
||||
defaultResponse(exc)
|
||||
else:
|
||||
serverRes = true
|
||||
testFut.complete()
|
||||
return defaultResponse()
|
||||
defaultResponse()
|
||||
|
||||
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
let serverFlags = {Secure}
|
||||
@ -179,5 +187,48 @@ suite "Secure HTTP server testing suite":
|
||||
|
||||
check waitFor(testHTTPS2(initTAddress("127.0.0.1:30080"))) == true
|
||||
|
||||
test "Leaks test":
|
||||
checkLeaks()
|
||||
asyncTest "HTTPS server - baseUri value test":
|
||||
proc process(r: RequestFence): Future[HttpResponseRef] {.
|
||||
async: (raises: [CancelledError]).} =
|
||||
defaultResponse()
|
||||
|
||||
let
|
||||
expectUri2 = "https://www.chronos-test.com/"
|
||||
address = initTAddress("127.0.0.1:0")
|
||||
socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
||||
serverFlags = {Secure}
|
||||
secureKey = TLSPrivateKey.init(HttpsSelfSignedRsaKey)
|
||||
secureCert = TLSCertificate.init(HttpsSelfSignedRsaCert)
|
||||
res1 = SecureHttpServerRef.new(address, process,
|
||||
socketFlags = socketFlags,
|
||||
serverFlags = serverFlags,
|
||||
tlsPrivateKey = secureKey,
|
||||
tlsCertificate = secureCert)
|
||||
res2 = SecureHttpServerRef.new(address, process,
|
||||
socketFlags = socketFlags,
|
||||
serverFlags = serverFlags,
|
||||
serverUri = parseUri(expectUri2),
|
||||
tlsPrivateKey = secureKey,
|
||||
tlsCertificate = secureCert)
|
||||
check:
|
||||
res1.isOk == true
|
||||
res2.isOk == true
|
||||
|
||||
let
|
||||
server1 = res1.get()
|
||||
server2 = res2.get()
|
||||
|
||||
try:
|
||||
server1.start()
|
||||
server2.start()
|
||||
let
|
||||
localAddress = server1.instance.localAddress()
|
||||
expectUri1 = "https://127.0.0.1:" & $localAddress.port & "/"
|
||||
check:
|
||||
server1.baseUri == parseUri(expectUri1)
|
||||
server2.baseUri == parseUri(expectUri2)
|
||||
finally:
|
||||
await server1.stop()
|
||||
await server1.closeWait()
|
||||
await server2.stop()
|
||||
await server2.closeWait()
|
||||
|
||||
@ -11,75 +11,83 @@ import ../chronos
|
||||
{.used.}
|
||||
|
||||
suite "callSoon() tests suite":
|
||||
const CallSoonTests = 10
|
||||
var soonTest1 = 0'u
|
||||
var timeoutsTest1 = 0
|
||||
var timeoutsTest2 = 0
|
||||
var soonTest2 = 0
|
||||
|
||||
proc callback1(udata: pointer) {.gcsafe.} =
|
||||
soonTest1 = soonTest1 xor cast[uint](udata)
|
||||
|
||||
proc test1(): uint =
|
||||
callSoon(callback1, cast[pointer](0x12345678'u))
|
||||
callSoon(callback1, cast[pointer](0x23456789'u))
|
||||
callSoon(callback1, cast[pointer](0x3456789A'u))
|
||||
callSoon(callback1, cast[pointer](0x456789AB'u))
|
||||
callSoon(callback1, cast[pointer](0x56789ABC'u))
|
||||
callSoon(callback1, cast[pointer](0x6789ABCD'u))
|
||||
callSoon(callback1, cast[pointer](0x789ABCDE'u))
|
||||
callSoon(callback1, cast[pointer](0x89ABCDEF'u))
|
||||
callSoon(callback1, cast[pointer](0x9ABCDEF1'u))
|
||||
callSoon(callback1, cast[pointer](0xABCDEF12'u))
|
||||
callSoon(callback1, cast[pointer](0xBCDEF123'u))
|
||||
callSoon(callback1, cast[pointer](0xCDEF1234'u))
|
||||
callSoon(callback1, cast[pointer](0xDEF12345'u))
|
||||
callSoon(callback1, cast[pointer](0xEF123456'u))
|
||||
callSoon(callback1, cast[pointer](0xF1234567'u))
|
||||
callSoon(callback1, cast[pointer](0x12345678'u))
|
||||
## All callbacks must be processed exactly with 1 poll() call.
|
||||
poll()
|
||||
result = soonTest1
|
||||
|
||||
proc testProc() {.async.} =
|
||||
for i in 1..CallSoonTests:
|
||||
await sleepAsync(100.milliseconds)
|
||||
timeoutsTest1 += 1
|
||||
|
||||
var callbackproc: proc(udata: pointer) {.gcsafe, raises: [].}
|
||||
callbackproc = proc (udata: pointer) {.gcsafe, raises: [].} =
|
||||
timeoutsTest2 += 1
|
||||
{.gcsafe.}:
|
||||
callSoon(callbackproc)
|
||||
|
||||
proc test2(timers, callbacks: var int) =
|
||||
callSoon(callbackproc)
|
||||
waitFor(testProc())
|
||||
timers = timeoutsTest1
|
||||
callbacks = timeoutsTest2
|
||||
|
||||
proc testCallback(udata: pointer) =
|
||||
soonTest2 = 987654321
|
||||
|
||||
proc test3(): bool =
|
||||
callSoon(testCallback)
|
||||
poll()
|
||||
result = soonTest2 == 987654321
|
||||
|
||||
test "User-defined callback argument test":
|
||||
var values = [0x12345678'u, 0x23456789'u, 0x3456789A'u, 0x456789AB'u,
|
||||
0x56789ABC'u, 0x6789ABCD'u, 0x789ABCDE'u, 0x89ABCDEF'u,
|
||||
0x9ABCDEF1'u, 0xABCDEF12'u, 0xBCDEF123'u, 0xCDEF1234'u,
|
||||
0xDEF12345'u, 0xEF123456'u, 0xF1234567'u, 0x12345678'u]
|
||||
var expect = 0'u
|
||||
for item in values:
|
||||
expect = expect xor item
|
||||
check test1() == expect
|
||||
proc test(): bool =
|
||||
var soonTest = 0'u
|
||||
|
||||
proc callback(udata: pointer) {.gcsafe.} =
|
||||
soonTest = soonTest xor cast[uint](udata)
|
||||
|
||||
callSoon(callback, cast[pointer](0x12345678'u))
|
||||
callSoon(callback, cast[pointer](0x23456789'u))
|
||||
callSoon(callback, cast[pointer](0x3456789A'u))
|
||||
callSoon(callback, cast[pointer](0x456789AB'u))
|
||||
callSoon(callback, cast[pointer](0x56789ABC'u))
|
||||
callSoon(callback, cast[pointer](0x6789ABCD'u))
|
||||
callSoon(callback, cast[pointer](0x789ABCDE'u))
|
||||
callSoon(callback, cast[pointer](0x89ABCDEF'u))
|
||||
callSoon(callback, cast[pointer](0x9ABCDEF1'u))
|
||||
callSoon(callback, cast[pointer](0xABCDEF12'u))
|
||||
callSoon(callback, cast[pointer](0xBCDEF123'u))
|
||||
callSoon(callback, cast[pointer](0xCDEF1234'u))
|
||||
callSoon(callback, cast[pointer](0xDEF12345'u))
|
||||
callSoon(callback, cast[pointer](0xEF123456'u))
|
||||
callSoon(callback, cast[pointer](0xF1234567'u))
|
||||
callSoon(callback, cast[pointer](0x12345678'u))
|
||||
## All callbacks must be processed exactly with 1 poll() call.
|
||||
poll()
|
||||
|
||||
var values = [0x12345678'u, 0x23456789'u, 0x3456789A'u, 0x456789AB'u,
|
||||
0x56789ABC'u, 0x6789ABCD'u, 0x789ABCDE'u, 0x89ABCDEF'u,
|
||||
0x9ABCDEF1'u, 0xABCDEF12'u, 0xBCDEF123'u, 0xCDEF1234'u,
|
||||
0xDEF12345'u, 0xEF123456'u, 0xF1234567'u, 0x12345678'u]
|
||||
var expect = 0'u
|
||||
for item in values:
|
||||
expect = expect xor item
|
||||
|
||||
soonTest == expect
|
||||
|
||||
check test() == true
|
||||
|
||||
test "`Asynchronous dead end` #7193 test":
|
||||
var timers, callbacks: int
|
||||
test2(timers, callbacks)
|
||||
check:
|
||||
timers == CallSoonTests
|
||||
callbacks > CallSoonTests * 2
|
||||
const CallSoonTests = 5
|
||||
proc test() =
|
||||
var
|
||||
timeoutsTest1 = 0
|
||||
timeoutsTest2 = 0
|
||||
stopFlag = false
|
||||
|
||||
var callbackproc: proc(udata: pointer) {.gcsafe, raises: [].}
|
||||
callbackproc = proc (udata: pointer) {.gcsafe, raises: [].} =
|
||||
timeoutsTest2 += 1
|
||||
if not(stopFlag):
|
||||
callSoon(callbackproc)
|
||||
|
||||
proc testProc() {.async.} =
|
||||
for i in 1 .. CallSoonTests:
|
||||
await sleepAsync(10.milliseconds)
|
||||
timeoutsTest1 += 1
|
||||
|
||||
callSoon(callbackproc)
|
||||
waitFor(testProc())
|
||||
stopFlag = true
|
||||
poll()
|
||||
|
||||
check:
|
||||
timeoutsTest1 == CallSoonTests
|
||||
timeoutsTest2 > CallSoonTests * 2
|
||||
|
||||
test()
|
||||
|
||||
test "`callSoon() is not working prior getGlobalDispatcher()` #7192 test":
|
||||
check test3() == true
|
||||
proc test(): bool =
|
||||
var soonTest = 0
|
||||
|
||||
proc testCallback(udata: pointer) =
|
||||
soonTest = 987654321
|
||||
|
||||
callSoon(testCallback)
|
||||
poll()
|
||||
soonTest == 987654321
|
||||
|
||||
check test() == true
|
||||
|
||||
1114
tests/teststream.nim
1114
tests/teststream.nim
File diff suppressed because it is too large
Load Diff
@ -150,9 +150,9 @@ suite "Asynchronous sync primitives test suite":
|
||||
var fut2 = task(lock, 2, n2)
|
||||
var fut3 = task(lock, 3, n3)
|
||||
if cancelIndex == 2:
|
||||
fut2.cancel()
|
||||
fut2.cancelSoon()
|
||||
else:
|
||||
fut3.cancel()
|
||||
fut3.cancelSoon()
|
||||
await allFutures(fut1, fut2, fut3)
|
||||
result = stripe
|
||||
|
||||
|
||||
@ -39,9 +39,12 @@ type
|
||||
Sync, Async
|
||||
|
||||
const
|
||||
TestsCount = 1000
|
||||
TestsCount = when sizeof(int) == 8: 1000 else: 100
|
||||
|
||||
suite "Asynchronous multi-threading sync primitives test suite":
|
||||
teardown:
|
||||
checkLeaks()
|
||||
|
||||
proc setResult(thr: ThreadResultPtr, value: int) =
|
||||
thr[].value = value
|
||||
|
||||
@ -322,19 +325,31 @@ suite "Asynchronous multi-threading sync primitives test suite":
|
||||
|
||||
asyncTest "ThreadSignal: Multiple thread switches [" & $TestsCount &
|
||||
"] test [sync -> sync]":
|
||||
threadSignalTest2(TestsCount, WaitSendKind.Sync, WaitSendKind.Sync)
|
||||
when sizeof(int) == 8:
|
||||
threadSignalTest2(TestsCount, WaitSendKind.Sync, WaitSendKind.Sync)
|
||||
else:
|
||||
skip()
|
||||
|
||||
asyncTest "ThreadSignal: Multiple thread switches [" & $TestsCount &
|
||||
"] test [async -> async]":
|
||||
threadSignalTest2(TestsCount, WaitSendKind.Async, WaitSendKind.Async)
|
||||
when sizeof(int) == 8:
|
||||
threadSignalTest2(TestsCount, WaitSendKind.Async, WaitSendKind.Async)
|
||||
else:
|
||||
skip()
|
||||
|
||||
asyncTest "ThreadSignal: Multiple thread switches [" & $TestsCount &
|
||||
"] test [sync -> async]":
|
||||
threadSignalTest2(TestsCount, WaitSendKind.Sync, WaitSendKind.Async)
|
||||
when sizeof(int) == 8:
|
||||
threadSignalTest2(TestsCount, WaitSendKind.Sync, WaitSendKind.Async)
|
||||
else:
|
||||
skip()
|
||||
|
||||
asyncTest "ThreadSignal: Multiple thread switches [" & $TestsCount &
|
||||
"] test [async -> sync]":
|
||||
threadSignalTest2(TestsCount, WaitSendKind.Async, WaitSendKind.Sync)
|
||||
when sizeof(int) == 8:
|
||||
threadSignalTest2(TestsCount, WaitSendKind.Async, WaitSendKind.Sync)
|
||||
else:
|
||||
skip()
|
||||
|
||||
asyncTest "ThreadSignal: Multiple signals [" & $TestsCount &
|
||||
"] to multiple threads [" & $numProcs & "] test [sync -> sync]":
|
||||
|
||||
@ -89,28 +89,41 @@ suite "Asynchronous timers & steps test suite":
|
||||
$nanoseconds(1_000_000_900) == "1s900ns"
|
||||
$nanoseconds(1_800_700_000) == "1s800ms700us"
|
||||
$nanoseconds(1_800_000_600) == "1s800ms600ns"
|
||||
nanoseconds(1_800_000_600).toString(0) == ""
|
||||
nanoseconds(1_800_000_600).toString(1) == "1s"
|
||||
nanoseconds(1_800_000_600).toString(2) == "1s800ms"
|
||||
|
||||
test "Asynchronous steps test":
|
||||
var futn1 = stepsAsync(-1)
|
||||
var fut0 = stepsAsync(0)
|
||||
var fut1 = stepsAsync(1)
|
||||
var fut2 = stepsAsync(2)
|
||||
var fut3 = stepsAsync(3)
|
||||
|
||||
check:
|
||||
futn1.completed() == true
|
||||
fut0.completed() == true
|
||||
fut1.completed() == false
|
||||
fut2.completed() == false
|
||||
fut3.completed() == false
|
||||
poll()
|
||||
|
||||
# We need `fut` because `stepsAsync` do not power `poll()` anymore.
|
||||
block:
|
||||
var fut {.used.} = sleepAsync(50.milliseconds)
|
||||
poll()
|
||||
|
||||
check:
|
||||
fut1.completed() == true
|
||||
fut2.completed() == false
|
||||
fut3.completed() == false
|
||||
poll()
|
||||
|
||||
block:
|
||||
var fut {.used.} = sleepAsync(50.milliseconds)
|
||||
poll()
|
||||
|
||||
check:
|
||||
fut2.completed() == true
|
||||
fut3.completed() == false
|
||||
poll()
|
||||
|
||||
block:
|
||||
var fut {.used.} = sleepAsync(50.milliseconds)
|
||||
poll()
|
||||
|
||||
check:
|
||||
fut3.completed() == true
|
||||
|
||||
@ -56,7 +56,7 @@ suite "Asynchronous utilities test suite":
|
||||
check:
|
||||
getCount() == 1'u
|
||||
pendingFuturesCount() == 1'u
|
||||
fut3.cancel()
|
||||
discard fut3.tryCancel()
|
||||
poll()
|
||||
check:
|
||||
getCount() == 0'u
|
||||
@ -75,11 +75,6 @@ suite "Asynchronous utilities test suite":
|
||||
pendingFuturesCount() == 2'u
|
||||
|
||||
waitFor fut
|
||||
check:
|
||||
getCount() == 1'u
|
||||
pendingFuturesCount() == 1'u
|
||||
|
||||
poll()
|
||||
check:
|
||||
getCount() == 0'u
|
||||
pendingFuturesCount() == 0'u
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user