mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 15:13:28 +00:00
async: use std http client with loop
This commit is contained in:
@@ -3,9 +3,13 @@
|
|||||||
//! Connections are opened in a thread-safe manner, but individual Requests are not.
|
//! Connections are opened in a thread-safe manner, but individual Requests are not.
|
||||||
//!
|
//!
|
||||||
//! TLS support may be disabled via `std.options.http_disable_tls`.
|
//! TLS support may be disabled via `std.options.http_disable_tls`.
|
||||||
|
//!
|
||||||
|
//! This file is a copy of the original std.http.Client with little changes to
|
||||||
|
//! handle non-blocking I/O with the jsruntime.Loop.
|
||||||
|
|
||||||
const std = @import("../std.zig");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
const Stream = @import("stream.zig").Stream;
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const http = std.http;
|
const http = std.http;
|
||||||
const mem = std.mem;
|
const mem = std.mem;
|
||||||
@@ -16,7 +20,10 @@ const assert = std.debug.assert;
|
|||||||
const use_vectors = builtin.zig_backend != .stage2_x86_64;
|
const use_vectors = builtin.zig_backend != .stage2_x86_64;
|
||||||
|
|
||||||
const Client = @This();
|
const Client = @This();
|
||||||
const proto = @import("protocol.zig");
|
const proto = http.protocol;
|
||||||
|
|
||||||
|
const Loop = @import("jsruntime").Loop;
|
||||||
|
const tcp = @import("tcp.zig");
|
||||||
|
|
||||||
pub const disable_tls = std.options.http_disable_tls;
|
pub const disable_tls = std.options.http_disable_tls;
|
||||||
|
|
||||||
@@ -25,6 +32,9 @@ pub const disable_tls = std.options.http_disable_tls;
|
|||||||
/// This allocator must be thread-safe.
|
/// This allocator must be thread-safe.
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
|
|
||||||
|
// std.net.Stream implementation using jsruntime Loop
|
||||||
|
loop: *Loop,
|
||||||
|
|
||||||
ca_bundle: if (disable_tls) void else std.crypto.Certificate.Bundle = if (disable_tls) {} else .{},
|
ca_bundle: if (disable_tls) void else std.crypto.Certificate.Bundle = if (disable_tls) {} else .{},
|
||||||
ca_bundle_mutex: std.Thread.Mutex = .{},
|
ca_bundle_mutex: std.Thread.Mutex = .{},
|
||||||
|
|
||||||
@@ -194,7 +204,7 @@ pub const Connection = struct {
|
|||||||
|
|
||||||
pub const Protocol = enum { plain, tls };
|
pub const Protocol = enum { plain, tls };
|
||||||
|
|
||||||
stream: net.Stream,
|
stream: Stream,
|
||||||
/// undefined unless protocol is tls.
|
/// undefined unless protocol is tls.
|
||||||
tls_client: if (!disable_tls) *std.crypto.tls.Client else void,
|
tls_client: if (!disable_tls) *std.crypto.tls.Client else void,
|
||||||
|
|
||||||
@@ -1210,7 +1220,7 @@ pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connec
|
|||||||
errdefer client.allocator.destroy(conn);
|
errdefer client.allocator.destroy(conn);
|
||||||
conn.* = .{ .data = undefined };
|
conn.* = .{ .data = undefined };
|
||||||
|
|
||||||
const stream = net.tcpConnectToHost(client.allocator, host, port) catch |err| switch (err) {
|
const stream = tcp.tcpConnectToHost(client.allocator, client.loop, host, port) catch |err| switch (err) {
|
||||||
error.ConnectionRefused => return error.ConnectionRefused,
|
error.ConnectionRefused => return error.ConnectionRefused,
|
||||||
error.NetworkUnreachable => return error.NetworkUnreachable,
|
error.NetworkUnreachable => return error.NetworkUnreachable,
|
||||||
error.ConnectionTimedOut => return error.ConnectionTimedOut,
|
error.ConnectionTimedOut => return error.ConnectionTimedOut,
|
||||||
@@ -1250,43 +1260,6 @@ pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connec
|
|||||||
return &conn.data;
|
return &conn.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const ConnectUnixError = Allocator.Error || std.os.SocketError || error{ NameTooLong, Unsupported } || std.os.ConnectError;
|
|
||||||
|
|
||||||
/// Connect to `path` as a unix domain socket. This will reuse a connection if one is already open.
|
|
||||||
///
|
|
||||||
/// This function is threadsafe.
|
|
||||||
pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connection {
|
|
||||||
if (!net.has_unix_sockets) return error.Unsupported;
|
|
||||||
|
|
||||||
if (client.connection_pool.findConnection(.{
|
|
||||||
.host = path,
|
|
||||||
.port = 0,
|
|
||||||
.protocol = .plain,
|
|
||||||
})) |node|
|
|
||||||
return node;
|
|
||||||
|
|
||||||
const conn = try client.allocator.create(ConnectionPool.Node);
|
|
||||||
errdefer client.allocator.destroy(conn);
|
|
||||||
conn.* = .{ .data = undefined };
|
|
||||||
|
|
||||||
const stream = try std.net.connectUnixSocket(path);
|
|
||||||
errdefer stream.close();
|
|
||||||
|
|
||||||
conn.data = .{
|
|
||||||
.stream = stream,
|
|
||||||
.tls_client = undefined,
|
|
||||||
.protocol = .plain,
|
|
||||||
|
|
||||||
.host = try client.allocator.dupe(u8, path),
|
|
||||||
.port = 0,
|
|
||||||
};
|
|
||||||
errdefer client.allocator.free(conn.data.host);
|
|
||||||
|
|
||||||
client.connection_pool.addUsed(conn);
|
|
||||||
|
|
||||||
return &conn.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Connect to `tunnel_host:tunnel_port` using the specified proxy with HTTP CONNECT. This will reuse a connection if one is already open.
|
/// Connect to `tunnel_host:tunnel_port` using the specified proxy with HTTP CONNECT. This will reuse a connection if one is already open.
|
||||||
///
|
///
|
||||||
/// This function is threadsafe.
|
/// This function is threadsafe.
|
||||||
|
|||||||
168
src/async/stream.zig
Normal file
168
src/async/stream.zig
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const os = std.os;
|
||||||
|
const io = std.io;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
const Loop = @import("jsruntime").Loop;
|
||||||
|
|
||||||
|
const WriteCmd = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
stream: Stream,
|
||||||
|
done: bool = false,
|
||||||
|
res: usize = undefined,
|
||||||
|
err: ?anyerror = null,
|
||||||
|
|
||||||
|
fn run(self: *Self, buffer: []const u8) void {
|
||||||
|
self.stream.loop.send(*Self, self, callback, self.stream.handle, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn callback(self: *Self, err: ?anyerror, res: usize) void {
|
||||||
|
self.res = res;
|
||||||
|
self.err = err;
|
||||||
|
self.done = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait(self: *Self) !usize {
|
||||||
|
while (!self.done) try self.stream.loop.tick();
|
||||||
|
if (self.err) |err| return err;
|
||||||
|
return self.res;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReadCmd = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
stream: Stream,
|
||||||
|
done: bool = false,
|
||||||
|
res: usize = undefined,
|
||||||
|
err: ?anyerror = null,
|
||||||
|
|
||||||
|
fn run(self: *Self, buffer: []u8) void {
|
||||||
|
self.stream.loop.receive(*Self, self, callback, self.stream.handle, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn callback(self: *Self, _: []const u8, err: ?anyerror, res: usize) void {
|
||||||
|
self.res = res;
|
||||||
|
self.err = err;
|
||||||
|
self.done = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait(self: *Self) !usize {
|
||||||
|
while (!self.done) try self.stream.loop.tick();
|
||||||
|
if (self.err) |err| return err;
|
||||||
|
return self.res;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Stream = struct {
|
||||||
|
loop: *Loop,
|
||||||
|
|
||||||
|
handle: std.os.socket_t,
|
||||||
|
|
||||||
|
pub fn close(self: Stream) void {
|
||||||
|
os.closeSocket(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const ReadError = os.ReadError;
|
||||||
|
pub const WriteError = os.WriteError;
|
||||||
|
|
||||||
|
pub const Reader = io.Reader(Stream, ReadError, read);
|
||||||
|
pub const Writer = io.Writer(Stream, WriteError, write);
|
||||||
|
|
||||||
|
pub fn reader(self: Stream) Reader {
|
||||||
|
return .{ .context = self };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writer(self: Stream) Writer {
|
||||||
|
return .{ .context = self };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(self: Stream, buffer: []u8) ReadError!usize {
|
||||||
|
var cmd = ReadCmd{ .stream = self };
|
||||||
|
cmd.run(buffer);
|
||||||
|
return cmd.wait() catch |err| switch (err) {
|
||||||
|
else => return error.Unexpected,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readv(s: Stream, iovecs: []const os.iovec) ReadError!usize {
|
||||||
|
return os.readv(s.handle, iovecs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of bytes read. If the number read is smaller than
|
||||||
|
/// `buffer.len`, it means the stream reached the end. Reaching the end of
|
||||||
|
/// a stream is not an error condition.
|
||||||
|
pub fn readAll(s: Stream, buffer: []u8) ReadError!usize {
|
||||||
|
return readAtLeast(s, buffer, buffer.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of bytes read, calling the underlying read function
|
||||||
|
/// the minimal number of times until the buffer has at least `len` bytes
|
||||||
|
/// filled. If the number read is less than `len` it means the stream
|
||||||
|
/// reached the end. Reaching the end of the stream is not an error
|
||||||
|
/// condition.
|
||||||
|
pub fn readAtLeast(s: Stream, buffer: []u8, len: usize) ReadError!usize {
|
||||||
|
assert(len <= buffer.len);
|
||||||
|
var index: usize = 0;
|
||||||
|
while (index < len) {
|
||||||
|
const amt = try s.read(buffer[index..]);
|
||||||
|
if (amt == 0) break;
|
||||||
|
index += amt;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TODO in evented I/O mode, this implementation incorrectly uses the event loop's
|
||||||
|
/// file system thread instead of non-blocking. It needs to be reworked to properly
|
||||||
|
/// use non-blocking I/O.
|
||||||
|
pub fn write(self: Stream, buffer: []const u8) WriteError!usize {
|
||||||
|
var cmd = WriteCmd{ .stream = self };
|
||||||
|
cmd.run(buffer);
|
||||||
|
|
||||||
|
return cmd.wait() catch |err| switch (err) {
|
||||||
|
error.AccessDenied => error.AccessDenied,
|
||||||
|
error.WouldBlock => error.WouldBlock,
|
||||||
|
error.ConnectionResetByPeer => error.ConnectionResetByPeer,
|
||||||
|
error.MessageTooBig => error.FileTooBig,
|
||||||
|
error.BrokenPipe => error.BrokenPipe,
|
||||||
|
else => return error.Unexpected,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writeAll(self: Stream, bytes: []const u8) WriteError!void {
|
||||||
|
var index: usize = 0;
|
||||||
|
while (index < bytes.len) {
|
||||||
|
index += try self.write(bytes[index..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See https://github.com/ziglang/zig/issues/7699
|
||||||
|
/// See equivalent function: `std.fs.File.writev`.
|
||||||
|
pub fn writev(self: Stream, iovecs: []const os.iovec_const) WriteError!usize {
|
||||||
|
if (iovecs.len == 0) return 0;
|
||||||
|
const first_buffer = iovecs[0].iov_base[0..iovecs[0].iov_len];
|
||||||
|
return try self.write(first_buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `iovecs` parameter is mutable because this function needs to mutate the fields in
|
||||||
|
/// order to handle partial writes from the underlying OS layer.
|
||||||
|
/// See https://github.com/ziglang/zig/issues/7699
|
||||||
|
/// See equivalent function: `std.fs.File.writevAll`.
|
||||||
|
pub fn writevAll(self: Stream, iovecs: []os.iovec_const) WriteError!void {
|
||||||
|
if (iovecs.len == 0) return;
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while (true) {
|
||||||
|
var amt = try self.writev(iovecs[i..]);
|
||||||
|
while (amt >= iovecs[i].iov_len) {
|
||||||
|
amt -= iovecs[i].iov_len;
|
||||||
|
i += 1;
|
||||||
|
if (i >= iovecs.len) return;
|
||||||
|
}
|
||||||
|
iovecs[i].iov_base += amt;
|
||||||
|
iovecs[i].iov_len -= amt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
62
src/async/tcp.zig
Normal file
62
src/async/tcp.zig
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const net = std.net;
|
||||||
|
const Stream = @import("stream.zig").Stream;
|
||||||
|
const Loop = @import("jsruntime").Loop;
|
||||||
|
|
||||||
|
const ConnectCmd = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
loop: *Loop,
|
||||||
|
socket: std.os.socket_t,
|
||||||
|
err: ?anyerror = null,
|
||||||
|
done: bool = false,
|
||||||
|
|
||||||
|
fn run(self: *Self, addr: std.net.Address) !void {
|
||||||
|
self.loop.connect(*Self, self, callback, self.socket, addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn callback(self: *Self, _: std.os.socket_t, err: ?anyerror) void {
|
||||||
|
self.err = err;
|
||||||
|
self.done = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait(self: *Self) !void {
|
||||||
|
while (!self.done) try self.loop.tick();
|
||||||
|
if (self.err) |err| return err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn tcpConnectToHost(alloc: std.mem.Allocator, loop: *Loop, name: []const u8, port: u16) !Stream {
|
||||||
|
// TODO async resolve
|
||||||
|
const list = try net.getAddressList(alloc, name, port);
|
||||||
|
defer list.deinit();
|
||||||
|
|
||||||
|
if (list.addrs.len == 0) return error.UnknownHostName;
|
||||||
|
|
||||||
|
for (list.addrs) |addr| {
|
||||||
|
return tcpConnectToAddress(loop, addr) catch |err| switch (err) {
|
||||||
|
error.ConnectionRefused => {
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
else => return err,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return std.os.ConnectError.ConnectionRefused;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tcpConnectToAddress(loop: *Loop, addr: net.Address) !Stream {
|
||||||
|
const sockfd = try loop.open(addr.any.family, std.os.SOCK.STREAM, std.os.IPPROTO.TCP);
|
||||||
|
errdefer std.os.closeSocket(sockfd);
|
||||||
|
|
||||||
|
var cmd = ConnectCmd{
|
||||||
|
.loop = loop,
|
||||||
|
.socket = sockfd,
|
||||||
|
};
|
||||||
|
try cmd.run(addr);
|
||||||
|
try cmd.wait();
|
||||||
|
|
||||||
|
return Stream{
|
||||||
|
.loop = loop,
|
||||||
|
.handle = sockfd,
|
||||||
|
};
|
||||||
|
}
|
||||||
60
src/async/test.zig
Normal file
60
src/async/test.zig
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const http = std.http;
|
||||||
|
const StdClient = @import("Client.zig");
|
||||||
|
// const hasync = @import("http.zig");
|
||||||
|
|
||||||
|
pub const Loop = @import("jsruntime").Loop;
|
||||||
|
|
||||||
|
const url = "https://www.w3.org/";
|
||||||
|
|
||||||
|
test "blocking mode fetch API" {
|
||||||
|
const alloc = std.testing.allocator;
|
||||||
|
|
||||||
|
var loop = try Loop.init(alloc);
|
||||||
|
defer loop.deinit();
|
||||||
|
|
||||||
|
var client: StdClient = .{
|
||||||
|
.allocator = alloc,
|
||||||
|
.loop = &loop,
|
||||||
|
};
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
// force client's CA cert scan from system.
|
||||||
|
try client.ca_bundle.rescan(client.allocator);
|
||||||
|
|
||||||
|
var res = try client.fetch(alloc, .{
|
||||||
|
.location = .{ .uri = try std.Uri.parse(url) },
|
||||||
|
.payload = .none,
|
||||||
|
});
|
||||||
|
defer res.deinit();
|
||||||
|
|
||||||
|
try std.testing.expect(res.status == .ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "blocking mode open/send/wait API" {
|
||||||
|
const alloc = std.testing.allocator;
|
||||||
|
|
||||||
|
var loop = try Loop.init(alloc);
|
||||||
|
defer loop.deinit();
|
||||||
|
|
||||||
|
var client: StdClient = .{
|
||||||
|
.allocator = alloc,
|
||||||
|
.loop = &loop,
|
||||||
|
};
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
// force client's CA cert scan from system.
|
||||||
|
try client.ca_bundle.rescan(client.allocator);
|
||||||
|
|
||||||
|
var headers = try std.http.Headers.initList(alloc, &[_]std.http.Field{});
|
||||||
|
defer headers.deinit();
|
||||||
|
|
||||||
|
var req = try client.open(.GET, try std.Uri.parse(url), headers, .{});
|
||||||
|
defer req.deinit();
|
||||||
|
|
||||||
|
try req.send(.{});
|
||||||
|
try req.finish();
|
||||||
|
try req.wait();
|
||||||
|
|
||||||
|
try std.testing.expect(req.response.status == .ok);
|
||||||
|
}
|
||||||
@@ -94,6 +94,11 @@ pub fn main() !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
const TestAsync = @import("async/test.zig");
|
||||||
|
std.testing.refAllDecls(TestAsync);
|
||||||
|
}
|
||||||
|
|
||||||
test "jsruntime" {
|
test "jsruntime" {
|
||||||
// generate tests
|
// generate tests
|
||||||
try generate.tests();
|
try generate.tests();
|
||||||
|
|||||||
Reference in New Issue
Block a user