tcp: read server preferences from the config

This commit is contained in:
Aarnav Tale
2024-04-20 02:30:52 -04:00
parent b592910dd3
commit 6799cca2a6
5 changed files with 80 additions and 24 deletions

View File

@ -1353,6 +1353,17 @@ keybind: Keybinds = .{},
/// If `true`, the bold text will use the bright color palette.
@"bold-is-bright": bool = false,
/// If set, Ghostty will start a TCP server on the specified socket path. Valid
/// values can include a valid Unix socket path or an IP address and port. For
/// example, `0.0.0.0:9393` will start a TCP server on all interfaces on port
/// 9393. A value such as `/tmp/ghostty.sock` will start a socket on that path.
@"remote-tcp-socket": ?[:0]const u8 = null,
/// The maximum number of connections (default 10) that can be made to the
/// remote TCP server. If this limit is reached, new connections will be
/// rejected with a message and the connection will be closed.
@"remote-max-connections": u8 = 10,
/// This will be used to set the `TERM` environment variable.
/// HACK: We set this with an `xterm` prefix because vim uses that to enable key
/// protocols (specifically this will enable `modifyOtherKeys`), among other

View File

@ -2,7 +2,6 @@
//! responding to TCP requests and dispatching them to the app's Mailbox.
pub const Thread = @import("tcp/Thread.zig");
pub const Server = @import("tcp/Server.zig");
pub const Command = @import("tcp/Command.zig").Command;
test {
@import("std").testing.refAllDecls(@This());

View File

@ -24,7 +24,6 @@ pub const Command = enum {
// Not using .first() because it returns everything if there is no space
// Instead we're doing what's equivalent to popping the first element
const cmdName = iter.next() orelse return error.InvalidInput;
log.debug("got command name={s}", .{cmdName});
// TODO: Handle/support arguments
return std.meta.stringToEnum(Command, cmdName) orelse return error.InvalidCommand;

View File

@ -1,6 +1,6 @@
const std = @import("std");
const xev = @import("xev");
const tcp = @import("../tcp.zig");
const Config = @import("../Config.zig").Config;
const reject_client = @import("./handlers/reject.zig").reject_client;
const read_client = @import("./handlers/reader.zig").read_client;
@ -11,9 +11,6 @@ const SocketPool = std.heap.MemoryPool(xev.TCP);
const BufferPool = std.heap.MemoryPool([1024]u8);
const log = std.log.scoped(.tcp_thread);
/// Maximum connections we allow at once
const MAX_CLIENTS = 2;
/// Acceptor polling rate in milliseconds
const ACCEPTOR_RATE = 1;
@ -32,9 +29,6 @@ sock_pool: SocketPool,
/// Memory pool that stores buffers for reading and writing
buf_pool: BufferPool,
/// Stop flag
stop: bool,
/// Event loop
loop: xev.Loop,
@ -45,32 +39,75 @@ socket: xev.TCP,
acceptor: xev.Timer,
/// Array of client sockets
clients: [MAX_CLIENTS]*xev.TCP,
/// TODO: Merge with `sock_pool`? or make a hashmap
clients: SocketPool,
/// Number of clients connected
clients_count: usize,
/// Initializes the server with the given allocator and address
pub fn init(alloc: Allocator, addr: std.net.Address) !Server {
const socket = try xev.TCP.init(addr);
try socket.bind(addr);
/// Address to bind to
addr: std.net.Address,
/// Maximum clients allowed
max_clients: u8,
/// Initializes the server with the given allocator and address
pub fn init(
alloc: Allocator,
addr: std.net.Address,
max_clients: u8,
) !Server {
return Server{
.comp_pool = CompletionPool.init(alloc),
.sock_pool = SocketPool.init(alloc),
.buf_pool = BufferPool.init(alloc),
.stop = false,
.loop = try xev.Loop.init(.{}),
.socket = socket,
.socket = try xev.TCP.init(addr),
.acceptor = try xev.Timer.init(),
.clients = undefined,
.clients_count = 0,
.alloc = alloc,
.max_clients = max_clients,
.addr = addr,
};
}
const BindError = error{
NoAddress,
InvalidAddress,
};
/// Tries to generate a valid address to bind to
/// TODO: Maybe unix sockets should start with unix://
pub fn parseAddress(raw_addr: ?[:0]const u8) BindError!std.net.Address {
const addr = raw_addr orelse {
return BindError.NoAddress;
};
if (addr.len == 0) {
return BindError.NoAddress;
}
var iter = std.mem.splitScalar(u8, addr, ':');
const host = iter.next() orelse return BindError.InvalidAddress;
const port = iter.next() orelse return BindError.InvalidAddress;
const numPort = std.fmt.parseInt(u16, port, 10) catch {
return std.net.Address.initUnix(addr) catch BindError.InvalidAddress;
};
const ip = std.net.Address.parseIp4(host, numPort) catch {
return std.net.Address.initUnix(addr) catch BindError.InvalidAddress;
};
return ip;
}
/// Deinitializes the server
pub fn deinit(self: *Server) void {
log.info("shutting down server", .{});
var c: xev.Completion = undefined;
self.socket.close(&self.loop, &c, Server, self, closeHandler);
self.buf_pool.deinit();
self.comp_pool.deinit();
self.sock_pool.deinit();
@ -80,7 +117,8 @@ pub fn deinit(self: *Server) void {
/// Starts the timer which tries to accept connections
pub fn start(self: *Server) !void {
try self.socket.listen(MAX_CLIENTS);
try self.socket.bind(self.addr);
try self.socket.listen(self.max_clients);
log.info("bound server to socket={any}", .{self.socket});
// Each acceptor borrows a completion from the pool
@ -91,7 +129,7 @@ pub fn start(self: *Server) !void {
};
self.acceptor.run(&self.loop, c, ACCEPTOR_RATE, Server, self, acceptor);
while (!self.stop) {
while (true) {
try self.loop.run(.until_done);
}
}
@ -153,14 +191,14 @@ fn acceptHandler(
return .disarm;
};
if (self.clients_count == MAX_CLIENTS) {
if (self.clients_count == self.max_clients) {
log.warn("max clients reached, rejecting fd={d}", .{sock.fd});
reject_client(self, sock) catch return .rearm;
return .disarm;
}
log.info("accepted connection fd={d}", .{sock.fd});
self.clients[self.clients_count] = sock;
// self.clients[self.clients_count] = sock;
self.clients_count += 1;
read_client(self, sock, c) catch {

View File

@ -5,6 +5,7 @@ const std = @import("std");
const xev = @import("xev");
const tcp = @import("../tcp.zig");
const App = @import("../App.zig");
const Config = @import("../Config.zig").Config;
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.tcp_thread);
@ -19,9 +20,17 @@ server: tcp.Server,
/// up all the internal state necessary prior to starting the thread. It
/// is up to the caller to start the thread with the threadMain entrypoint.
pub fn init(alloc: Allocator) !Thread {
// TODO: Configurable addresses and socket paths
const addr = try std.net.Address.parseIp4("127.0.0.1", 9090);
var server = try tcp.Server.init(alloc, addr);
const config = try Config.load(alloc);
const max_clients = config.@"remote-max-connections";
const addr = config.@"remote-tcp-socket";
const parsedAddr = tcp.Server.parseAddress(addr) catch |err| {
log.err("failed to parse address addr={any} err={any}", .{ addr, err });
return err;
};
log.debug("parsed address addr={any}", .{parsedAddr});
var server = try tcp.Server.init(alloc, parsedAddr, max_clients);
errdefer server.deinit();
return Thread{
@ -45,6 +54,6 @@ pub fn threadMain(self: *Thread) void {
fn threadMain_(self: *Thread) !void {
log.debug("starting tcp thread", .{});
defer log.debug("tcp thread exited", .{});
try self.server.start();
errdefer log.debug("tcp thread exited", .{});
}