diff --git a/cmd/tools/vtest-self.v b/cmd/tools/vtest-self.v index 13f440a9bc..893e7444f7 100644 --- a/cmd/tools/vtest-self.v +++ b/cmd/tools/vtest-self.v @@ -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', diff --git a/examples/pico/pico.v b/examples/pico/pico.v index ca4b76f109..2dc64cb360 100644 --- a/examples/pico/pico.v +++ b/examples/pico/pico.v @@ -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() } diff --git a/examples/pico/raw_callback.v b/examples/pico/raw_callback.v index 181da7d0aa..4eb62ceb03 100644 --- a/examples/pico/raw_callback.v +++ b/examples/pico/raw_callback.v @@ -12,7 +12,7 @@ fn main() { mut pico := picoev.new( port: port raw_cb: handle_conn - ) + )! pico.serve() } diff --git a/vlib/picoev/picoev.v b/vlib/picoev/picoev.v index 5a8024eb24..df328a4569 100644 --- a/vlib/picoev/picoev.v +++ b/vlib/picoev/picoev.v @@ -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] @@ -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 diff --git a/vlib/picoev/socket_util.c.v b/vlib/picoev/socket_util.c.v index f90946b17c..7589da488e 100644 --- a/vlib/picoev/socket_util.c.v +++ b/vlib/picoev/socket_util.c.v @@ -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 @@ -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 @@ -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{}, diff --git a/vlib/x/vweb/context.v b/vlib/x/vweb/context.v index f4c44f20cb..a8b6909621 100644 --- a/vlib/x/vweb/context.v +++ b/vlib/x/vweb/context.v @@ -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, @@ -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) { @@ -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()}') diff --git a/vlib/x/vweb/controller.v b/vlib/x/vweb/controller.v index 0141f077f5..7e066a7467 100644 --- a/vlib/x/vweb/controller.v +++ b/vlib/x/vweb/controller.v @@ -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: @@ -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) @@ -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 } } } @@ -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 { diff --git a/vlib/x/vweb/csrf/csrf_test.v b/vlib/x/vweb/csrf/csrf_test.v index b5ec1ec7f6..fb23dbe440 100644 --- a/vlib/x/vweb/csrf/csrf_test.v +++ b/vlib/x/vweb/csrf/csrf_test.v @@ -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 { @@ -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() { diff --git a/vlib/x/vweb/sendfile_linux.c.v b/vlib/x/vweb/sendfile_linux.c.v new file mode 100644 index 0000000000..22508bead0 --- /dev/null +++ b/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) +} diff --git a/vlib/x/vweb/sse/sse_test.v b/vlib/x/vweb/sse/sse_test.v index 6e14af6b58..cc56d0dbf4 100644 --- a/vlib/x/vweb/sse/sse_test.v +++ b/vlib/x/vweb/sse/sse_test.v @@ -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() @@ -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() ! { diff --git a/vlib/x/vweb/tests/controller_test.v b/vlib/x/vweb/tests/controller_test.v index cbb19bbfc9..a0736ff336 100644 --- a/vlib/x/vweb/tests/controller_test.v +++ b/vlib/x/vweb/tests/controller_test.v @@ -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 { @@ -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 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)! + 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) + spawn vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip) + _ := <-app.started spawn fn () { time.sleep(exit_after) diff --git a/vlib/x/vweb/tests/large_payload_test.v b/vlib/x/vweb/tests/large_payload_test.v index e95cfefaad..8989b3cada 100644 --- a/vlib/x/vweb/tests/large_payload_test.v +++ b/vlib/x/vweb/tests/large_payload_test.v @@ -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!') @@ -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() { diff --git a/vlib/x/vweb/tests/middleware_test.v b/vlib/x/vweb/tests/middleware_test.v index c1f954af5e..6fc97e398f 100644 --- a/vlib/x/vweb/tests/middleware_test.v +++ b/vlib/x/vweb/tests/middleware_test.v @@ -18,6 +18,12 @@ pub mut: @[heap] pub struct App { vweb.Middleware[Context] +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 { @@ -66,28 +72,24 @@ fn after_middleware(mut ctx Context) bool { fn testsuite_begin() { os.chdir(os.dir(@FILE))! - spawn fn () { - mut app := &App{} - // even though `route_use` is called first, global middleware is still executed first - app.Middleware.route_use('/unreachable', handler: middleware_unreachable) + mut app := &App{} + // even though `route_use` is called first, global middleware is still executed first + app.Middleware.route_use('/unreachable', handler: middleware_unreachable) - // global middleware - app.Middleware.use(handler: middleware_handler) - app.Middleware.use(handler: app.app_middleware) + // global middleware + app.Middleware.use(handler: middleware_handler) + app.Middleware.use(handler: app.app_middleware) - // should match only one slash - app.Middleware.route_use('/bar/:foo', handler: middleware_handler) - // should match multiple slashes - app.Middleware.route_use('/nested/:path...', handler: middleware_handler) + // should match only one slash + app.Middleware.route_use('/bar/:foo', handler: middleware_handler) + // should match multiple slashes + app.Middleware.route_use('/nested/:path...', handler: middleware_handler) - app.Middleware.route_use('/after', handler: after_middleware, after: true) + app.Middleware.route_use('/after', handler: after_middleware, after: true) - vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip) or { - panic('could not start vweb app') - } - }() + spawn vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip) // app startup time - time.sleep(time.second * 2) + _ := <-app.started spawn fn () { time.sleep(exit_after) diff --git a/vlib/x/vweb/tests/static_handler_test.v b/vlib/x/vweb/tests/static_handler_test.v index 8ddc952601..31ae20ba53 100644 --- a/vlib/x/vweb/tests/static_handler_test.v +++ b/vlib/x/vweb/tests/static_handler_test.v @@ -11,6 +11,12 @@ const exit_after = time.second * 10 pub struct App { vweb.StaticHandler +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 { @@ -28,16 +34,13 @@ pub struct Context { fn testsuite_begin() { os.chdir(os.dir(@FILE))! - spawn run_app_test() - - // app startup time - time.sleep(time.second * 2) - spawn fn () { time.sleep(exit_after) assert true == false, 'timeout reached!' exit(1) }() + + run_app_test() } fn run_app_test() { @@ -72,9 +75,9 @@ fn run_app_test() { app.mount_static_folder_at('testdata', '/static') or { panic(err) } - vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip) or { - panic('could not start vweb app') - } + spawn vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip) + // app startup time + _ := <-app.started } fn test_static_root() { diff --git a/vlib/x/vweb/tests/vweb_app_test.v b/vlib/x/vweb/tests/vweb_app_test.v index b63e78162a..660d4a5c12 100644 --- a/vlib/x/vweb/tests/vweb_app_test.v +++ b/vlib/x/vweb/tests/vweb_app_test.v @@ -12,7 +12,12 @@ pub mut: pub struct App { pub mut: - db sqlite.DB + db sqlite.DB + started chan bool +} + +pub fn (mut app App) before_accept_loop() { + app.started <- true } struct Article { @@ -27,7 +32,9 @@ fn test_a_vweb_application_compiles() { exit(0) }() mut app := &App{} - vweb.run_at[App, Context](mut app, port: port, family: .ip, timeout_in_seconds: 10)! + spawn vweb.run_at[App, Context](mut app, port: port, family: .ip, timeout_in_seconds: 2) + // app startup time + _ := <-app.started } pub fn (mut ctx Context) before_request() { diff --git a/vlib/x/vweb/tests/vweb_should_listen_on_both_ipv4_and_ipv6_by_default_test.v b/vlib/x/vweb/tests/vweb_should_listen_on_both_ipv4_and_ipv6_by_default_test.v new file mode 100644 index 0000000000..876cb4ebba --- /dev/null +++ b/vlib/x/vweb/tests/vweb_should_listen_on_both_ipv4_and_ipv6_by_default_test.v @@ -0,0 +1,123 @@ +import os +import log +import time +import x.vweb +import net.http + +const vexe = os.getenv('VEXE') +const vroot = os.dir(vexe) +const port = 28872 +const welcome_text = 'Welcome to our simple vweb server' + +// Use a known good http client like `curl` (if it exists): +const curl_executable = os.find_abs_path_of_executable('curl') or { '' } +const curl_ok = curl_supports_ipv6() + +fn curl_supports_ipv6() bool { + if curl_executable == '' { + return false + } + curl_res := os.execute('${curl_executable} --version') + if curl_res.exit_code != 0 { + return false + } + if !curl_res.output.match_glob('curl*Features: * IPv6 *') { + return false + } + return true +} + +fn testsuite_begin() { + log.set_level(.debug) + log.debug(@FN) + os.chdir(vroot) or {} + if curl_ok { + log.info('working curl_executable found at: ${curl_executable}') + } else { + log.warn('no working working curl_executable found') + } + start_services() +} + +fn testsuite_end() { + log.debug(@FN) +} + +// + +fn ensure_curl_works(tname string) ? { + if !curl_ok { + log.warn('skipping test ${tname}, since it needs a working curl') + return none + } +} + +fn test_curl_connecting_through_ipv4_works() { + ensure_curl_works(@FN) or { return } + res := os.execute('${curl_executable} --connect-timeout 0.5 --silent http://127.0.0.1:${port}/') + assert res.exit_code == 0, res.output + assert res.output == welcome_text + log.info('> ${@FN}') +} + +fn test_net_http_connecting_through_ipv4_works() { + res := http.get('http://127.0.0.1:${port}/')! + assert res.status_code == 200, res.str() + assert res.status_msg == 'OK', res.str() + assert res.body == welcome_text, res.str() + log.info('> ${@FN}') +} + +fn test_curl_connecting_through_ipv6_works() { + ensure_curl_works(@FN) or { return } + res := os.execute('${curl_executable} --silent --connect-timeout 0.5 http://[::1]:${port}/') + assert res.exit_code == 0, res.output + assert res.output == welcome_text + log.info('> ${@FN}') +} + +fn test_net_http_connecting_through_ipv6_works() { + $if windows { + log.warn('skipping test ${@FN} on windows for now') + return + } + res := http.get('http://[::1]:${port}/')! + assert res.status_code == 200, res.str() + assert res.status_msg == 'OK', res.str() + assert res.body == welcome_text, res.str() + log.info('> ${@FN}') +} + +// + +pub struct Context { + vweb.Context +} + +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(welcome_text) +} + +fn start_services() { + log.debug('starting watchdog thread to ensure the test will always exit one way or another...') + spawn fn (timeout_in_ms int) { + time.sleep(timeout_in_ms * time.millisecond) + log.error('Timeout of ${timeout_in_ms} ms reached, for webserver: pid: ${os.getpid()}. Exiting ...') + exit(1) + }(10_000) + + log.debug('starting webserver...') + mut app := &App{} + spawn vweb.run[App, Context](mut app, port) + _ := <-app.started + log.debug('webserver started') +} diff --git a/vlib/x/vweb/tests/vweb_test.v b/vlib/x/vweb/tests/vweb_test.v index eaac77c8c4..fec4190222 100644 --- a/vlib/x/vweb/tests/vweb_test.v +++ b/vlib/x/vweb/tests/vweb_test.v @@ -339,3 +339,16 @@ ${config.content}' } return read.bytestr() } + +// for issue 20476 +// phenomenon: parsing url error when querypath is `//` +fn test_empty_querypath() { + mut x := http.get('http://${localserver}') or { panic(err) } + assert x.body == 'Welcome to VWeb' + x = http.get('http://${localserver}/') or { panic(err) } + assert x.body == 'Welcome to VWeb' + x = http.get('http://${localserver}//') or { panic(err) } + assert x.body == 'Welcome to VWeb' + x = http.get('http://${localserver}///') or { panic(err) } + assert x.body == 'Welcome to VWeb' +} diff --git a/vlib/x/vweb/vweb.v b/vlib/x/vweb/vweb.v index fb72693151..0324cbcb36 100644 --- a/vlib/x/vweb/vweb.v +++ b/vlib/x/vweb/vweb.v @@ -187,16 +187,18 @@ fn generate_routes[A, X](app &A) !map[string]Route { return error('error parsing method attributes: ${err}') } - routes[method.name] = Route{ + mut route := Route{ methods: http_methods path: route_path host: host } $if A is MiddlewareApp { - routes[method.name].middlewares = app.Middleware.get_handlers_for_route[X](route_path) - routes[method.name].after_middlewares = app.Middleware.get_handlers_for_route_after[X](route_path) + route.middlewares = app.Middleware.get_handlers_for_route[X](route_path) + route.after_middlewares = app.Middleware.get_handlers_for_route_after[X](route_path) } + + routes[method.name] = route } } return routes @@ -272,6 +274,11 @@ pub fn (mut params RequestParams) request_done(fd int) { params.idx[fd] = 0 } +interface BeforeAcceptApp { +mut: + before_accept_loop() +} + // run_at - start a new VWeb server, listening only on a specific address `host`, at the specified `port` // Example: vweb.run_at(new_app(), vweb.RunParams{ host: 'localhost' port: 8099 family: .ip }) or { panic(err) } @[direct_array_access; manualfree] @@ -284,7 +291,8 @@ pub fn run_at[A, X](mut global_app A, params RunParams) ! { controllers_sorted := check_duplicate_routes_in_controllers[A](global_app, routes)! if params.show_startup_message { - println('[Vweb] Running app on http://localhost:${params.port}/') + host := if params.host == '' { 'localhost' } else { params.host } + println('[Vweb] Running app on http://${host}:${params.port}/') } flush_stdout() @@ -312,7 +320,11 @@ pub fn run_at[A, X](mut global_app A, params RunParams) ! { timeout_secs: params.timeout_in_seconds family: params.family host: params.host - ) + )! + + $if A is BeforeAcceptApp { + global_app.before_accept_loop() + } // Forever accept every connection that comes pico.serve() @@ -371,32 +383,38 @@ fn handle_timeout(mut pv picoev.Picoev, mut params RequestParams, fd int) { fn handle_write_file(mut pv picoev.Picoev, mut params RequestParams, fd int) { mut bytes_to_write := int(params.file_responses[fd].total - params.file_responses[fd].pos) - if bytes_to_write > vweb.max_write { - bytes_to_write = vweb.max_write - } - data := unsafe { malloc(bytes_to_write) } - defer { - unsafe { free(data) } + $if linux { + bytes_written := sendfile(fd, params.file_responses[fd].file.fd, bytes_to_write) + params.file_responses[fd].pos += bytes_written + } $else { + if bytes_to_write > vweb.max_write { + bytes_to_write = vweb.max_write + } + + data := unsafe { malloc(bytes_to_write) } + defer { + unsafe { free(data) } + } + + mut conn := &net.TcpConn{ + sock: net.tcp_socket_from_handle_raw(fd) + handle: fd + is_blocking: false + } + + params.file_responses[fd].file.read_into_ptr(data, bytes_to_write) or { + params.file_responses[fd].done() + pv.close_conn(fd) + return + } + actual_written := send_string_ptr(mut conn, data, bytes_to_write) or { + params.file_responses[fd].done() + pv.close_conn(fd) + return + } + params.file_responses[fd].pos += actual_written } - mut conn := &net.TcpConn{ - sock: net.tcp_socket_from_handle_raw(fd) - handle: fd - is_blocking: false - } - - // TODO: use `sendfile` in linux for optimizations (?) - params.file_responses[fd].file.read_into_ptr(data, bytes_to_write) or { - params.file_responses[fd].done() - pv.close_conn(fd) - return - } - actual_written := send_string_ptr(mut conn, data, bytes_to_write) or { - params.file_responses[fd].done() - pv.close_conn(fd) - return - } - params.file_responses[fd].pos += actual_written if params.file_responses[fd].pos == params.file_responses[fd].total { // file is done writing params.file_responses[fd].done() @@ -622,7 +640,7 @@ fn handle_read[A, X](mut pv picoev.Picoev, mut params RequestParams, fd int) { } } -fn handle_request[A, X](mut conn net.TcpConn, req http.Request, params &RequestParams) ?Context { +fn handle_request[A, X](mut conn net.TcpConn, req http.Request, params &RequestParams) ?&Context { mut global_app := unsafe { &A(params.global_app) } // TODO: change this variable to include the total wait time over each network cycle @@ -654,7 +672,7 @@ fn handle_request[A, X](mut conn net.TcpConn, req http.Request, params &RequestP host, _ := urllib.split_host_port(host_with_port) // Create Context with request data - mut ctx := Context{ + mut ctx := &Context{ req: req page_gen_start: page_gen_start conn: conn @@ -681,8 +699,8 @@ fn handle_request[A, X](mut conn net.TcpConn, req http.Request, params &RequestP user_context.Context = ctx handle_route[A, X](mut global_app, mut user_context, url, host, params.routes) - - return user_context.Context + // we need to explicitly tell the V compiler to return a reference + return &user_context.Context } fn handle_route[A, X](mut app A, mut user_context X, url urllib.URL, host string, routes &map[string]Route) {