mirror of
https://github.com/vlang/v.git
synced 2025-08-04 02:07:28 -04:00
picoev, x.vweb: small fixes and backport changes from vweb (#20584)
This commit is contained in:
parent
2874e7cac0
commit
d88ca11a4c
@ -264,8 +264,8 @@ const skip_on_ubuntu_musl = [
|
|||||||
'vlib/net/smtp/smtp_test.v',
|
'vlib/net/smtp/smtp_test.v',
|
||||||
'vlib/v/tests/websocket_logger_interface_should_compile_test.v',
|
'vlib/v/tests/websocket_logger_interface_should_compile_test.v',
|
||||||
'vlib/v/tests/fn_literal_type_test.v',
|
'vlib/v/tests/fn_literal_type_test.v',
|
||||||
'vlib/vweb/x/tests/vweb_test.v',
|
'vlib/x/vweb/tests/vweb_test.v',
|
||||||
'vlib/vweb/x/tests/vweb_app_test.v',
|
'vlib/x/vweb/tests/vweb_app_test.v',
|
||||||
]
|
]
|
||||||
const skip_on_linux = [
|
const skip_on_linux = [
|
||||||
'do_not_remove',
|
'do_not_remove',
|
||||||
|
@ -50,6 +50,6 @@ fn callback(data voidptr, req picohttpparser.Request, mut res picohttpparser.Res
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println('Starting webserver on http://localhost:${port}/ ...')
|
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()
|
server.serve()
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ fn main() {
|
|||||||
mut pico := picoev.new(
|
mut pico := picoev.new(
|
||||||
port: port
|
port: port
|
||||||
raw_cb: handle_conn
|
raw_cb: handle_conn
|
||||||
)
|
)!
|
||||||
pico.serve()
|
pico.serve()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,8 +44,8 @@ pub:
|
|||||||
max_headers int = 100
|
max_headers int = 100
|
||||||
max_read int = 4096
|
max_read int = 4096
|
||||||
max_write int = 8192
|
max_write int = 8192
|
||||||
family net.AddrFamily = .ip
|
family net.AddrFamily = .ip6
|
||||||
host string = 'localhost'
|
host string
|
||||||
}
|
}
|
||||||
|
|
||||||
@[heap]
|
@[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
|
// new creates a `Picoev` struct and initializes the main loop
|
||||||
pub fn new(config Config) &Picoev {
|
pub fn new(config Config) !&Picoev {
|
||||||
listen_fd := listen(config)
|
listen_fd := listen(config)!
|
||||||
|
|
||||||
mut pv := &Picoev{
|
mut pv := &Picoev{
|
||||||
num_loops: 1
|
num_loops: 1
|
||||||
|
@ -99,7 +99,7 @@ fn fatal_socket_error(fd int) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// listen creates a listening tcp socket and returns its file descriptor
|
// 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
|
// not using the `net` modules sockets, because not all socket options are defined
|
||||||
fd := C.socket(config.family, net.SocketType.tcp, 0)
|
fd := C.socket(config.family, net.SocketType.tcp, 0)
|
||||||
assert fd != -1
|
assert fd != -1
|
||||||
@ -110,16 +110,23 @@ fn listen(config Config) int {
|
|||||||
|
|
||||||
// Setting flags for socket
|
// Setting flags for socket
|
||||||
flag := 1
|
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 {
|
$if linux {
|
||||||
// epoll socket options
|
// epoll socket options
|
||||||
assert C.setsockopt(fd, C.SOL_SOCKET, C.SO_REUSEPORT, &flag, sizeof(int)) == 0
|
net.socket_error(C.setsockopt(fd, C.SOL_SOCKET, C.SO_REUSEPORT, &flag, sizeof(int)))!
|
||||||
assert C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_QUICKACK, &flag, sizeof(int)) == 0
|
net.socket_error(C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_QUICKACK, &flag, sizeof(int)))!
|
||||||
assert C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_DEFER_ACCEPT, &config.timeout_secs,
|
net.socket_error(C.setsockopt(fd, C.IPPROTO_TCP, C.TCP_DEFER_ACCEPT, &config.timeout_secs,
|
||||||
sizeof(int)) == 0
|
sizeof(int)))!
|
||||||
queue_len := max_queue
|
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
|
// addr settings
|
||||||
@ -128,12 +135,8 @@ fn listen(config Config) int {
|
|||||||
addr := addrs[0]
|
addr := addrs[0]
|
||||||
alen := addr.len()
|
alen := addr.len()
|
||||||
|
|
||||||
net.socket_error_message(C.bind(fd, voidptr(&addr), alen), 'binding to ${saddr} failed') or {
|
net.socket_error_message(C.bind(fd, voidptr(&addr), alen), 'binding to ${saddr} failed')!
|
||||||
panic(err)
|
net.socket_error_message(C.listen(fd, C.SOMAXCONN), 'listening on ${saddr} with maximum backlog pending queue of ${C.SOMAXCONN}, failed')!
|
||||||
}
|
|
||||||
net.socket_error_message(C.listen(fd, C.SOMAXCONN), 'listening on ${saddr} with maximum backlog pending queue of ${C.SOMAXCONN}, failed') or {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_sock(fd) or {
|
setup_sock(fd) or {
|
||||||
config.err_cb(config.user_data, picohttpparser.Request{}, mut &picohttpparser.Response{},
|
config.err_cb(config.user_data, picohttpparser.Request{}, mut &picohttpparser.Response{},
|
||||||
|
@ -20,6 +20,7 @@ pub enum RedirectType {
|
|||||||
|
|
||||||
// The Context struct represents the Context which holds the HTTP request and response.
|
// 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
|
// It has fields for the query, form, files and methods for handling the request and response
|
||||||
|
@[heap]
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
mut:
|
mut:
|
||||||
// vweb wil try to infer the content type base on file extension,
|
// 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{}
|
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 {
|
pub fn (mut ctx Context) html(s string) Result {
|
||||||
return ctx.send_response_to_client('text/html', s)
|
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 {
|
pub fn (mut ctx Context) text(s string) Result {
|
||||||
return ctx.send_response_to_client('text/plain', s)
|
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 {
|
pub fn (mut ctx Context) json[T](j T) Result {
|
||||||
json_s := json.encode(j)
|
json_s := json.encode(j)
|
||||||
return ctx.send_response_to_client('application/json', json_s)
|
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 {
|
pub fn (mut ctx Context) json_pretty[T](j T) Result {
|
||||||
json_s := json.encode_pretty(j)
|
json_s := json.encode_pretty(j)
|
||||||
return ctx.send_response_to_client('application/json', json_s)
|
return ctx.send_response_to_client('application/json', json_s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO - test + turn read_file into streaming
|
|
||||||
// Response HTTP_OK with file as payload
|
// Response HTTP_OK with file as payload
|
||||||
pub fn (mut ctx Context) file(file_path string) Result {
|
pub fn (mut ctx Context) file(file_path string) Result {
|
||||||
if !os.exists(file_path) {
|
if !os.exists(file_path) {
|
||||||
@ -187,10 +187,7 @@ fn (mut ctx Context) send_file(content_type string, file_path string) Result {
|
|||||||
}
|
}
|
||||||
file.close()
|
file.close()
|
||||||
|
|
||||||
// optimization: use max_read on purpose instead of max_write to take into account
|
if ctx.takeover {
|
||||||
// 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 {
|
|
||||||
// it's a small file so we can send the response directly
|
// it's a small file so we can send the response directly
|
||||||
data := os.read_file(file_path) or {
|
data := os.read_file(file_path) or {
|
||||||
eprintln('[vweb] error while trying to read file: ${err.msg()}')
|
eprintln('[vweb] error while trying to read file: ${err.msg()}')
|
||||||
|
@ -2,7 +2,7 @@ module vweb
|
|||||||
|
|
||||||
import net.urllib
|
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 struct ControllerPath {
|
||||||
pub:
|
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
|
// no need to type `ControllerHandler` as generic since it's not needed for closures
|
||||||
return &ControllerPath{
|
return &ControllerPath{
|
||||||
path: path
|
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
|
// transform the url
|
||||||
url.path = url.path.all_after_first(path)
|
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
|
user_context.Context = ctx
|
||||||
|
|
||||||
handle_route[A, X](mut global_app, mut user_context, url, host, &routes)
|
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
|
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 {
|
for controller in controllers {
|
||||||
// skip controller if the hosts don't match
|
// skip controller if the hosts don't match
|
||||||
if controller.host != '' && host != controller.host {
|
if controller.host != '' && host != controller.host {
|
||||||
|
@ -187,6 +187,12 @@ pub struct Context {
|
|||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
vweb.Middleware[Context]
|
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 {
|
fn (app &App) index(mut ctx Context) vweb.Result {
|
||||||
@ -234,10 +240,9 @@ fn test_run_app_in_background() {
|
|||||||
mut app := &App{}
|
mut app := &App{}
|
||||||
app.route_use('/auth', csrf.middleware[Context](csrf_config))
|
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)
|
spawn exit_after_timeout(mut app, exit_after_time)
|
||||||
|
spawn vweb.run_at[App, Context](mut app, port: sport, family: .ip)
|
||||||
time.sleep(500 * time.millisecond)
|
_ := <-app.started
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_token_input() {
|
fn test_token_input() {
|
||||||
|
8
vlib/x/vweb/sendfile_linux.c.v
Normal file
8
vlib/x/vweb/sendfile_linux.c.v
Normal file
@ -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)
|
||||||
|
}
|
@ -12,7 +12,14 @@ pub struct Context {
|
|||||||
vweb.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 {
|
fn (app &App) sse(mut ctx Context) vweb.Result {
|
||||||
ctx.takeover_conn()
|
ctx.takeover_conn()
|
||||||
@ -32,15 +39,15 @@ fn handle_sse_conn(mut ctx Context) {
|
|||||||
|
|
||||||
fn testsuite_begin() {
|
fn testsuite_begin() {
|
||||||
mut app := &App{}
|
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 () {
|
spawn fn () {
|
||||||
time.sleep(exit_after)
|
time.sleep(exit_after)
|
||||||
assert true == false, 'timeout reached!'
|
assert true == false, 'timeout reached!'
|
||||||
exit(1)
|
exit(1)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
spawn vweb.run_at[App, Context](mut app, port: port, family: .ip)
|
||||||
|
// app startup time
|
||||||
|
_ := <-app.started
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_sse() ! {
|
fn test_sse() ! {
|
||||||
|
@ -15,6 +15,12 @@ pub struct Context {
|
|||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
vweb.Controller
|
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 {
|
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() {
|
fn testsuite_begin() {
|
||||||
os.chdir(os.dir(@FILE))!
|
os.chdir(os.dir(@FILE))!
|
||||||
|
|
||||||
spawn fn () ! {
|
mut sub := &SubController{}
|
||||||
mut sub := &SubController{}
|
mut other := &Other{}
|
||||||
mut other := &Other{}
|
other.register_controller[SubController, Context]('/sub', mut sub)!
|
||||||
other.register_controller[SubController, Context]('/sub', mut sub)!
|
mut hidden := &HiddenByOther{}
|
||||||
mut hidden := &HiddenByOther{}
|
|
||||||
|
|
||||||
mut app := &App{}
|
mut app := &App{}
|
||||||
app.register_controller[Other, Context]('/other', mut other)!
|
app.register_controller[Other, Context]('/other', mut other)!
|
||||||
// controllers should be sorted, so this controller should be accessible
|
// controllers should be sorted, so this controller should be accessible
|
||||||
// even though it is declared last
|
// even though it is declared last
|
||||||
app.register_controller[HiddenByOther, Context]('/other/hide', mut hidden)!
|
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 {
|
spawn vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip)
|
||||||
panic('could not start vweb app')
|
_ := <-app.started
|
||||||
}
|
|
||||||
}()
|
|
||||||
// app startup time
|
|
||||||
time.sleep(time.second * 10)
|
|
||||||
|
|
||||||
spawn fn () {
|
spawn fn () {
|
||||||
time.sleep(exit_after)
|
time.sleep(exit_after)
|
||||||
|
@ -13,7 +13,14 @@ const exit_after = time.second * 10
|
|||||||
|
|
||||||
const tmp_file = os.join_path(os.vtmp_dir(), 'vweb_large_payload.txt')
|
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 {
|
pub fn (mut app App) index(mut ctx Context) vweb.Result {
|
||||||
return ctx.text('Hello V!')
|
return ctx.text('Hello V!')
|
||||||
@ -33,21 +40,16 @@ pub struct Context {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn testsuite_begin() {
|
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 () {
|
spawn fn () {
|
||||||
time.sleep(exit_after)
|
time.sleep(exit_after)
|
||||||
assert true == false, 'timeout reached!'
|
assert true == false, 'timeout reached!'
|
||||||
exit(1)
|
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() {
|
fn test_large_request_body() {
|
||||||
|
@ -18,6 +18,12 @@ pub mut:
|
|||||||
@[heap]
|
@[heap]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
vweb.Middleware[Context]
|
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 {
|
pub fn (app &App) index(mut ctx Context) vweb.Result {
|
||||||
@ -66,28 +72,24 @@ fn after_middleware(mut ctx Context) bool {
|
|||||||
fn testsuite_begin() {
|
fn testsuite_begin() {
|
||||||
os.chdir(os.dir(@FILE))!
|
os.chdir(os.dir(@FILE))!
|
||||||
|
|
||||||
spawn fn () {
|
mut app := &App{}
|
||||||
mut app := &App{}
|
// even though `route_use` is called first, global middleware is still executed first
|
||||||
// even though `route_use` is called first, global middleware is still executed first
|
app.Middleware.route_use('/unreachable', handler: middleware_unreachable)
|
||||||
app.Middleware.route_use('/unreachable', handler: middleware_unreachable)
|
|
||||||
|
|
||||||
// global middleware
|
// global middleware
|
||||||
app.Middleware.use(handler: middleware_handler)
|
app.Middleware.use(handler: middleware_handler)
|
||||||
app.Middleware.use(handler: app.app_middleware)
|
app.Middleware.use(handler: app.app_middleware)
|
||||||
|
|
||||||
// should match only one slash
|
// should match only one slash
|
||||||
app.Middleware.route_use('/bar/:foo', handler: middleware_handler)
|
app.Middleware.route_use('/bar/:foo', handler: middleware_handler)
|
||||||
// should match multiple slashes
|
// should match multiple slashes
|
||||||
app.Middleware.route_use('/nested/:path...', handler: middleware_handler)
|
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 {
|
spawn vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip)
|
||||||
panic('could not start vweb app')
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
// app startup time
|
// app startup time
|
||||||
time.sleep(time.second * 2)
|
_ := <-app.started
|
||||||
|
|
||||||
spawn fn () {
|
spawn fn () {
|
||||||
time.sleep(exit_after)
|
time.sleep(exit_after)
|
||||||
|
@ -11,6 +11,12 @@ const exit_after = time.second * 10
|
|||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
vweb.StaticHandler
|
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 {
|
pub fn (mut app App) index(mut ctx Context) vweb.Result {
|
||||||
@ -28,16 +34,13 @@ pub struct Context {
|
|||||||
|
|
||||||
fn testsuite_begin() {
|
fn testsuite_begin() {
|
||||||
os.chdir(os.dir(@FILE))!
|
os.chdir(os.dir(@FILE))!
|
||||||
spawn run_app_test()
|
|
||||||
|
|
||||||
// app startup time
|
|
||||||
time.sleep(time.second * 2)
|
|
||||||
|
|
||||||
spawn fn () {
|
spawn fn () {
|
||||||
time.sleep(exit_after)
|
time.sleep(exit_after)
|
||||||
assert true == false, 'timeout reached!'
|
assert true == false, 'timeout reached!'
|
||||||
exit(1)
|
exit(1)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
run_app_test()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn 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) }
|
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 {
|
spawn vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2, family: .ip)
|
||||||
panic('could not start vweb app')
|
// app startup time
|
||||||
}
|
_ := <-app.started
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_static_root() {
|
fn test_static_root() {
|
||||||
|
@ -12,7 +12,12 @@ pub mut:
|
|||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub mut:
|
pub mut:
|
||||||
db sqlite.DB
|
db sqlite.DB
|
||||||
|
started chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (mut app App) before_accept_loop() {
|
||||||
|
app.started <- true
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Article {
|
struct Article {
|
||||||
@ -27,7 +32,9 @@ fn test_a_vweb_application_compiles() {
|
|||||||
exit(0)
|
exit(0)
|
||||||
}()
|
}()
|
||||||
mut app := &App{}
|
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() {
|
pub fn (mut ctx Context) before_request() {
|
||||||
|
@ -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')
|
||||||
|
}
|
@ -339,3 +339,16 @@ ${config.content}'
|
|||||||
}
|
}
|
||||||
return read.bytestr()
|
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'
|
||||||
|
}
|
||||||
|
@ -187,16 +187,18 @@ fn generate_routes[A, X](app &A) !map[string]Route {
|
|||||||
return error('error parsing method attributes: ${err}')
|
return error('error parsing method attributes: ${err}')
|
||||||
}
|
}
|
||||||
|
|
||||||
routes[method.name] = Route{
|
mut route := Route{
|
||||||
methods: http_methods
|
methods: http_methods
|
||||||
path: route_path
|
path: route_path
|
||||||
host: host
|
host: host
|
||||||
}
|
}
|
||||||
|
|
||||||
$if A is MiddlewareApp {
|
$if A is MiddlewareApp {
|
||||||
routes[method.name].middlewares = app.Middleware.get_handlers_for_route[X](route_path)
|
route.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.after_middlewares = app.Middleware.get_handlers_for_route_after[X](route_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
routes[method.name] = route
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return routes
|
return routes
|
||||||
@ -272,6 +274,11 @@ pub fn (mut params RequestParams) request_done(fd int) {
|
|||||||
params.idx[fd] = 0
|
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`
|
// 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) }
|
// Example: vweb.run_at(new_app(), vweb.RunParams{ host: 'localhost' port: 8099 family: .ip }) or { panic(err) }
|
||||||
@[direct_array_access; manualfree]
|
@[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)!
|
controllers_sorted := check_duplicate_routes_in_controllers[A](global_app, routes)!
|
||||||
|
|
||||||
if params.show_startup_message {
|
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()
|
flush_stdout()
|
||||||
|
|
||||||
@ -312,7 +320,11 @@ pub fn run_at[A, X](mut global_app A, params RunParams) ! {
|
|||||||
timeout_secs: params.timeout_in_seconds
|
timeout_secs: params.timeout_in_seconds
|
||||||
family: params.family
|
family: params.family
|
||||||
host: params.host
|
host: params.host
|
||||||
)
|
)!
|
||||||
|
|
||||||
|
$if A is BeforeAcceptApp {
|
||||||
|
global_app.before_accept_loop()
|
||||||
|
}
|
||||||
|
|
||||||
// Forever accept every connection that comes
|
// Forever accept every connection that comes
|
||||||
pico.serve()
|
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) {
|
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)
|
mut bytes_to_write := int(params.file_responses[fd].total - params.file_responses[fd].pos)
|
||||||
|
|
||||||
if bytes_to_write > vweb.max_write {
|
$if linux {
|
||||||
bytes_to_write = vweb.max_write
|
bytes_written := sendfile(fd, params.file_responses[fd].file.fd, bytes_to_write)
|
||||||
}
|
params.file_responses[fd].pos += bytes_written
|
||||||
data := unsafe { malloc(bytes_to_write) }
|
} $else {
|
||||||
defer {
|
if bytes_to_write > vweb.max_write {
|
||||||
unsafe { free(data) }
|
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 {
|
if params.file_responses[fd].pos == params.file_responses[fd].total {
|
||||||
// file is done writing
|
// file is done writing
|
||||||
params.file_responses[fd].done()
|
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) }
|
mut global_app := unsafe { &A(params.global_app) }
|
||||||
|
|
||||||
// TODO: change this variable to include the total wait time over each network cycle
|
// 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)
|
host, _ := urllib.split_host_port(host_with_port)
|
||||||
|
|
||||||
// Create Context with request data
|
// Create Context with request data
|
||||||
mut ctx := Context{
|
mut ctx := &Context{
|
||||||
req: req
|
req: req
|
||||||
page_gen_start: page_gen_start
|
page_gen_start: page_gen_start
|
||||||
conn: conn
|
conn: conn
|
||||||
@ -681,8 +699,8 @@ fn handle_request[A, X](mut conn net.TcpConn, req http.Request, params &RequestP
|
|||||||
user_context.Context = ctx
|
user_context.Context = ctx
|
||||||
|
|
||||||
handle_route[A, X](mut global_app, mut user_context, url, host, params.routes)
|
handle_route[A, X](mut global_app, mut user_context, url, host, params.routes)
|
||||||
|
// we need to explicitly tell the V compiler to return a reference
|
||||||
return user_context.Context
|
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) {
|
fn handle_route[A, X](mut app A, mut user_context X, url urllib.URL, host string, routes &map[string]Route) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user