zig/std/event/net.zig

376 lines
14 KiB
Zig

const std = @import("../std.zig");
const builtin = @import("builtin");
const testing = std.testing;
const event = std.event;
const mem = std.mem;
const os = std.os;
const posix = os.posix;
const Loop = std.event.Loop;
pub const Server = struct {
handleRequestFn: async<*mem.Allocator> fn (*Server, *const std.net.Address, os.File) void,
loop: *Loop,
sockfd: ?i32,
accept_coro: ?promise,
listen_address: std.net.Address,
waiting_for_emfile_node: PromiseNode,
listen_resume_node: event.Loop.ResumeNode,
const PromiseNode = std.LinkedList(promise).Node;
pub fn init(loop: *Loop) Server {
// TODO can't initialize handler coroutine here because we need well defined copy elision
return Server{
.loop = loop,
.sockfd = null,
.accept_coro = null,
.handleRequestFn = undefined,
.waiting_for_emfile_node = undefined,
.listen_address = undefined,
.listen_resume_node = event.Loop.ResumeNode{
.id = event.Loop.ResumeNode.Id.Basic,
.handle = undefined,
.overlapped = event.Loop.ResumeNode.overlapped_init,
},
};
}
pub fn listen(
self: *Server,
address: *const std.net.Address,
handleRequestFn: async<*mem.Allocator> fn (*Server, *const std.net.Address, os.File) void,
) !void {
self.handleRequestFn = handleRequestFn;
const sockfd = try os.posixSocket(posix.AF_INET, posix.SOCK_STREAM | posix.SOCK_CLOEXEC | posix.SOCK_NONBLOCK, posix.PROTO_tcp);
errdefer os.close(sockfd);
self.sockfd = sockfd;
try os.posixBind(sockfd, &address.os_addr);
try os.posixListen(sockfd, posix.SOMAXCONN);
self.listen_address = std.net.Address.initPosix(try os.posixGetSockName(sockfd));
self.accept_coro = try async<self.loop.allocator> Server.handler(self);
errdefer cancel self.accept_coro.?;
self.listen_resume_node.handle = self.accept_coro.?;
try self.loop.linuxAddFd(sockfd, &self.listen_resume_node, posix.EPOLLIN | posix.EPOLLOUT | posix.EPOLLET);
errdefer self.loop.removeFd(sockfd);
}
/// Stop listening
pub fn close(self: *Server) void {
self.loop.linuxRemoveFd(self.sockfd.?);
os.close(self.sockfd.?);
}
pub fn deinit(self: *Server) void {
if (self.accept_coro) |accept_coro| cancel accept_coro;
if (self.sockfd) |sockfd| os.close(sockfd);
}
pub async fn handler(self: *Server) void {
while (true) {
var accepted_addr: std.net.Address = undefined;
// TODO just inline the following function here and don't expose it as posixAsyncAccept
if (os.posixAsyncAccept(self.sockfd.?, &accepted_addr.os_addr, posix.SOCK_NONBLOCK | posix.SOCK_CLOEXEC)) |accepted_fd| {
if (accepted_fd == -1) {
// would block
suspend; // we will get resumed by epoll_wait in the event loop
continue;
}
var socket = os.File.openHandle(accepted_fd);
_ = async<self.loop.allocator> self.handleRequestFn(self, &accepted_addr, socket) catch |err| switch (err) {
error.OutOfMemory => {
socket.close();
continue;
},
};
} else |err| switch (err) {
error.ProcessFdQuotaExceeded => {
errdefer os.emfile_promise_queue.remove(&self.waiting_for_emfile_node);
suspend {
self.waiting_for_emfile_node = PromiseNode.init(@handle());
os.emfile_promise_queue.append(&self.waiting_for_emfile_node);
}
continue;
},
error.ConnectionAborted => continue,
error.FileDescriptorNotASocket => unreachable,
error.OperationNotSupported => unreachable,
error.SystemFdQuotaExceeded, error.SystemResources, error.ProtocolFailure, error.BlockedByFirewall, error.Unexpected => {
@panic("TODO handle this error");
},
}
}
}
};
pub async fn connectUnixSocket(loop: *Loop, path: []const u8) !i32 {
const sockfd = try os.posixSocket(
posix.AF_UNIX,
posix.SOCK_STREAM | posix.SOCK_CLOEXEC | posix.SOCK_NONBLOCK,
0,
);
errdefer os.close(sockfd);
var sock_addr = posix.sockaddr_un{
.family = posix.AF_UNIX,
.path = undefined,
};
if (path.len > @typeOf(sock_addr.path).len) return error.NameTooLong;
mem.copy(u8, sock_addr.path[0..], path);
const size = @intCast(u32, @sizeOf(posix.sa_family_t) + path.len);
try os.posixConnectAsync(sockfd, &sock_addr, size);
try await try async loop.linuxWaitFd(sockfd, posix.EPOLLIN | posix.EPOLLOUT | posix.EPOLLET);
try os.posixGetSockOptConnectError(sockfd);
return sockfd;
}
pub const ReadError = error{
SystemResources,
Unexpected,
UserResourceLimitReached,
InputOutput,
FileDescriptorNotRegistered, // TODO remove this possibility
OperationCausesCircularLoop, // TODO remove this possibility
FileDescriptorAlreadyPresentInSet, // TODO remove this possibility
FileDescriptorIncompatibleWithEpoll, // TODO remove this possibility
};
/// returns number of bytes read. 0 means EOF.
pub async fn read(loop: *std.event.Loop, fd: os.FileHandle, buffer: []u8) ReadError!usize {
const iov = posix.iovec{
.iov_base = buffer.ptr,
.iov_len = buffer.len,
};
const iovs: *const [1]posix.iovec = &iov;
return await (async readvPosix(loop, fd, iovs, 1) catch unreachable);
}
pub const WriteError = error{};
pub async fn write(loop: *std.event.Loop, fd: os.FileHandle, buffer: []const u8) WriteError!void {
const iov = posix.iovec_const{
.iov_base = buffer.ptr,
.iov_len = buffer.len,
};
const iovs: *const [1]posix.iovec_const = &iov;
return await (async writevPosix(loop, fd, iovs, 1) catch unreachable);
}
pub async fn writevPosix(loop: *Loop, fd: i32, iov: [*]const posix.iovec_const, count: usize) !void {
while (true) {
switch (builtin.os) {
builtin.Os.macosx, builtin.Os.linux => {
const rc = posix.writev(fd, iov, count);
const err = posix.getErrno(rc);
switch (err) {
0 => return,
posix.EINTR => continue,
posix.ESPIPE => unreachable,
posix.EINVAL => unreachable,
posix.EFAULT => unreachable,
posix.EAGAIN => {
try await (async loop.linuxWaitFd(fd, posix.EPOLLET | posix.EPOLLOUT) catch unreachable);
continue;
},
posix.EBADF => unreachable, // always a race condition
posix.EDESTADDRREQ => unreachable, // connect was never called
posix.EDQUOT => unreachable,
posix.EFBIG => unreachable,
posix.EIO => return error.InputOutput,
posix.ENOSPC => unreachable,
posix.EPERM => return error.AccessDenied,
posix.EPIPE => unreachable,
else => return os.unexpectedErrorPosix(err),
}
},
else => @compileError("Unsupported OS"),
}
}
}
/// returns number of bytes read. 0 means EOF.
pub async fn readvPosix(loop: *std.event.Loop, fd: i32, iov: [*]posix.iovec, count: usize) !usize {
while (true) {
switch (builtin.os) {
builtin.Os.linux, builtin.Os.freebsd, builtin.Os.macosx => {
const rc = posix.readv(fd, iov, count);
const err = posix.getErrno(rc);
switch (err) {
0 => return rc,
posix.EINTR => continue,
posix.EINVAL => unreachable,
posix.EFAULT => unreachable,
posix.EAGAIN => {
try await (async loop.linuxWaitFd(fd, posix.EPOLLET | posix.EPOLLIN) catch unreachable);
continue;
},
posix.EBADF => unreachable, // always a race condition
posix.EIO => return error.InputOutput,
posix.EISDIR => unreachable,
posix.ENOBUFS => return error.SystemResources,
posix.ENOMEM => return error.SystemResources,
else => return os.unexpectedErrorPosix(err),
}
},
else => @compileError("Unsupported OS"),
}
}
}
pub async fn writev(loop: *Loop, fd: os.FileHandle, data: []const []const u8) !void {
const iovecs = try loop.allocator.alloc(os.posix.iovec_const, data.len);
defer loop.allocator.free(iovecs);
for (data) |buf, i| {
iovecs[i] = os.posix.iovec_const{
.iov_base = buf.ptr,
.iov_len = buf.len,
};
}
return await (async writevPosix(loop, fd, iovecs.ptr, data.len) catch unreachable);
}
pub async fn readv(loop: *Loop, fd: os.FileHandle, data: []const []u8) !usize {
const iovecs = try loop.allocator.alloc(os.posix.iovec, data.len);
defer loop.allocator.free(iovecs);
for (data) |buf, i| {
iovecs[i] = os.posix.iovec{
.iov_base = buf.ptr,
.iov_len = buf.len,
};
}
return await (async readvPosix(loop, fd, iovecs.ptr, data.len) catch unreachable);
}
pub async fn connect(loop: *Loop, _address: *const std.net.Address) !os.File {
var address = _address.*; // TODO https://github.com/ziglang/zig/issues/1592
const sockfd = try os.posixSocket(posix.AF_INET, posix.SOCK_STREAM | posix.SOCK_CLOEXEC | posix.SOCK_NONBLOCK, posix.PROTO_tcp);
errdefer os.close(sockfd);
try os.posixConnectAsync(sockfd, &address.os_addr, @sizeOf(posix.sockaddr_in));
try await try async loop.linuxWaitFd(sockfd, posix.EPOLLIN | posix.EPOLLOUT | posix.EPOLLET);
try os.posixGetSockOptConnectError(sockfd);
return os.File.openHandle(sockfd);
}
test "listen on a port, send bytes, receive bytes" {
// https://github.com/ziglang/zig/issues/1908
if (builtin.single_threaded) return error.SkipZigTest;
if (builtin.os != builtin.Os.linux) {
// TODO build abstractions for other operating systems
return error.SkipZigTest;
}
const MyServer = struct {
tcp_server: Server,
const Self = @This();
async<*mem.Allocator> fn handler(tcp_server: *Server, _addr: *const std.net.Address, _socket: os.File) void {
const self = @fieldParentPtr(Self, "tcp_server", tcp_server);
var socket = _socket; // TODO https://github.com/ziglang/zig/issues/1592
defer socket.close();
// TODO guarantee elision of this allocation
const next_handler = async errorableHandler(self, _addr, socket) catch unreachable;
(await next_handler) catch |err| {
std.debug.panic("unable to handle connection: {}\n", err);
};
suspend {
cancel @handle();
}
}
async fn errorableHandler(self: *Self, _addr: *const std.net.Address, _socket: os.File) !void {
const addr = _addr.*; // TODO https://github.com/ziglang/zig/issues/1592
var socket = _socket; // TODO https://github.com/ziglang/zig/issues/1592
const stream = &socket.outStream().stream;
try stream.print("hello from server\n");
}
};
const ip4addr = std.net.parseIp4("127.0.0.1") catch unreachable;
const addr = std.net.Address.initIp4(ip4addr, 0);
var loop: Loop = undefined;
try loop.initSingleThreaded(std.debug.global_allocator);
var server = MyServer{ .tcp_server = Server.init(&loop) };
defer server.tcp_server.deinit();
try server.tcp_server.listen(&addr, MyServer.handler);
const p = try async<std.debug.global_allocator> doAsyncTest(&loop, &server.tcp_server.listen_address, &server.tcp_server);
defer cancel p;
loop.run();
}
async fn doAsyncTest(loop: *Loop, address: *const std.net.Address, server: *Server) void {
errdefer @panic("test failure");
var socket_file = try await try async connect(loop, address);
defer socket_file.close();
var buf: [512]u8 = undefined;
const amt_read = try socket_file.read(buf[0..]);
const msg = buf[0..amt_read];
testing.expect(mem.eql(u8, msg, "hello from server\n"));
server.close();
}
pub const OutStream = struct {
fd: os.FileHandle,
stream: Stream,
loop: *Loop,
pub const Error = WriteError;
pub const Stream = event.io.OutStream(Error);
pub fn init(loop: *Loop, fd: os.FileHandle) OutStream {
return OutStream{
.fd = fd,
.loop = loop,
.stream = Stream{ .writeFn = writeFn },
};
}
async<*mem.Allocator> fn writeFn(out_stream: *Stream, bytes: []const u8) Error!void {
const self = @fieldParentPtr(OutStream, "stream", out_stream);
return await (async write(self.loop, self.fd, bytes) catch unreachable);
}
};
pub const InStream = struct {
fd: os.FileHandle,
stream: Stream,
loop: *Loop,
pub const Error = ReadError;
pub const Stream = event.io.InStream(Error);
pub fn init(loop: *Loop, fd: os.FileHandle) InStream {
return InStream{
.fd = fd,
.loop = loop,
.stream = Stream{ .readFn = readFn },
};
}
async<*mem.Allocator> fn readFn(in_stream: *Stream, bytes: []u8) Error!usize {
const self = @fieldParentPtr(InStream, "stream", in_stream);
return await (async read(self.loop, self.fd, bytes) catch unreachable);
}
};