Skip to content

Commit

Permalink
picoev, x.vweb: small fixes and backport changes from vweb (#20584)
Browse files Browse the repository at this point in the history
  • Loading branch information
Casper64 committed Jan 23, 2024
1 parent 2874e7c commit d88ca11
Show file tree
Hide file tree
Showing 18 changed files with 320 additions and 130 deletions.
4 changes: 2 additions & 2 deletions cmd/tools/vtest-self.v
Expand Up @@ -264,8 +264,8 @@ const skip_on_ubuntu_musl = [
'vlib/net/smtp/smtp_test.v',
'vlib/v/tests/websocket_logger_interface_should_compile_test.v',
'vlib/v/tests/fn_literal_type_test.v',
'vlib/vweb/x/tests/vweb_test.v',
'vlib/vweb/x/tests/vweb_app_test.v',
'vlib/x/vweb/tests/vweb_test.v',
'vlib/x/vweb/tests/vweb_app_test.v',
]
const skip_on_linux = [
'do_not_remove',
Expand Down
2 changes: 1 addition & 1 deletion examples/pico/pico.v
Expand Up @@ -50,6 +50,6 @@ fn callback(data voidptr, req picohttpparser.Request, mut res picohttpparser.Res

fn main() {
println('Starting webserver on http://localhost:${port}/ ...')
mut server := picoev.new(port: port, cb: callback)
mut server := picoev.new(port: port, cb: callback)!
server.serve()
}
2 changes: 1 addition & 1 deletion examples/pico/raw_callback.v
Expand Up @@ -12,7 +12,7 @@ fn main() {
mut pico := picoev.new(
port: port
raw_cb: handle_conn
)
)!
pico.serve()
}

Expand Down
8 changes: 4 additions & 4 deletions vlib/picoev/picoev.v
Expand Up @@ -44,8 +44,8 @@ pub:
max_headers int = 100
max_read int = 4096
max_write int = 8192
family net.AddrFamily = .ip
host string = 'localhost'
family net.AddrFamily = .ip6
host string
}

@[heap]
Expand Down Expand Up @@ -302,8 +302,8 @@ fn default_err_cb(data voidptr, req picohttpparser.Request, mut res picohttppars
}

// new creates a `Picoev` struct and initializes the main loop
pub fn new(config Config) &Picoev {
listen_fd := listen(config)
pub fn new(config Config) !&Picoev {
listen_fd := listen(config)!

mut pv := &Picoev{
num_loops: 1
Expand Down
29 changes: 16 additions & 13 deletions vlib/picoev/socket_util.c.v
Expand Up @@ -99,7 +99,7 @@ fn fatal_socket_error(fd int) bool {
}

// listen creates a listening tcp socket and returns its file descriptor
fn listen(config Config) int {
fn listen(config Config) !int {
// not using the `net` modules sockets, because not all socket options are defined
fd := C.socket(config.family, net.SocketType.tcp, 0)
assert fd != -1
Expand All @@ -110,16 +110,23 @@ fn listen(config Config) int {

// Setting flags for socket
flag := 1
assert C.setsockopt(fd, C.SOL_SOCKET, C.SO_REUSEADDR, &flag, sizeof(int)) == 0
flag_zero := 0
net.socket_error(C.setsockopt(fd, C.SOL_SOCKET, C.SO_REUSEADDR, &flag, sizeof(int)))!

if config.family == .ip6 {
// set socket to dualstack so connections to both ipv4 and ipv6 addresses
// can be accepted
net.socket_error(C.setsockopt(fd, C.IPPROTO_IPV6, C.IPV6_V6ONLY, &flag_zero, sizeof(int)))!
}

$if linux {
// epoll socket options
assert C.setsockopt(fd, C.SOL_SOCKET, C.SO_REUSEPORT, &flag, sizeof(int)) == 0
assert C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_QUICKACK, &flag, sizeof(int)) == 0
assert C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_DEFER_ACCEPT, &config.timeout_secs,
sizeof(int)) == 0
net.socket_error(C.setsockopt(fd, C.SOL_SOCKET, C.SO_REUSEPORT, &flag, sizeof(int)))!
net.socket_error(C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_QUICKACK, &flag, sizeof(int)))!
net.socket_error(C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_DEFER_ACCEPT, &config.timeout_secs,
sizeof(int)))!
queue_len := max_queue
assert C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_FASTOPEN, &queue_len, sizeof(int)) == 0
net.socket_error(C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_FASTOPEN, &queue_len, sizeof(int)))!
}

// addr settings
Expand All @@ -128,12 +135,8 @@ fn listen(config Config) int {
addr := addrs[0]
alen := addr.len()

net.socket_error_message(C.bind(fd, voidptr(&addr), alen), 'binding to ${saddr} failed') or {
panic(err)
}
net.socket_error_message(C.listen(fd, C.SOMAXCONN), 'listening on ${saddr} with maximum backlog pending queue of ${C.SOMAXCONN}, failed') or {
panic(err)
}
net.socket_error_message(C.bind(fd, voidptr(&addr), alen), 'binding to ${saddr} failed')!
net.socket_error_message(C.listen(fd, C.SOMAXCONN), 'listening on ${saddr} with maximum backlog pending queue of ${C.SOMAXCONN}, failed')!

setup_sock(fd) or {
config.err_cb(config.user_data, picohttpparser.Request{}, mut &picohttpparser.Response{},
Expand Down
15 changes: 6 additions & 9 deletions vlib/x/vweb/context.v
Expand Up @@ -20,6 +20,7 @@ pub enum RedirectType {

// The Context struct represents the Context which holds the HTTP request and response.
// It has fields for the query, form, files and methods for handling the request and response
@[heap]
pub struct Context {
mut:
// vweb wil try to infer the content type base on file extension,
Expand Down Expand Up @@ -120,29 +121,28 @@ pub fn (mut ctx Context) send_response_to_client(mimetype string, response strin
return Result{}
}

// Response HTTP_OK with s as payload with content-type `text/html`
// Response with payload and content-type `text/html`
pub fn (mut ctx Context) html(s string) Result {
return ctx.send_response_to_client('text/html', s)
}

// Response HTTP_OK with s as payload with content-type `text/plain`
// Response with `s` as payload and content-type `text/plain`
pub fn (mut ctx Context) text(s string) Result {
return ctx.send_response_to_client('text/plain', s)
}

// Response HTTP_OK with j as payload with content-type `application/json`
// Response with json_s as payload and content-type `application/json`
pub fn (mut ctx Context) json[T](j T) Result {
json_s := json.encode(j)
return ctx.send_response_to_client('application/json', json_s)
}

// Response HTTP_OK with a pretty-printed JSON result
// Response with a pretty-printed JSON result
pub fn (mut ctx Context) json_pretty[T](j T) Result {
json_s := json.encode_pretty(j)
return ctx.send_response_to_client('application/json', json_s)
}

// TODO - test + turn read_file into streaming
// Response HTTP_OK with file as payload
pub fn (mut ctx Context) file(file_path string) Result {
if !os.exists(file_path) {
Expand Down Expand Up @@ -187,10 +187,7 @@ fn (mut ctx Context) send_file(content_type string, file_path string) Result {
}
file.close()

// optimization: use max_read on purpose instead of max_write to take into account
// the HTTP header size and the fact that it's not likely that the socket/OS
// is able to write 8KB at once under load.
if file_size < max_read || ctx.takeover {
if ctx.takeover {
// it's a small file so we can send the response directly
data := os.read_file(file_path) or {
eprintln('[vweb] error while trying to read file: ${err.msg()}')
Expand Down
9 changes: 5 additions & 4 deletions vlib/x/vweb/controller.v
Expand Up @@ -2,7 +2,7 @@ module vweb

import net.urllib

type ControllerHandler = fn (ctx Context, mut url urllib.URL, host string) Context
type ControllerHandler = fn (ctx &Context, mut url urllib.URL, host string) &Context

pub struct ControllerPath {
pub:
Expand Down Expand Up @@ -35,7 +35,7 @@ pub fn controller[A, X](path string, mut global_app A) !&ControllerPath {
// no need to type `ControllerHandler` as generic since it's not needed for closures
return &ControllerPath{
path: path
handler: fn [mut global_app, path, routes, controllers_sorted] [A, X](ctx Context, mut url urllib.URL, host string) Context {
handler: fn [mut global_app, path, routes, controllers_sorted] [A, X](ctx &Context, mut url urllib.URL, host string) &Context {
// transform the url
url.path = url.path.all_after_first(path)

Expand All @@ -53,7 +53,8 @@ pub fn controller[A, X](path string, mut global_app A) !&ControllerPath {
user_context.Context = ctx

handle_route[A, X](mut global_app, mut user_context, url, host, &routes)
return user_context.Context
// we need to explicitly tell the V compiler to return a reference
return &user_context.Context
}
}
}
Expand Down Expand Up @@ -95,7 +96,7 @@ fn check_duplicate_routes_in_controllers[T](global_app &T, routes map[string]Rou
return controllers_sorted
}

fn handle_controllers[X](controllers []&ControllerPath, ctx Context, mut url urllib.URL, host string) ?Context {
fn handle_controllers[X](controllers []&ControllerPath, ctx &Context, mut url urllib.URL, host string) ?&Context {
for controller in controllers {
// skip controller if the hosts don't match
if controller.host != '' && host != controller.host {
Expand Down
11 changes: 8 additions & 3 deletions vlib/x/vweb/csrf/csrf_test.v
Expand Up @@ -187,6 +187,12 @@ pub struct Context {

pub struct App {
vweb.Middleware[Context]
mut:
started chan bool
}

pub fn (mut app App) before_accept_loop() {
app.started <- true
}

fn (app &App) index(mut ctx Context) vweb.Result {
Expand Down Expand Up @@ -234,10 +240,9 @@ fn test_run_app_in_background() {
mut app := &App{}
app.route_use('/auth', csrf.middleware[Context](csrf_config))

spawn vweb.run_at[App, Context](mut app, port: sport, family: .ip)
spawn exit_after_timeout(mut app, exit_after_time)

time.sleep(500 * time.millisecond)
spawn vweb.run_at[App, Context](mut app, port: sport, family: .ip)
_ := <-app.started
}

fn test_token_input() {
Expand Down
8 changes: 8 additions & 0 deletions vlib/x/vweb/sendfile_linux.c.v
@@ -0,0 +1,8 @@
module vweb

fn C.sendfile(out_fd int, in_fd int, offset voidptr, count int) int

fn sendfile(out_fd int, in_fd int, nr_bytes int) int {
// always pass nil as offset, so the file offset will be used and updated.
return C.sendfile(out_fd, in_fd, 0, nr_bytes)
}
17 changes: 12 additions & 5 deletions vlib/x/vweb/sse/sse_test.v
Expand Up @@ -12,7 +12,14 @@ pub struct Context {
vweb.Context
}

pub struct App {}
pub struct App {
mut:
started chan bool
}

pub fn (mut app App) before_accept_loop() {
app.started <- true
}

fn (app &App) sse(mut ctx Context) vweb.Result {
ctx.takeover_conn()
Expand All @@ -32,15 +39,15 @@ fn handle_sse_conn(mut ctx Context) {

fn testsuite_begin() {
mut app := &App{}

spawn vweb.run_at[App, Context](mut app, port: port, family: .ip)
// app startup time
time.sleep(time.second * 2)
spawn fn () {
time.sleep(exit_after)
assert true == false, 'timeout reached!'
exit(1)
}()

spawn vweb.run_at[App, Context](mut app, port: port, family: .ip)
// app startup time
_ := <-app.started
}

fn test_sse() ! {
Expand Down
37 changes: 19 additions & 18 deletions vlib/x/vweb/tests/controller_test.v
Expand Up @@ -15,6 +15,12 @@ pub struct Context {

pub struct App {
vweb.Controller
mut:
started chan bool
}

pub fn (mut app App) before_accept_loop() {
app.started <- true
}

pub fn (app &App) index(mut ctx Context) vweb.Result {
Expand Down Expand Up @@ -49,24 +55,19 @@ pub fn (app &SubController) index(mut ctx Context) vweb.Result {
fn testsuite_begin() {
os.chdir(os.dir(@FILE))!

spawn fn () ! {
mut sub := &SubController{}
mut other := &Other{}
other.register_controller[SubController, Context]('/sub', mut sub)!
mut hidden := &HiddenByOther{}

mut app := &App{}
app.register_controller[Other, Context]('/other', mut other)!
// controllers should be sorted, so this controller should be accessible
// even though it is declared last
app.register_controller[HiddenByOther, Context]('/other/hide', mut hidden)!

vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip) or {
panic('could not start vweb app')
}
}()
// app startup time
time.sleep(time.second * 10)
mut sub := &SubController{}
mut other := &Other{}
other.register_controller[SubController, Context]('/sub', mut sub)!
mut hidden := &HiddenByOther{}

mut app := &App{}
app.register_controller[Other, Context]('/other', mut other)!
// controllers should be sorted, so this controller should be accessible
// even though it is declared last
app.register_controller[HiddenByOther, Context]('/other/hide', mut hidden)!

spawn vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip)
_ := <-app.started

spawn fn () {
time.sleep(exit_after)
Expand Down
24 changes: 13 additions & 11 deletions vlib/x/vweb/tests/large_payload_test.v
Expand Up @@ -13,7 +13,14 @@ const exit_after = time.second * 10

const tmp_file = os.join_path(os.vtmp_dir(), 'vweb_large_payload.txt')

pub struct App {}
pub struct App {
mut:
started chan bool
}

pub fn (mut app App) before_accept_loop() {
app.started <- true
}

pub fn (mut app App) index(mut ctx Context) vweb.Result {
return ctx.text('Hello V!')
Expand All @@ -33,21 +40,16 @@ pub struct Context {
}

fn testsuite_begin() {
spawn fn () {
mut app := &App{}
vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip) or {
panic('could not start vweb app')
}
}()

// app startup time
time.sleep(time.millisecond * 500)

spawn fn () {
time.sleep(exit_after)
assert true == false, 'timeout reached!'
exit(1)
}()

mut app := &App{}
spawn vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip)
// app startup time
_ := <-app.started
}

fn test_large_request_body() {
Expand Down

0 comments on commit d88ca11

Please sign in to comment.