mirror of
https://github.com/c-cube/nanoev.git
synced 2025-12-06 11:15:48 -05:00
Merge pull request #1 from c-cube/simon/tiny_httpd
add support for tiny_httpd
This commit is contained in:
commit
14d365547d
25 changed files with 1178 additions and 71 deletions
35
.github/workflows/gh-pages.yml
vendored
Normal file
35
.github/workflows/gh-pages.yml
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
name: github pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main # Set a branch name to trigger deployment
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy doc
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@main
|
||||||
|
|
||||||
|
- name: Use OCaml
|
||||||
|
uses: ocaml/setup-ocaml@v3
|
||||||
|
with:
|
||||||
|
ocaml-compiler: '5.2'
|
||||||
|
dune-cache: true
|
||||||
|
allow-prerelease-opam: true
|
||||||
|
|
||||||
|
# temporary until it's in a release
|
||||||
|
- run: opam pin picos 0.6.0 -y -n
|
||||||
|
|
||||||
|
- run: opam install picos moonpool trace
|
||||||
|
|
||||||
|
- run: opam exec -- odig odoc --cache-dir=_doc/ nanoev
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
uses: peaceiris/actions-gh-pages@v3
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
publish_dir: ./_doc/html
|
||||||
|
destination_dir: .
|
||||||
|
enable_jekyll: false
|
||||||
61
.github/workflows/main.yml
vendored
Normal file
61
.github/workflows/main.yml
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
name: Build and Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run:
|
||||||
|
name: build # build+test on various versions of OCaml, on linux
|
||||||
|
timeout-minutes: 15
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-latest
|
||||||
|
ocaml-compiler:
|
||||||
|
- '5.0'
|
||||||
|
- '5.3'
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@main
|
||||||
|
- name: Use OCaml ${{ matrix.ocaml-compiler }}
|
||||||
|
uses: ocaml/setup-ocaml@v3
|
||||||
|
with:
|
||||||
|
ocaml-compiler: ${{ matrix.ocaml-compiler }}
|
||||||
|
dune-cache: true
|
||||||
|
allow-prerelease-opam: true
|
||||||
|
|
||||||
|
- run: opam pin picos 0.6.0 -y -n
|
||||||
|
- run: opam install picos
|
||||||
|
- run: opam install -t --deps-only .
|
||||||
|
|
||||||
|
- run: opam exec -- dune build @install
|
||||||
|
|
||||||
|
# install some depopts and test deps
|
||||||
|
- run: opam install -t trace
|
||||||
|
|
||||||
|
- run: opam exec -- dune build --profile=release --force @install @runtest
|
||||||
|
|
||||||
|
format:
|
||||||
|
name: format
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
ocaml-compiler:
|
||||||
|
- '5.2'
|
||||||
|
runs-on: 'ubuntu-latest'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@main
|
||||||
|
- name: Use OCaml ${{ matrix.ocaml-compiler }}
|
||||||
|
uses: ocaml/setup-ocaml@v3
|
||||||
|
with:
|
||||||
|
ocaml-compiler: ${{ matrix.ocaml-compiler }}
|
||||||
|
dune-cache: true
|
||||||
|
allow-prerelease-opam: true
|
||||||
|
|
||||||
|
- run: opam install ocamlformat.0.26.2
|
||||||
|
- run: opam exec -- make format-check
|
||||||
|
|
||||||
31
README.md
Normal file
31
README.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# nanoev
|
||||||
|
|
||||||
|
A minimalistic but modular abstraction for IO event loops.
|
||||||
|
|
||||||
|
The goal of this library is to provide a uniform abstraction over multiple
|
||||||
|
system event loops, in a way that plays well with Picos.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Very basic usage would look like this:
|
||||||
|
|
||||||
|
```ocaml
|
||||||
|
module EV = Nanoev_picos
|
||||||
|
|
||||||
|
|
||||||
|
let () =
|
||||||
|
(* use a backend, eg. select *)
|
||||||
|
let ev = Nanoev_unix.create () in
|
||||||
|
|
||||||
|
(* install the backend *)
|
||||||
|
Nanoev_picos.setup_bg_thread ev;
|
||||||
|
|
||||||
|
(* setup a picos scheduler and use EV.read, EV.write, etc. *)
|
||||||
|
…
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Backends
|
||||||
|
|
||||||
|
- [x] select
|
||||||
|
- [ ] uring
|
||||||
21
dune-project
21
dune-project
|
|
@ -1,6 +1,7 @@
|
||||||
(lang dune 2.7)
|
(lang dune 2.7)
|
||||||
|
|
||||||
(name nanoev)
|
(name nanoev)
|
||||||
|
|
||||||
(generate_opam_files true)
|
(generate_opam_files true)
|
||||||
|
|
||||||
(source
|
(source
|
||||||
|
|
@ -16,8 +17,24 @@
|
||||||
|
|
||||||
(package
|
(package
|
||||||
(name nanoev)
|
(name nanoev)
|
||||||
(synopsis "Tiny event loop around `select`")
|
(synopsis "Tiny event loop abstraction")
|
||||||
(depends ocaml dune base-unix)
|
(depends ocaml dune base-unix)
|
||||||
(tags (unix select async)))
|
(depopts
|
||||||
|
(trace (>= 0.7))
|
||||||
|
(picos
|
||||||
|
(and (>= 0.5) (< 0.7))))
|
||||||
|
(tags
|
||||||
|
(unix select async)))
|
||||||
|
|
||||||
|
(package
|
||||||
|
(name nanoev_tiny_httpd)
|
||||||
|
(synopsis "Use nanoev as a basis for tiny_httpd")
|
||||||
|
(depends
|
||||||
|
ocaml
|
||||||
|
dune
|
||||||
|
nanoev
|
||||||
|
picos
|
||||||
|
(tiny_httpd (>= 0.17)))
|
||||||
|
(tags (nanoev http)))
|
||||||
|
|
||||||
; See the complete stanza docs at https://dune.readthedocs.io/en/stable/reference/dune-project/index.html
|
; See the complete stanza docs at https://dune.readthedocs.io/en/stable/reference/dune-project/index.html
|
||||||
|
|
|
||||||
2
echo.sh
Executable file
2
echo.sh
Executable file
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
exec dune exec --display=quiet -- examples/echo/echo.exe $@
|
||||||
4
examples/echo/dune
Normal file
4
examples/echo/dune
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
(executable
|
||||||
|
(name echo)
|
||||||
|
(libraries nanoev nanoev.unix moonpool moonpool.fib trace trace-tef
|
||||||
|
nanoev_tiny_httpd))
|
||||||
280
examples/echo/echo.ml
Normal file
280
examples/echo/echo.ml
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
open Tiny_httpd_core
|
||||||
|
module Log = Tiny_httpd.Log
|
||||||
|
|
||||||
|
let ( let@ ) = ( @@ )
|
||||||
|
let now_ = Unix.gettimeofday
|
||||||
|
|
||||||
|
let alice_text =
|
||||||
|
"CHAPTER I. Down the Rabbit-Hole Alice was beginning to get very tired of \
|
||||||
|
sitting by her sister on the bank, and of having nothing to do: once or \
|
||||||
|
twice she had peeped into the book her sister was reading, but it had no \
|
||||||
|
pictures or conversations in it, <and what is the use of a book,> thought \
|
||||||
|
Alice <without pictures or conversations?> So she was considering in her \
|
||||||
|
own mind (as well as she could, for the hot day made her feel very sleepy \
|
||||||
|
and stupid), whether the pleasure of making a daisy-chain would be worth \
|
||||||
|
the trouble of getting up and picking the daisies, when suddenly a White \
|
||||||
|
Rabbit with pink eyes ran close by her. There was nothing so very \
|
||||||
|
remarkable in that; nor did Alice think it so very much out of the way to \
|
||||||
|
hear the Rabbit say to itself, <Oh dear! Oh dear! I shall be late!> (when \
|
||||||
|
she thought it over afterwards, it occurred to her that she ought to have \
|
||||||
|
wondered at this, but at the time it all seemed quite natural); but when \
|
||||||
|
the Rabbit actually took a watch out of its waistcoat-pocket, and looked at \
|
||||||
|
it, and then hurried on, Alice started to her feet, for it flashed across \
|
||||||
|
her mind that she had never before seen a rabbit with either a \
|
||||||
|
waistcoat-pocket, or a watch to take out of it, and burning with curiosity, \
|
||||||
|
she ran across the field after it, and fortunately was just in time to see \
|
||||||
|
it pop down a large rabbit-hole under the hedge. In another moment down \
|
||||||
|
went Alice after it, never once considering how in the world she was to get \
|
||||||
|
out again. The rabbit-hole went straight on like a tunnel for some way, and \
|
||||||
|
then dipped suddenly down, so suddenly that Alice had not a moment to think \
|
||||||
|
about stopping herself before she found herself falling down a very deep \
|
||||||
|
well. Either the well was very deep, or she fell very slowly, for she had \
|
||||||
|
plenty of time as she went down to look about her and to wonder what was \
|
||||||
|
going to happen next. First, she tried to look down and make out what she \
|
||||||
|
was coming to, but it was too dark to see anything; then she looked at the \
|
||||||
|
sides of the well, and noticed that they were filled with cupboards......"
|
||||||
|
|
||||||
|
(* util: a little middleware collecting statistics *)
|
||||||
|
let middleware_stat () : Server.Middleware.t * (unit -> string) =
|
||||||
|
let n_req = ref 0 in
|
||||||
|
let total_time_ = ref 0. in
|
||||||
|
let parse_time_ = ref 0. in
|
||||||
|
let build_time_ = ref 0. in
|
||||||
|
let write_time_ = ref 0. in
|
||||||
|
|
||||||
|
let m h req ~resp =
|
||||||
|
incr n_req;
|
||||||
|
let t1 = Request.start_time req in
|
||||||
|
let t2 = now_ () in
|
||||||
|
h req ~resp:(fun response ->
|
||||||
|
let t3 = now_ () in
|
||||||
|
resp response;
|
||||||
|
let t4 = now_ () in
|
||||||
|
total_time_ := !total_time_ +. (t4 -. t1);
|
||||||
|
parse_time_ := !parse_time_ +. (t2 -. t1);
|
||||||
|
build_time_ := !build_time_ +. (t3 -. t2);
|
||||||
|
write_time_ := !write_time_ +. (t4 -. t3))
|
||||||
|
and get_stat () =
|
||||||
|
Printf.sprintf
|
||||||
|
"%d requests (average response time: %.3fms = %.3fms + %.3fms + %.3fms)"
|
||||||
|
!n_req
|
||||||
|
(!total_time_ /. float !n_req *. 1e3)
|
||||||
|
(!parse_time_ /. float !n_req *. 1e3)
|
||||||
|
(!build_time_ /. float !n_req *. 1e3)
|
||||||
|
(!write_time_ /. float !n_req *. 1e3)
|
||||||
|
in
|
||||||
|
m, get_stat
|
||||||
|
|
||||||
|
(* ugly AF *)
|
||||||
|
let base64 x =
|
||||||
|
let ic, oc = Unix.open_process "base64" in
|
||||||
|
output_string oc x;
|
||||||
|
flush oc;
|
||||||
|
close_out oc;
|
||||||
|
let r = input_line ic in
|
||||||
|
ignore (Unix.close_process (ic, oc));
|
||||||
|
r
|
||||||
|
|
||||||
|
let setup_logging () =
|
||||||
|
Logs.set_reporter @@ Logs.format_reporter ();
|
||||||
|
Logs.set_level ~all:true (Some Logs.Debug)
|
||||||
|
|
||||||
|
let () =
|
||||||
|
let@ () = Trace_tef.with_setup () in
|
||||||
|
Trace.set_thread_name "main";
|
||||||
|
|
||||||
|
let port_ = ref 8080 in
|
||||||
|
let max_conn = ref 1024 in
|
||||||
|
let j = ref 8 in
|
||||||
|
Arg.parse
|
||||||
|
(Arg.align
|
||||||
|
[
|
||||||
|
"--port", Arg.Set_int port_, " set port";
|
||||||
|
"-p", Arg.Set_int port_, " set port";
|
||||||
|
"-j", Arg.Set_int j, " number of threads";
|
||||||
|
"--debug", Arg.Unit setup_logging, " enable debug";
|
||||||
|
"--max-conns", Arg.Set_int max_conn, " maximum concurrent connections";
|
||||||
|
])
|
||||||
|
(fun _ -> raise (Arg.Bad ""))
|
||||||
|
"echo [option]*";
|
||||||
|
|
||||||
|
let@ pool = Moonpool.Ws_pool.with_ ~num_threads:!j () in
|
||||||
|
let@ _runner = Moonpool_fib.main in
|
||||||
|
|
||||||
|
let ev = Nanoev_unix.create () in
|
||||||
|
Nanoev_picos.setup_bg_thread ev;
|
||||||
|
|
||||||
|
let server =
|
||||||
|
Nanoev_tiny_httpd.create ~new_thread:(Moonpool.run_async pool) ~port:!port_
|
||||||
|
~max_connections:!max_conn ()
|
||||||
|
in
|
||||||
|
|
||||||
|
let m_stats, get_stats = middleware_stat () in
|
||||||
|
Server.add_middleware server ~stage:(`Stage 1) m_stats;
|
||||||
|
|
||||||
|
(* say hello *)
|
||||||
|
Server.add_route_handler ~meth:`GET server
|
||||||
|
Route.(exact "hello" @/ string @/ return)
|
||||||
|
(fun name _req -> Response.make_string (Ok ("hello " ^ name ^ "!\n")));
|
||||||
|
|
||||||
|
(* compressed file access *)
|
||||||
|
Server.add_route_handler ~meth:`GET server
|
||||||
|
Route.(exact "zcat" @/ string_urlencoded @/ return)
|
||||||
|
(fun path _req ->
|
||||||
|
let ic = open_in path in
|
||||||
|
let str = IO.Input.of_in_channel ic in
|
||||||
|
let mime_type =
|
||||||
|
try
|
||||||
|
let p = Unix.open_process_in (Printf.sprintf "file -i -b %S" path) in
|
||||||
|
try
|
||||||
|
let s = [ "Content-Type", String.trim (input_line p) ] in
|
||||||
|
ignore @@ Unix.close_process_in p;
|
||||||
|
s
|
||||||
|
with _ ->
|
||||||
|
ignore @@ Unix.close_process_in p;
|
||||||
|
[]
|
||||||
|
with _ -> []
|
||||||
|
in
|
||||||
|
Response.make_stream ~headers:mime_type (Ok str));
|
||||||
|
|
||||||
|
(* echo request *)
|
||||||
|
Server.add_route_handler server
|
||||||
|
Route.(exact "echo" @/ return)
|
||||||
|
(fun req ->
|
||||||
|
let q =
|
||||||
|
Request.query req
|
||||||
|
|> List.map (fun (k, v) -> Printf.sprintf "%S = %S" k v)
|
||||||
|
|> String.concat ";"
|
||||||
|
in
|
||||||
|
Response.make_string
|
||||||
|
(Ok (Format.asprintf "echo:@ %a@ (query: %s)@." Request.pp req q)));
|
||||||
|
|
||||||
|
(* file upload *)
|
||||||
|
Server.add_route_handler_stream ~meth:`PUT server
|
||||||
|
Route.(exact "upload" @/ string @/ return)
|
||||||
|
(fun path req ->
|
||||||
|
Log.debug (fun k ->
|
||||||
|
k "start upload %S, headers:\n%s\n\n%!" path
|
||||||
|
(Format.asprintf "%a" Headers.pp (Request.headers req)));
|
||||||
|
try
|
||||||
|
let oc = open_out @@ "/tmp/" ^ path in
|
||||||
|
IO.Input.to_chan oc req.Request.body;
|
||||||
|
flush oc;
|
||||||
|
Response.make_string (Ok "uploaded file")
|
||||||
|
with e ->
|
||||||
|
Response.fail ~code:500 "couldn't upload file: %s"
|
||||||
|
(Printexc.to_string e));
|
||||||
|
|
||||||
|
(* protected by login *)
|
||||||
|
Server.add_route_handler server
|
||||||
|
Route.(exact "protected" @/ return)
|
||||||
|
(fun req ->
|
||||||
|
let ok =
|
||||||
|
match Request.get_header req "authorization" with
|
||||||
|
| Some v ->
|
||||||
|
Log.debug (fun k -> k "authenticate with %S" v);
|
||||||
|
v = "Basic " ^ base64 "user:foobar"
|
||||||
|
| None -> false
|
||||||
|
in
|
||||||
|
if ok then (
|
||||||
|
(* FIXME: a logout link *)
|
||||||
|
let s =
|
||||||
|
"<p>hello, this is super secret!</p><a href=\"/logout\">log out</a>"
|
||||||
|
in
|
||||||
|
Response.make_string (Ok s)
|
||||||
|
) else (
|
||||||
|
let headers =
|
||||||
|
Headers.(empty |> set "www-authenticate" "basic realm=\"echo\"")
|
||||||
|
in
|
||||||
|
Response.fail ~code:401 ~headers "invalid"
|
||||||
|
));
|
||||||
|
|
||||||
|
(* logout *)
|
||||||
|
Server.add_route_handler server
|
||||||
|
Route.(exact "logout" @/ return)
|
||||||
|
(fun _req -> Response.fail ~code:401 "logged out");
|
||||||
|
|
||||||
|
(* stats *)
|
||||||
|
Server.add_route_handler server
|
||||||
|
Route.(exact "stats" @/ return)
|
||||||
|
(fun _req ->
|
||||||
|
let stats = get_stats () in
|
||||||
|
Response.make_string @@ Ok stats);
|
||||||
|
|
||||||
|
Server.add_route_handler server
|
||||||
|
Route.(exact "alice" @/ return)
|
||||||
|
(fun _req -> Response.make_string (Ok alice_text));
|
||||||
|
|
||||||
|
(* main page *)
|
||||||
|
Server.add_route_handler server
|
||||||
|
Route.(return)
|
||||||
|
(fun _req ->
|
||||||
|
let open Tiny_httpd_html in
|
||||||
|
let h =
|
||||||
|
html []
|
||||||
|
[
|
||||||
|
head [] [ title [] [ txt "index of echo" ] ];
|
||||||
|
body []
|
||||||
|
[
|
||||||
|
h3 [] [ txt "welcome!" ];
|
||||||
|
p [] [ b [] [ txt "endpoints are:" ] ];
|
||||||
|
ul []
|
||||||
|
[
|
||||||
|
li [] [ pre [] [ txt "/hello/:name (GET)" ] ];
|
||||||
|
li []
|
||||||
|
[
|
||||||
|
pre []
|
||||||
|
[
|
||||||
|
a [ A.href "/echo/" ] [ txt "echo" ];
|
||||||
|
txt " echo back query";
|
||||||
|
];
|
||||||
|
];
|
||||||
|
li []
|
||||||
|
[ pre [] [ txt "/upload/:path (PUT) to upload a file" ] ];
|
||||||
|
li []
|
||||||
|
[
|
||||||
|
pre []
|
||||||
|
[
|
||||||
|
txt
|
||||||
|
"/zcat/:path (GET) to download a file (deflate \
|
||||||
|
transfer-encoding)";
|
||||||
|
];
|
||||||
|
];
|
||||||
|
li []
|
||||||
|
[
|
||||||
|
pre []
|
||||||
|
[
|
||||||
|
a [ A.href "/stats/" ] [ txt "/stats/" ];
|
||||||
|
txt " (GET) to access statistics";
|
||||||
|
];
|
||||||
|
];
|
||||||
|
li []
|
||||||
|
[
|
||||||
|
pre []
|
||||||
|
[
|
||||||
|
a [ A.href "/protected" ] [ txt "/protected" ];
|
||||||
|
txt
|
||||||
|
" (GET) to see a protected page (login: user, \
|
||||||
|
password: foobar)";
|
||||||
|
];
|
||||||
|
];
|
||||||
|
li []
|
||||||
|
[
|
||||||
|
pre []
|
||||||
|
[
|
||||||
|
a [ A.href "/logout" ] [ txt "/logout" ];
|
||||||
|
txt " (POST) to log out";
|
||||||
|
];
|
||||||
|
];
|
||||||
|
];
|
||||||
|
];
|
||||||
|
]
|
||||||
|
in
|
||||||
|
let s = to_string_top h in
|
||||||
|
Response.make_string ~headers:[ "content-type", "text/html" ] @@ Ok s);
|
||||||
|
|
||||||
|
Printf.printf "listening on http://%s:%d\n%!" (Server.addr server)
|
||||||
|
(Server.port server);
|
||||||
|
match Server.run server with
|
||||||
|
| Ok () -> ()
|
||||||
|
| Error e -> raise e
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# This file is generated by dune, edit dune-project instead
|
# This file is generated by dune, edit dune-project instead
|
||||||
opam-version: "2.0"
|
opam-version: "2.0"
|
||||||
synopsis: "Tiny event loop around `select`"
|
synopsis: "Tiny event loop abstraction"
|
||||||
maintainer: ["Simon Cruanes"]
|
maintainer: ["Simon Cruanes"]
|
||||||
authors: ["Simon Cruanes"]
|
authors: ["Simon Cruanes"]
|
||||||
license: "MIT"
|
license: "MIT"
|
||||||
|
|
@ -13,6 +13,10 @@ depends: [
|
||||||
"base-unix"
|
"base-unix"
|
||||||
"odoc" {with-doc}
|
"odoc" {with-doc}
|
||||||
]
|
]
|
||||||
|
depopts: [
|
||||||
|
"trace" {>= "0.7"}
|
||||||
|
"picos" {>= "0.5" & < "0.7"}
|
||||||
|
]
|
||||||
build: [
|
build: [
|
||||||
["dune" "subst"] {dev}
|
["dune" "subst"] {dev}
|
||||||
[
|
[
|
||||||
|
|
|
||||||
32
nanoev_tiny_httpd.opam
Normal file
32
nanoev_tiny_httpd.opam
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# This file is generated by dune, edit dune-project instead
|
||||||
|
opam-version: "2.0"
|
||||||
|
synopsis: "Use nanoev as a basis for tiny_httpd"
|
||||||
|
maintainer: ["Simon Cruanes"]
|
||||||
|
authors: ["Simon Cruanes"]
|
||||||
|
license: "MIT"
|
||||||
|
tags: ["nanoev" "http"]
|
||||||
|
homepage: "https://github.com/c-cube/nanoev"
|
||||||
|
bug-reports: "https://github.com/c-cube/nanoev/issues"
|
||||||
|
depends: [
|
||||||
|
"ocaml"
|
||||||
|
"dune" {>= "2.7"}
|
||||||
|
"nanoev"
|
||||||
|
"picos"
|
||||||
|
"tiny_httpd" {>= "0.17"}
|
||||||
|
"odoc" {with-doc}
|
||||||
|
]
|
||||||
|
build: [
|
||||||
|
["dune" "subst"] {dev}
|
||||||
|
[
|
||||||
|
"dune"
|
||||||
|
"build"
|
||||||
|
"-p"
|
||||||
|
name
|
||||||
|
"-j"
|
||||||
|
jobs
|
||||||
|
"@install"
|
||||||
|
"@runtest" {with-test}
|
||||||
|
"@doc" {with-doc}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
dev-repo: "git+https://github.com/c-cube/nanoev.git"
|
||||||
11
src/core/dune
Normal file
11
src/core/dune
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
(library
|
||||||
|
(name nanoev)
|
||||||
|
(public_name nanoev)
|
||||||
|
(synopsis "Nano ev loop")
|
||||||
|
(libraries
|
||||||
|
unix
|
||||||
|
(select
|
||||||
|
trace_.ml
|
||||||
|
from
|
||||||
|
(trace.core -> trace_.real.ml)
|
||||||
|
(-> trace_.dummy.ml))))
|
||||||
|
|
@ -1,11 +1,28 @@
|
||||||
|
module Trace_ = Trace_
|
||||||
|
|
||||||
|
exception Closed
|
||||||
|
|
||||||
module Impl = struct
|
module Impl = struct
|
||||||
type 'st ops = {
|
type 'st ops = {
|
||||||
clear: 'st -> unit;
|
clear: 'st -> unit;
|
||||||
wakeup_from_outside: 'st -> unit;
|
wakeup_from_outside: 'st -> unit;
|
||||||
|
close: 'st -> Unix.file_descr -> unit;
|
||||||
on_readable:
|
on_readable:
|
||||||
'a 'b. 'st -> Unix.file_descr -> 'a -> 'b -> ('a -> 'b -> unit) -> unit;
|
'a 'b.
|
||||||
|
'st ->
|
||||||
|
Unix.file_descr ->
|
||||||
|
'a ->
|
||||||
|
'b ->
|
||||||
|
(closed:bool -> 'a -> 'b -> unit) ->
|
||||||
|
unit;
|
||||||
on_writable:
|
on_writable:
|
||||||
'a 'b. 'st -> Unix.file_descr -> 'a -> 'b -> ('a -> 'b -> unit) -> unit;
|
'a 'b.
|
||||||
|
'st ->
|
||||||
|
Unix.file_descr ->
|
||||||
|
'a ->
|
||||||
|
'b ->
|
||||||
|
(closed:bool -> 'a -> 'b -> unit) ->
|
||||||
|
unit;
|
||||||
run_after_s: 'a 'b. 'st -> float -> 'a -> 'b -> ('a -> 'b -> unit) -> unit;
|
run_after_s: 'a 'b. 'st -> float -> 'a -> 'b -> ('a -> 'b -> unit) -> unit;
|
||||||
step: 'st -> unit;
|
step: 'st -> unit;
|
||||||
}
|
}
|
||||||
|
|
@ -21,6 +38,7 @@ type t = Impl.t
|
||||||
|
|
||||||
let[@inline] clear (Ev (ops, st)) = ops.clear st
|
let[@inline] clear (Ev (ops, st)) = ops.clear st
|
||||||
let[@inline] wakeup_from_outside (Ev (ops, st)) = ops.wakeup_from_outside st
|
let[@inline] wakeup_from_outside (Ev (ops, st)) = ops.wakeup_from_outside st
|
||||||
|
let[@inline] close (Ev (ops, st)) fd = ops.close st fd
|
||||||
|
|
||||||
let[@inline] on_readable (Ev (ops, st)) fd x y f : unit =
|
let[@inline] on_readable (Ev (ops, st)) fd x y f : unit =
|
||||||
ops.on_readable st fd x y f
|
ops.on_readable st fd x y f
|
||||||
|
|
@ -32,13 +50,3 @@ let[@inline] run_after_s (Ev (ops, st)) time x y f : unit =
|
||||||
ops.run_after_s st time x y f
|
ops.run_after_s st time x y f
|
||||||
|
|
||||||
let[@inline] step (Ev (ops, st)) : unit = ops.step st
|
let[@inline] step (Ev (ops, st)) : unit = ops.step st
|
||||||
|
|
||||||
(*
|
|
||||||
let rec read (self:t) fd buf i len : int =
|
|
||||||
match Unix.read fd buf i len with
|
|
||||||
| n -> n
|
|
||||||
| exception Unix.Unix_error (Unix, _, _) ->
|
|
||||||
read self fd buf i len
|
|
||||||
*)
|
|
||||||
|
|
||||||
|
|
||||||
58
src/core/nanoev.mli
Normal file
58
src/core/nanoev.mli
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
(** Nano event loop *)
|
||||||
|
|
||||||
|
type t
|
||||||
|
|
||||||
|
exception Closed
|
||||||
|
|
||||||
|
module Impl : sig
|
||||||
|
type 'st ops = {
|
||||||
|
clear: 'st -> unit;
|
||||||
|
wakeup_from_outside: 'st -> unit;
|
||||||
|
close: 'st -> Unix.file_descr -> unit;
|
||||||
|
on_readable:
|
||||||
|
'a 'b.
|
||||||
|
'st ->
|
||||||
|
Unix.file_descr ->
|
||||||
|
'a ->
|
||||||
|
'b ->
|
||||||
|
(closed:bool -> 'a -> 'b -> unit) ->
|
||||||
|
unit;
|
||||||
|
on_writable:
|
||||||
|
'a 'b.
|
||||||
|
'st ->
|
||||||
|
Unix.file_descr ->
|
||||||
|
'a ->
|
||||||
|
'b ->
|
||||||
|
(closed:bool -> 'a -> 'b -> unit) ->
|
||||||
|
unit;
|
||||||
|
run_after_s: 'a 'b. 'st -> float -> 'a -> 'b -> ('a -> 'b -> unit) -> unit;
|
||||||
|
step: 'st -> unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
val build : 'a ops -> 'a -> t
|
||||||
|
end
|
||||||
|
|
||||||
|
val clear : t -> unit
|
||||||
|
(** Reset the state *)
|
||||||
|
|
||||||
|
val wakeup_from_outside : t -> unit
|
||||||
|
|
||||||
|
val step : t -> unit
|
||||||
|
(** Run one step of the event loop until something happens *)
|
||||||
|
|
||||||
|
val close : t -> Unix.file_descr -> unit
|
||||||
|
(** Close the file descriptor and clean it up *)
|
||||||
|
|
||||||
|
val on_readable :
|
||||||
|
t -> Unix.file_descr -> 'a -> 'b -> (closed:bool -> 'a -> 'b -> unit) -> unit
|
||||||
|
|
||||||
|
val on_writable :
|
||||||
|
t -> Unix.file_descr -> 'a -> 'b -> (closed:bool -> 'a -> 'b -> unit) -> unit
|
||||||
|
|
||||||
|
val run_after_s : t -> float -> 'a -> 'b -> ('a -> 'b -> unit) -> unit
|
||||||
|
|
||||||
|
(**/**)
|
||||||
|
|
||||||
|
module Trace_ = Trace_
|
||||||
|
|
||||||
|
(**/**)
|
||||||
3
src/core/trace_.dummy.ml
Normal file
3
src/core/trace_.dummy.ml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
let[@inline] with_span ?data:_ ~__FILE__:_ ~__LINE__:_ _name f = f 0L
|
||||||
|
let[@inline] message ?data:_ _ = ()
|
||||||
|
let set_thread_name (_ : string) = ()
|
||||||
5
src/core/trace_.real.ml
Normal file
5
src/core/trace_.real.ml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
let[@inline] with_span ?data ~__FILE__ ~__LINE__ name f =
|
||||||
|
Trace_core.with_span ?data ~__FILE__ ~__LINE__ name f
|
||||||
|
|
||||||
|
let[@inline] message ?data m = Trace_core.message ?data m
|
||||||
|
let set_thread_name name = Trace_core.set_thread_name name
|
||||||
5
src/dune
5
src/dune
|
|
@ -1,5 +0,0 @@
|
||||||
(library
|
|
||||||
(name nanoev)
|
|
||||||
(public_name nanoev)
|
|
||||||
(synopsis "Nano ev loop")
|
|
||||||
(libraries unix))
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
(** Nano event loop *)
|
|
||||||
|
|
||||||
type t
|
|
||||||
|
|
||||||
module Impl : sig
|
|
||||||
type 'st ops = {
|
|
||||||
clear: 'st -> unit;
|
|
||||||
wakeup_from_outside: 'st -> unit;
|
|
||||||
on_readable:
|
|
||||||
'a 'b. 'st -> Unix.file_descr -> 'a -> 'b -> ('a -> 'b -> unit) -> unit;
|
|
||||||
on_writable:
|
|
||||||
'a 'b. 'st -> Unix.file_descr -> 'a -> 'b -> ('a -> 'b -> unit) -> unit;
|
|
||||||
run_after_s: 'a 'b. 'st -> float -> 'a -> 'b -> ('a -> 'b -> unit) -> unit;
|
|
||||||
step: 'st -> unit;
|
|
||||||
}
|
|
||||||
|
|
||||||
val build : 'a ops -> 'a -> t
|
|
||||||
end
|
|
||||||
|
|
||||||
val clear : t -> unit
|
|
||||||
(** Reset the state *)
|
|
||||||
|
|
||||||
val wakeup_from_outside : t -> unit
|
|
||||||
|
|
||||||
val step : t -> unit
|
|
||||||
(** Run one step of the event loop until something happens *)
|
|
||||||
|
|
||||||
val on_readable : t -> Unix.file_descr -> 'a -> 'b -> ('a -> 'b -> unit) -> unit
|
|
||||||
val on_writable : t -> Unix.file_descr -> 'a -> 'b -> ('a -> 'b -> unit) -> unit
|
|
||||||
val run_after_s : t -> float -> 'a -> 'b -> ('a -> 'b -> unit) -> unit
|
|
||||||
5
src/picos/dune
Normal file
5
src/picos/dune
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
(library
|
||||||
|
(name nanoev_picos)
|
||||||
|
(public_name nanoev.picos)
|
||||||
|
(optional) ; picos
|
||||||
|
(libraries threads picos nanoev))
|
||||||
146
src/picos/nanoev_picos.ml
Normal file
146
src/picos/nanoev_picos.ml
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
open struct
|
||||||
|
module Trace_ = Nanoev.Trace_
|
||||||
|
|
||||||
|
let ( let@ ) = ( @@ )
|
||||||
|
end
|
||||||
|
|
||||||
|
exception Closed = Nanoev.Closed
|
||||||
|
|
||||||
|
module Global_ = struct
|
||||||
|
type st =
|
||||||
|
| None
|
||||||
|
| Some of {
|
||||||
|
active: bool Atomic.t;
|
||||||
|
nanoev: Nanoev.t;
|
||||||
|
th: Thread.t;
|
||||||
|
}
|
||||||
|
|
||||||
|
let st : st Atomic.t = Atomic.make None
|
||||||
|
let lock = Mutex.create ()
|
||||||
|
|
||||||
|
let with_lock lock f =
|
||||||
|
Mutex.lock lock;
|
||||||
|
match f () with
|
||||||
|
| exception e ->
|
||||||
|
Mutex.unlock lock;
|
||||||
|
raise e
|
||||||
|
| x ->
|
||||||
|
Mutex.unlock lock;
|
||||||
|
x
|
||||||
|
|
||||||
|
let bg_thread_ ~active ~evloop () : unit =
|
||||||
|
Trace_.set_thread_name "nanoev.picos.bg-thread";
|
||||||
|
while Atomic.get active do
|
||||||
|
Nanoev.step evloop
|
||||||
|
done
|
||||||
|
|
||||||
|
let[@inline] has_bg_thread () = Atomic.get st <> None
|
||||||
|
|
||||||
|
let setup_bg_thread (ev : Nanoev.t) : unit =
|
||||||
|
let@ () = with_lock lock in
|
||||||
|
(* shutdown existing thread, if any *)
|
||||||
|
(match Atomic.get st with
|
||||||
|
| Some st ->
|
||||||
|
Atomic.set st.active false;
|
||||||
|
Nanoev.wakeup_from_outside st.nanoev;
|
||||||
|
Thread.join st.th
|
||||||
|
| None -> ());
|
||||||
|
|
||||||
|
(* start new bg thread *)
|
||||||
|
let active = Atomic.make true in
|
||||||
|
Atomic.set st
|
||||||
|
@@ Some
|
||||||
|
{
|
||||||
|
active;
|
||||||
|
nanoev = ev;
|
||||||
|
th = Thread.create (bg_thread_ ~active ~evloop:ev) ();
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let has_bg_thread = Global_.has_bg_thread
|
||||||
|
let setup_bg_thread = Global_.setup_bg_thread
|
||||||
|
|
||||||
|
let[@inline] get_loop_exn_ () : Nanoev.t =
|
||||||
|
match Atomic.get Global_.st with
|
||||||
|
| None -> failwith "No nanoev loop installed."
|
||||||
|
| Some st -> st.nanoev
|
||||||
|
|
||||||
|
let[@inline] unwrap_ = function
|
||||||
|
| None -> ()
|
||||||
|
| Some (exn, bt) -> Printexc.raise_with_backtrace exn bt
|
||||||
|
|
||||||
|
let retry_read_ fd f =
|
||||||
|
let ev = get_loop_exn_ () in
|
||||||
|
let[@unroll 1] rec loop () =
|
||||||
|
match f () with
|
||||||
|
| res -> res
|
||||||
|
| exception
|
||||||
|
Unix.Unix_error ((Unix.EAGAIN | Unix.EWOULDBLOCK | Unix.EINTR), _, _) ->
|
||||||
|
Trace_.message "read must wait";
|
||||||
|
let trigger = Picos.Trigger.create () in
|
||||||
|
let closed_r = ref false in
|
||||||
|
Nanoev.on_readable ev fd trigger closed_r (fun ~closed trigger closed_r ->
|
||||||
|
closed_r := closed;
|
||||||
|
Picos.Trigger.signal trigger);
|
||||||
|
Picos.Trigger.await trigger |> unwrap_;
|
||||||
|
if !closed_r then raise Closed;
|
||||||
|
loop ()
|
||||||
|
in
|
||||||
|
loop ()
|
||||||
|
|
||||||
|
let retry_write_ fd f =
|
||||||
|
let ev = get_loop_exn_ () in
|
||||||
|
let rec loop () =
|
||||||
|
match f () with
|
||||||
|
| res -> res
|
||||||
|
| exception
|
||||||
|
Unix.Unix_error ((Unix.EAGAIN | Unix.EWOULDBLOCK | Unix.EINTR), _, _) ->
|
||||||
|
Trace_.message "write must wait";
|
||||||
|
let trigger = Picos.Trigger.create () in
|
||||||
|
let closed_r = ref false in
|
||||||
|
Nanoev.on_writable ev fd trigger closed_r (fun ~closed trigger closed_r ->
|
||||||
|
closed_r := closed;
|
||||||
|
Picos.Trigger.signal trigger);
|
||||||
|
Picos.Trigger.await trigger |> unwrap_;
|
||||||
|
if !closed_r then raise Closed;
|
||||||
|
loop ()
|
||||||
|
in
|
||||||
|
loop ()
|
||||||
|
|
||||||
|
let read fd buf i len : int =
|
||||||
|
try
|
||||||
|
retry_read_ fd (fun () ->
|
||||||
|
Trace_.message "read";
|
||||||
|
Unix.read fd buf i len)
|
||||||
|
with Closed -> 0
|
||||||
|
|
||||||
|
let close fd =
|
||||||
|
Unix.close fd;
|
||||||
|
let ev = get_loop_exn_ () in
|
||||||
|
Nanoev.close ev fd
|
||||||
|
|
||||||
|
let accept fd =
|
||||||
|
try
|
||||||
|
retry_read_ fd (fun () ->
|
||||||
|
Trace_.message "accept";
|
||||||
|
Unix.accept fd)
|
||||||
|
with Unix.Unix_error ((Unix.ESHUTDOWN | Unix.ECONNABORTED), _, _) ->
|
||||||
|
raise Closed
|
||||||
|
|
||||||
|
let write fd buf i len : int =
|
||||||
|
try
|
||||||
|
retry_write_ fd (fun () ->
|
||||||
|
Trace_.message "write";
|
||||||
|
Unix.write fd buf i len)
|
||||||
|
with Closed -> 0
|
||||||
|
|
||||||
|
let connect fd addr = retry_write_ fd (fun () -> Unix.connect fd addr)
|
||||||
|
|
||||||
|
let sleep t =
|
||||||
|
if t > 0. then (
|
||||||
|
let ev = get_loop_exn_ () in
|
||||||
|
let trigger = Picos.Trigger.create () in
|
||||||
|
Nanoev.run_after_s ev t trigger () (fun trigger () ->
|
||||||
|
Picos.Trigger.signal trigger);
|
||||||
|
Picos.Trigger.await trigger |> unwrap_
|
||||||
|
)
|
||||||
32
src/picos/nanoev_picos.mli
Normal file
32
src/picos/nanoev_picos.mli
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
(** Basic interface with picos *)
|
||||||
|
|
||||||
|
val setup_bg_thread : Nanoev.t -> unit
|
||||||
|
(** Install this event loop in a background thread *)
|
||||||
|
|
||||||
|
val has_bg_thread : unit -> bool
|
||||||
|
(** [has_bg_thread ()] is [true] iff a background thread is running a nanoev loop *)
|
||||||
|
|
||||||
|
(** {2 Non blocking IO primitives} *)
|
||||||
|
|
||||||
|
val read : Unix.file_descr -> bytes -> int -> int -> int
|
||||||
|
(** Read from the non blocking FD.
|
||||||
|
@raise Nanoev.Closed if the FD is closed
|
||||||
|
@raise Unix.Unix_error for other errors *)
|
||||||
|
|
||||||
|
val write : Unix.file_descr -> bytes -> int -> int -> int
|
||||||
|
(** Write into the non blocking FD.
|
||||||
|
@raise Nanoev.Closed if the FD is closed
|
||||||
|
@raise Unix.Unix_error for other errors *)
|
||||||
|
|
||||||
|
val close : Unix.file_descr -> unit
|
||||||
|
(** Close the file descriptor
|
||||||
|
@raise Unix.Unix_error when it fails *)
|
||||||
|
|
||||||
|
val connect : Unix.file_descr -> Unix.sockaddr -> unit
|
||||||
|
|
||||||
|
val accept : Unix.file_descr -> Unix.file_descr * Unix.sockaddr
|
||||||
|
(** Accept a connection on this fd.
|
||||||
|
@raise Nanoev.Closed if the FD is closed.
|
||||||
|
@raise Unix.Unix_error for other errors *)
|
||||||
|
|
||||||
|
val sleep : float -> unit
|
||||||
10
src/tiny_httpd/dune
Normal file
10
src/tiny_httpd/dune
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
(library
|
||||||
|
(name nanoev_tiny_httpd)
|
||||||
|
(public_name nanoev_tiny_httpd)
|
||||||
|
(libraries
|
||||||
|
threads
|
||||||
|
picos
|
||||||
|
(re_export nanoev)
|
||||||
|
nanoev.picos
|
||||||
|
(re_export iostream)
|
||||||
|
(re_export tiny_httpd)))
|
||||||
328
src/tiny_httpd/nanoev_tiny_httpd.ml
Normal file
328
src/tiny_httpd/nanoev_tiny_httpd.ml
Normal file
|
|
@ -0,0 +1,328 @@
|
||||||
|
module TH = Tiny_httpd_core
|
||||||
|
module EV = Nanoev_picos
|
||||||
|
module Log = TH.Log
|
||||||
|
module Slice = Iostream.Slice
|
||||||
|
module Pool = TH.Pool
|
||||||
|
module Buf = TH.Buf
|
||||||
|
|
||||||
|
let unwrap_ = function
|
||||||
|
| None -> ()
|
||||||
|
| Some (exn, bt) -> Printexc.raise_with_backtrace exn bt
|
||||||
|
|
||||||
|
(** Non blocking semaphore *)
|
||||||
|
module Sem_ = struct
|
||||||
|
type t = {
|
||||||
|
mutable n: int;
|
||||||
|
max: int;
|
||||||
|
waiting: Picos.Trigger.t Queue.t;
|
||||||
|
mutex: Mutex.t;
|
||||||
|
}
|
||||||
|
|
||||||
|
let create n =
|
||||||
|
if n <= 0 then invalid_arg "Semaphore.create";
|
||||||
|
{ n; max = n; mutex = Mutex.create (); waiting = Queue.create () }
|
||||||
|
|
||||||
|
let acquire self =
|
||||||
|
Mutex.lock self.mutex;
|
||||||
|
while self.n = 0 do
|
||||||
|
let tr = Picos.Trigger.create () in
|
||||||
|
Queue.push tr self.waiting;
|
||||||
|
Mutex.unlock self.mutex;
|
||||||
|
let res = Picos.Trigger.await tr in
|
||||||
|
unwrap_ res;
|
||||||
|
Mutex.lock self.mutex
|
||||||
|
done;
|
||||||
|
assert (self.n > 0);
|
||||||
|
self.n <- self.n - 1;
|
||||||
|
Mutex.unlock self.mutex
|
||||||
|
|
||||||
|
let release self =
|
||||||
|
Mutex.lock self.mutex;
|
||||||
|
self.n <- self.n + 1;
|
||||||
|
Option.iter Picos.Trigger.signal (Queue.take_opt self.waiting);
|
||||||
|
Mutex.unlock self.mutex
|
||||||
|
|
||||||
|
let num_acquired self = self.max - self.n
|
||||||
|
end
|
||||||
|
|
||||||
|
module Out = struct
|
||||||
|
open Iostream
|
||||||
|
|
||||||
|
class type t = Out_buf.t
|
||||||
|
|
||||||
|
class of_unix_fd ?(close_noerr = false) ~closed ~(buf : Slice.t)
|
||||||
|
(fd : Unix.file_descr) :
|
||||||
|
t =
|
||||||
|
object
|
||||||
|
inherit Out_buf.t_from_output ~bytes:buf.bytes ()
|
||||||
|
|
||||||
|
method private output_underlying bs i len0 =
|
||||||
|
let i = ref i in
|
||||||
|
let len = ref len0 in
|
||||||
|
while !len > 0 do
|
||||||
|
match EV.write fd bs !i !len with
|
||||||
|
| 0 -> failwith "write failed"
|
||||||
|
| n ->
|
||||||
|
i := !i + n;
|
||||||
|
len := !len - n
|
||||||
|
| exception
|
||||||
|
Unix.Unix_error
|
||||||
|
( (( Unix.EBADF | Unix.ENOTCONN | Unix.ESHUTDOWN
|
||||||
|
| Unix.ECONNRESET | Unix.EPIPE ) as err),
|
||||||
|
fn,
|
||||||
|
_ ) ->
|
||||||
|
failwith
|
||||||
|
@@ Printf.sprintf "write failed in %s: %s" fn
|
||||||
|
(Unix.error_message err)
|
||||||
|
done
|
||||||
|
|
||||||
|
method private close_underlying () =
|
||||||
|
if not (Atomic.exchange closed true) then
|
||||||
|
if close_noerr then (
|
||||||
|
try EV.close fd with _ -> ()
|
||||||
|
) else
|
||||||
|
EV.close fd
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module In = struct
|
||||||
|
open Iostream
|
||||||
|
|
||||||
|
class type t = In_buf.t
|
||||||
|
|
||||||
|
let of_unix_fd ?(close_noerr = false) ~closed ~(buf : Slice.t)
|
||||||
|
(fd : Unix.file_descr) : t =
|
||||||
|
let eof = ref false in
|
||||||
|
object
|
||||||
|
inherit Iostream.In_buf.t_from_refill ~bytes:buf.bytes ()
|
||||||
|
|
||||||
|
method private refill (slice : Slice.t) =
|
||||||
|
if not !eof then (
|
||||||
|
slice.off <- 0;
|
||||||
|
let continue = ref true in
|
||||||
|
while !continue do
|
||||||
|
match EV.read fd slice.bytes 0 (Bytes.length slice.bytes) with
|
||||||
|
| n ->
|
||||||
|
slice.len <- n;
|
||||||
|
continue := false
|
||||||
|
| exception
|
||||||
|
Unix.Unix_error
|
||||||
|
( ( Unix.EBADF | Unix.ENOTCONN | Unix.ESHUTDOWN
|
||||||
|
| Unix.ECONNRESET | Unix.EPIPE ),
|
||||||
|
_,
|
||||||
|
_ ) ->
|
||||||
|
eof := true;
|
||||||
|
continue := false
|
||||||
|
done;
|
||||||
|
(* Printf.eprintf "read returned %d B\n%!" !n; *)
|
||||||
|
if slice.len = 0 then eof := true
|
||||||
|
)
|
||||||
|
|
||||||
|
method close () =
|
||||||
|
if not (Atomic.exchange closed true) then (
|
||||||
|
eof := true;
|
||||||
|
if close_noerr then (
|
||||||
|
try EV.close fd with _ -> ()
|
||||||
|
) else
|
||||||
|
EV.close fd
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module Unix_tcp_server_ = struct
|
||||||
|
let get_addr_ sock =
|
||||||
|
match Unix.getsockname sock with
|
||||||
|
| Unix.ADDR_INET (addr, port) -> addr, port
|
||||||
|
| _ -> invalid_arg "httpd: address is not INET"
|
||||||
|
|
||||||
|
type t = {
|
||||||
|
addr: string;
|
||||||
|
port: int;
|
||||||
|
buf_pool: Buf.t Pool.t;
|
||||||
|
slice_pool: Slice.t Pool.t;
|
||||||
|
max_connections: int;
|
||||||
|
sem_max_connections: Sem_.t;
|
||||||
|
(** semaphore to restrict the number of active concurrent connections *)
|
||||||
|
mutable sock: Unix.file_descr option; (** Socket *)
|
||||||
|
new_thread: (unit -> unit) -> unit;
|
||||||
|
timeout: float;
|
||||||
|
masksigpipe: bool;
|
||||||
|
mutable running: bool; (* TODO: use an atomic? *)
|
||||||
|
}
|
||||||
|
|
||||||
|
let shutdown_silent_ fd =
|
||||||
|
try Unix.shutdown fd Unix.SHUTDOWN_ALL with _ -> ()
|
||||||
|
|
||||||
|
let close_silent_ fd = try Unix.close fd with _ -> ()
|
||||||
|
|
||||||
|
let to_tcp_server (self : t) : TH.IO.TCP_server.builder =
|
||||||
|
{
|
||||||
|
TH.IO.TCP_server.serve =
|
||||||
|
(fun ~after_init ~handle () : unit ->
|
||||||
|
if self.masksigpipe && not Sys.win32 then
|
||||||
|
ignore (Unix.sigprocmask Unix.SIG_BLOCK [ Sys.sigpipe ] : _ list);
|
||||||
|
let sock, should_bind =
|
||||||
|
match self.sock with
|
||||||
|
| Some s ->
|
||||||
|
s, false
|
||||||
|
(* Because we're getting a socket from the caller (e.g. systemd) *)
|
||||||
|
| None ->
|
||||||
|
let s =
|
||||||
|
Unix.socket
|
||||||
|
(if TH.Util.is_ipv6_str self.addr then
|
||||||
|
Unix.PF_INET6
|
||||||
|
else
|
||||||
|
Unix.PF_INET)
|
||||||
|
Unix.SOCK_STREAM 0
|
||||||
|
in
|
||||||
|
s, true (* Because we're creating the socket ourselves *)
|
||||||
|
in
|
||||||
|
Unix.set_nonblock sock;
|
||||||
|
Unix.setsockopt_optint sock Unix.SO_LINGER None;
|
||||||
|
if should_bind then (
|
||||||
|
let inet_addr = Unix.inet_addr_of_string self.addr in
|
||||||
|
Unix.setsockopt sock Unix.SO_REUSEADDR true;
|
||||||
|
Unix.bind sock (Unix.ADDR_INET (inet_addr, self.port));
|
||||||
|
let n_listen = 2 * self.max_connections in
|
||||||
|
Unix.listen sock n_listen
|
||||||
|
);
|
||||||
|
|
||||||
|
self.sock <- Some sock;
|
||||||
|
|
||||||
|
let tcp_server =
|
||||||
|
{
|
||||||
|
TH.IO.TCP_server.stop = (fun () -> self.running <- false);
|
||||||
|
running = (fun () -> self.running);
|
||||||
|
active_connections =
|
||||||
|
(fun () -> Sem_.num_acquired self.sem_max_connections);
|
||||||
|
endpoint =
|
||||||
|
(fun () ->
|
||||||
|
let addr, port = get_addr_ sock in
|
||||||
|
Unix.string_of_inet_addr addr, port);
|
||||||
|
}
|
||||||
|
in
|
||||||
|
after_init tcp_server;
|
||||||
|
|
||||||
|
(* how to handle a single client *)
|
||||||
|
let handle_client_ (client_sock : Unix.file_descr)
|
||||||
|
(client_addr : Unix.sockaddr) : unit =
|
||||||
|
Log.debug (fun k ->
|
||||||
|
k "t[%d]: serving new client on %s"
|
||||||
|
(Thread.id @@ Thread.self ())
|
||||||
|
(TH.Util.show_sockaddr client_addr));
|
||||||
|
|
||||||
|
if self.masksigpipe && not Sys.win32 then
|
||||||
|
ignore (Unix.sigprocmask Unix.SIG_BLOCK [ Sys.sigpipe ] : _ list);
|
||||||
|
Unix.set_nonblock client_sock;
|
||||||
|
Unix.setsockopt client_sock Unix.TCP_NODELAY true;
|
||||||
|
Unix.(setsockopt_float client_sock SO_RCVTIMEO self.timeout);
|
||||||
|
Unix.(setsockopt_float client_sock SO_SNDTIMEO self.timeout);
|
||||||
|
|
||||||
|
Pool.with_resource self.slice_pool @@ fun ic_buf ->
|
||||||
|
Pool.with_resource self.slice_pool @@ fun oc_buf ->
|
||||||
|
let closed = Atomic.make false in
|
||||||
|
|
||||||
|
let oc =
|
||||||
|
new Out.of_unix_fd
|
||||||
|
~close_noerr:true ~closed ~buf:oc_buf client_sock
|
||||||
|
in
|
||||||
|
let ic =
|
||||||
|
In.of_unix_fd ~close_noerr:true ~closed ~buf:ic_buf client_sock
|
||||||
|
in
|
||||||
|
handle.handle ~client_addr ic oc
|
||||||
|
in
|
||||||
|
|
||||||
|
Unix.set_nonblock sock;
|
||||||
|
while self.running do
|
||||||
|
match EV.accept sock with
|
||||||
|
| client_sock, client_addr ->
|
||||||
|
(* limit concurrency *)
|
||||||
|
Sem_.acquire self.sem_max_connections;
|
||||||
|
(* Block INT/HUP while cloning to avoid children handling them.
|
||||||
|
When thread gets them, our Unix.accept raises neatly. *)
|
||||||
|
if not Sys.win32 then
|
||||||
|
ignore Unix.(sigprocmask SIG_BLOCK Sys.[ sigint; sighup ]);
|
||||||
|
self.new_thread (fun () ->
|
||||||
|
try
|
||||||
|
handle_client_ client_sock client_addr;
|
||||||
|
Log.debug (fun k ->
|
||||||
|
k "t[%d]: done with client on %s, exiting"
|
||||||
|
(Thread.id @@ Thread.self ())
|
||||||
|
@@ TH.Util.show_sockaddr client_addr);
|
||||||
|
shutdown_silent_ client_sock;
|
||||||
|
close_silent_ client_sock;
|
||||||
|
Sem_.release self.sem_max_connections
|
||||||
|
with e ->
|
||||||
|
let bt = Printexc.get_raw_backtrace () in
|
||||||
|
shutdown_silent_ client_sock;
|
||||||
|
close_silent_ client_sock;
|
||||||
|
Sem_.release self.sem_max_connections;
|
||||||
|
Log.error (fun k ->
|
||||||
|
k
|
||||||
|
"@[<v>Handler: uncaught exception for client %s:@ \
|
||||||
|
%s@ %s@]"
|
||||||
|
(TH.Util.show_sockaddr client_addr)
|
||||||
|
(Printexc.to_string e)
|
||||||
|
(Printexc.raw_backtrace_to_string bt)));
|
||||||
|
if not Sys.win32 then
|
||||||
|
ignore Unix.(sigprocmask SIG_UNBLOCK Sys.[ sigint; sighup ])
|
||||||
|
| exception e ->
|
||||||
|
Log.error (fun k ->
|
||||||
|
k "Unix.accept raised an exception: %s" (Printexc.to_string e))
|
||||||
|
done;
|
||||||
|
|
||||||
|
(* Wait for all threads to be done: this only works if all threads are done. *)
|
||||||
|
Unix.close sock;
|
||||||
|
(* TODO? *)
|
||||||
|
(* Sem_.acquire self.sem_max_connections.max self.sem_max_connections; *)
|
||||||
|
());
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
open struct
|
||||||
|
let get_max_connection_ ?(max_connections = 2048) () : int =
|
||||||
|
let max_connections = max 4 max_connections in
|
||||||
|
max_connections
|
||||||
|
|
||||||
|
let clear_slice (slice : Slice.t) =
|
||||||
|
Bytes.fill slice.bytes 0 (Bytes.length slice.bytes) '\x00';
|
||||||
|
slice.off <- 0;
|
||||||
|
slice.len <- 0
|
||||||
|
end
|
||||||
|
|
||||||
|
let create ?(masksigpipe = not Sys.win32) ?max_connections ?(timeout = 0.0)
|
||||||
|
?buf_size ?(get_time_s = Unix.gettimeofday) ?(addr = "127.0.0.1")
|
||||||
|
?(port = 8080) ?sock ?middlewares ~new_thread () : TH.Server.t =
|
||||||
|
let max_connections = get_max_connection_ ?max_connections () in
|
||||||
|
let server =
|
||||||
|
{
|
||||||
|
Unix_tcp_server_.addr;
|
||||||
|
new_thread;
|
||||||
|
buf_pool =
|
||||||
|
Pool.create ~clear:Buf.clear_and_zero
|
||||||
|
~mk_item:(fun () -> Buf.create ?size:buf_size ())
|
||||||
|
();
|
||||||
|
slice_pool =
|
||||||
|
Pool.create ~clear:clear_slice
|
||||||
|
~mk_item:
|
||||||
|
(let buf_size = Option.value buf_size ~default:4096 in
|
||||||
|
fun () -> Slice.create buf_size)
|
||||||
|
();
|
||||||
|
running = true;
|
||||||
|
port;
|
||||||
|
sock;
|
||||||
|
max_connections;
|
||||||
|
sem_max_connections = Sem_.create max_connections;
|
||||||
|
masksigpipe;
|
||||||
|
timeout;
|
||||||
|
}
|
||||||
|
in
|
||||||
|
let tcp_server_builder = Unix_tcp_server_.to_tcp_server server in
|
||||||
|
let module B = struct
|
||||||
|
let init_addr () = addr
|
||||||
|
let init_port () = port
|
||||||
|
let get_time_s = get_time_s
|
||||||
|
let tcp_server () = tcp_server_builder
|
||||||
|
end in
|
||||||
|
let backend = (module B : TH.Server.IO_BACKEND) in
|
||||||
|
TH.Server.create_from ?buf_size ?middlewares ~backend ()
|
||||||
15
src/tiny_httpd/nanoev_tiny_httpd.mli
Normal file
15
src/tiny_httpd/nanoev_tiny_httpd.mli
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
module TH = Tiny_httpd_core
|
||||||
|
|
||||||
|
val create :
|
||||||
|
?masksigpipe:bool ->
|
||||||
|
?max_connections:int ->
|
||||||
|
?timeout:float ->
|
||||||
|
?buf_size:int ->
|
||||||
|
?get_time_s:(unit -> float) ->
|
||||||
|
?addr:string ->
|
||||||
|
?port:int ->
|
||||||
|
?sock:Unix.file_descr ->
|
||||||
|
?middlewares:([ `Encoding | `Stage of int ] * TH.Server.Middleware.t) list ->
|
||||||
|
new_thread:((unit -> unit) -> unit) ->
|
||||||
|
unit ->
|
||||||
|
TH.Server.t
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
(* module type BACKEND = Intf.BACKEND *)
|
open struct
|
||||||
|
module Trace_ = Nanoev.Trace_
|
||||||
|
|
||||||
let ( let@ ) = ( @@ )
|
let ( let@ ) = ( @@ )
|
||||||
let now_ : unit -> float = Unix.gettimeofday
|
let now_ : unit -> float = Unix.gettimeofday
|
||||||
|
end
|
||||||
|
|
||||||
(** Callback list *)
|
(** Callback list *)
|
||||||
type cbs =
|
type cbs =
|
||||||
| Nil
|
| Nil
|
||||||
| Sub : 'a * 'b * ('a -> 'b -> unit) * cbs -> cbs
|
| Sub : 'a * 'b * (closed:bool -> 'a -> 'b -> unit) * cbs -> cbs
|
||||||
|
|
||||||
let[@inline] cb_is_empty = function
|
let[@inline] cb_is_empty = function
|
||||||
| Nil -> true
|
| Nil -> true
|
||||||
|
|
@ -42,6 +44,18 @@ type st = {
|
||||||
lock: Mutex.t;
|
lock: Mutex.t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let rec perform_cbs ~closed = function
|
||||||
|
| Nil -> ()
|
||||||
|
| Sub (x, y, f, tail) ->
|
||||||
|
f ~closed x y;
|
||||||
|
perform_cbs ~closed tail
|
||||||
|
|
||||||
|
let rec perform_cbs_closed ~closed = function
|
||||||
|
| Nil -> ()
|
||||||
|
| Sub (x, y, f, tail) ->
|
||||||
|
f ~closed x y;
|
||||||
|
perform_cbs_closed ~closed tail
|
||||||
|
|
||||||
let leq_timer (Timer a) (Timer b) = a.deadline <= b.deadline
|
let leq_timer (Timer a) (Timer b) = a.deadline <= b.deadline
|
||||||
|
|
||||||
let create_st () : st =
|
let create_st () : st =
|
||||||
|
|
@ -80,10 +94,12 @@ let clear (self : st) =
|
||||||
()
|
()
|
||||||
|
|
||||||
let wakeup_from_outside (self : st) : unit =
|
let wakeup_from_outside (self : st) : unit =
|
||||||
if not (Atomic.exchange self.wakeup_triggered true) then (
|
if not (Atomic.exchange self.wakeup_triggered true) then
|
||||||
|
let@ _sp =
|
||||||
|
Trace_.with_span ~__FILE__ ~__LINE__ "nanoev.wakeup-from-outside"
|
||||||
|
in
|
||||||
let b = Bytes.make 1 '!' in
|
let b = Bytes.make 1 '!' in
|
||||||
ignore (Unix.write self.wakeup_wr b 0 1 : int)
|
ignore (Unix.write self.wakeup_wr b 0 1 : int)
|
||||||
)
|
|
||||||
|
|
||||||
let get_fd_ (self : st) fd : per_fd =
|
let get_fd_ (self : st) fd : per_fd =
|
||||||
match Hashtbl.find self.fds fd with
|
match Hashtbl.find self.fds fd with
|
||||||
|
|
@ -93,7 +109,27 @@ let get_fd_ (self : st) fd : per_fd =
|
||||||
Hashtbl.add self.fds fd per_fd;
|
Hashtbl.add self.fds fd per_fd;
|
||||||
per_fd
|
per_fd
|
||||||
|
|
||||||
|
let close self fd : unit =
|
||||||
|
let@ _sp = Trace_.with_span ~__FILE__ ~__LINE__ "nanoev.close" in
|
||||||
|
let r, w =
|
||||||
|
let@ self = with_lock_ self in
|
||||||
|
match Hashtbl.find self.fds fd with
|
||||||
|
| per_fd ->
|
||||||
|
Hashtbl.remove self.fds fd;
|
||||||
|
self.sub_up_to_date <- false;
|
||||||
|
if Atomic.get self.in_select then wakeup_from_outside self;
|
||||||
|
per_fd.r, per_fd.w
|
||||||
|
| exception Not_found ->
|
||||||
|
invalid_arg "File descriptor is not known to Nanoev"
|
||||||
|
in
|
||||||
|
|
||||||
|
(* call callbacks outside of the lock *)
|
||||||
|
perform_cbs_closed ~closed:true r;
|
||||||
|
perform_cbs_closed ~closed:true w;
|
||||||
|
()
|
||||||
|
|
||||||
let on_readable self fd x y f : unit =
|
let on_readable self fd x y f : unit =
|
||||||
|
let@ _sp = Trace_.with_span ~__FILE__ ~__LINE__ "nanoev.on-readable" in
|
||||||
let@ self = with_lock_ self in
|
let@ self = with_lock_ self in
|
||||||
let per_fd = get_fd_ self fd in
|
let per_fd = get_fd_ self fd in
|
||||||
per_fd.r <- Sub (x, y, f, per_fd.r);
|
per_fd.r <- Sub (x, y, f, per_fd.r);
|
||||||
|
|
@ -101,6 +137,7 @@ let on_readable self fd x y f : unit =
|
||||||
if Atomic.get self.in_select then wakeup_from_outside self
|
if Atomic.get self.in_select then wakeup_from_outside self
|
||||||
|
|
||||||
let on_writable self fd x y f : unit =
|
let on_writable self fd x y f : unit =
|
||||||
|
let@ _sp = Trace_.with_span ~__FILE__ ~__LINE__ "nanoev.on-writable" in
|
||||||
let@ self = with_lock_ self in
|
let@ self = with_lock_ self in
|
||||||
let per_fd = get_fd_ self fd in
|
let per_fd = get_fd_ self fd in
|
||||||
per_fd.w <- Sub (x, y, f, per_fd.w);
|
per_fd.w <- Sub (x, y, f, per_fd.w);
|
||||||
|
|
@ -108,6 +145,7 @@ let on_writable self fd x y f : unit =
|
||||||
if Atomic.get self.in_select then wakeup_from_outside self
|
if Atomic.get self.in_select then wakeup_from_outside self
|
||||||
|
|
||||||
let run_after_s self time x y f : unit =
|
let run_after_s self time x y f : unit =
|
||||||
|
let@ _sp = Trace_.with_span ~__FILE__ ~__LINE__ "nanoev.run-after-s" in
|
||||||
let@ self = with_lock_ self in
|
let@ self = with_lock_ self in
|
||||||
let deadline = now_ () +. time in
|
let deadline = now_ () +. time in
|
||||||
Heap.insert self.timer (Timer { deadline; x; y; f });
|
Heap.insert self.timer (Timer { deadline; x; y; f });
|
||||||
|
|
@ -115,6 +153,7 @@ let run_after_s self time x y f : unit =
|
||||||
|
|
||||||
let recompute_if_needed (self : st) =
|
let recompute_if_needed (self : st) =
|
||||||
if not self.sub_up_to_date then (
|
if not self.sub_up_to_date then (
|
||||||
|
let@ _sp = Trace_.with_span ~__FILE__ ~__LINE__ "recompute-if-needed" in
|
||||||
self.sub_up_to_date <- true;
|
self.sub_up_to_date <- true;
|
||||||
self.sub_r <- [];
|
self.sub_r <- [];
|
||||||
self.sub_w <- [];
|
self.sub_w <- [];
|
||||||
|
|
@ -132,13 +171,8 @@ let next_deadline_ (self : st) : float option =
|
||||||
| exception Heap.Empty -> None
|
| exception Heap.Empty -> None
|
||||||
| Timer t -> Some t.deadline
|
| Timer t -> Some t.deadline
|
||||||
|
|
||||||
let rec perform_cbs = function
|
|
||||||
| Nil -> ()
|
|
||||||
| Sub (x, y, f, tail) ->
|
|
||||||
f x y;
|
|
||||||
perform_cbs tail
|
|
||||||
|
|
||||||
let step (self : st) : unit =
|
let step (self : st) : unit =
|
||||||
|
let@ _sp = Trace_.with_span ~__FILE__ ~__LINE__ "nanoev.unix.step" in
|
||||||
(* gather the subscriptions and timeout *)
|
(* gather the subscriptions and timeout *)
|
||||||
let timeout, sub_r, sub_w =
|
let timeout, sub_r, sub_w =
|
||||||
let@ self = with_lock_ self in
|
let@ self = with_lock_ self in
|
||||||
|
|
@ -154,6 +188,14 @@ let step (self : st) : unit =
|
||||||
(* enter [select] *)
|
(* enter [select] *)
|
||||||
Atomic.set self.in_select true;
|
Atomic.set self.in_select true;
|
||||||
let r_reads, r_writes, _ =
|
let r_reads, r_writes, _ =
|
||||||
|
let@ _sp =
|
||||||
|
Trace_.with_span ~__FILE__ ~__LINE__ "select" ~data:(fun () ->
|
||||||
|
[
|
||||||
|
"timeout", `Float timeout;
|
||||||
|
"reads", `Int (List.length sub_r);
|
||||||
|
"writes", `Int (List.length sub_w);
|
||||||
|
])
|
||||||
|
in
|
||||||
Unix.select (self.wakeup_rd :: sub_r) sub_w [] timeout
|
Unix.select (self.wakeup_rd :: sub_r) sub_w [] timeout
|
||||||
in
|
in
|
||||||
Atomic.set self.in_select false;
|
Atomic.set self.in_select false;
|
||||||
|
|
@ -176,8 +218,10 @@ let step (self : st) : unit =
|
||||||
|
|
||||||
List.iter
|
List.iter
|
||||||
(fun fd ->
|
(fun fd ->
|
||||||
|
if fd != self.wakeup_rd then (
|
||||||
let per_fd = Hashtbl.find self.fds fd in
|
let per_fd = Hashtbl.find self.fds fd in
|
||||||
ready_r := per_fd :: !ready_r)
|
ready_r := per_fd :: !ready_r
|
||||||
|
))
|
||||||
r_reads;
|
r_reads;
|
||||||
List.iter
|
List.iter
|
||||||
(fun fd ->
|
(fun fd ->
|
||||||
|
|
@ -188,19 +232,27 @@ let step (self : st) : unit =
|
||||||
(* call callbacks *)
|
(* call callbacks *)
|
||||||
List.iter
|
List.iter
|
||||||
(fun fd ->
|
(fun fd ->
|
||||||
perform_cbs fd.r;
|
perform_cbs ~closed:false fd.r;
|
||||||
fd.r <- Nil)
|
fd.r <- Nil)
|
||||||
!ready_r;
|
!ready_r;
|
||||||
List.iter
|
List.iter
|
||||||
(fun fd ->
|
(fun fd ->
|
||||||
perform_cbs fd.w;
|
perform_cbs ~closed:false fd.w;
|
||||||
fd.w <- Nil)
|
fd.w <- Nil)
|
||||||
!ready_w;
|
!ready_w;
|
||||||
|
|
||||||
()
|
()
|
||||||
|
|
||||||
let ops : st Nanoev.Impl.ops =
|
let ops : st Nanoev.Impl.ops =
|
||||||
{ step; on_readable; on_writable; run_after_s; wakeup_from_outside; clear }
|
{
|
||||||
|
step;
|
||||||
|
close;
|
||||||
|
on_readable;
|
||||||
|
on_writable;
|
||||||
|
run_after_s;
|
||||||
|
wakeup_from_outside;
|
||||||
|
clear;
|
||||||
|
}
|
||||||
|
|
||||||
include Nanoev
|
include Nanoev
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
(tests
|
(tests
|
||||||
(names t1)
|
(names t1)
|
||||||
(libraries nanoev nanoev.unix threads))
|
(libraries nanoev nanoev.unix threads))
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,11 @@ let () =
|
||||||
let ev = E.create () in
|
let ev = E.create () in
|
||||||
ignore (Thread.create loop ev : Thread.t);
|
ignore (Thread.create loop ev : Thread.t);
|
||||||
let rd, wr = mkpipe () in
|
let rd, wr = mkpipe () in
|
||||||
E.on_readable ev rd () () (fun () () -> print_endline "can read");
|
E.on_readable ev rd () () (fun ~closed () () ->
|
||||||
|
if closed then
|
||||||
|
print_endline "closed!"
|
||||||
|
else
|
||||||
|
print_endline "can read");
|
||||||
Thread.delay 0.05;
|
Thread.delay 0.05;
|
||||||
print_endline "writing";
|
print_endline "writing";
|
||||||
ignore
|
ignore
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue