From 8e3d0a23d555b2464aba5a07a90092535cec19e1 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Mon, 18 Mar 2019 14:09:58 +0100 Subject: LibGUI: Add GTCPSocket and base class GSocket (inherits from GIODevice.) And use these to do the line-by-line reading automagically instead of having that logic in IRCClient. This will definitely come in handy. --- Applications/IRCClient/IRCClient.cpp | 80 +++++++++++------------------------- Applications/IRCClient/IRCClient.h | 14 ++++--- Kernel/IPv4.h | 6 ++- LibGUI/GFile.cpp | 14 ------- LibGUI/GFile.h | 1 - LibGUI/GIODevice.cpp | 68 ++++++++++++++++++++++++++---- LibGUI/GIODevice.h | 7 +++- LibGUI/GSocket.cpp | 64 +++++++++++++++++++++++++++++ LibGUI/GSocket.h | 68 ++++++++++++++++++++++++++++++ LibGUI/GTCPSocket.cpp | 19 +++++++++ LibGUI/GTCPSocket.h | 10 +++++ LibGUI/Makefile | 2 + 12 files changed, 267 insertions(+), 86 deletions(-) create mode 100644 LibGUI/GSocket.cpp create mode 100644 LibGUI/GSocket.h create mode 100644 LibGUI/GTCPSocket.cpp create mode 100644 LibGUI/GTCPSocket.h diff --git a/Applications/IRCClient/IRCClient.cpp b/Applications/IRCClient/IRCClient.cpp index 33e112d63d..9d09a31988 100644 --- a/Applications/IRCClient/IRCClient.cpp +++ b/Applications/IRCClient/IRCClient.cpp @@ -26,6 +26,7 @@ IRCClient::IRCClient(const String& address, int port) , m_nickname("anon") , m_log(IRCLogBuffer::create()) { + m_socket = new GTCPSocket(this); m_client_window_list_model = new IRCWindowListModel(*this); } @@ -35,37 +36,15 @@ IRCClient::~IRCClient() bool IRCClient::connect() { - if (m_socket_fd != -1) { + if (m_socket->is_connected()) ASSERT_NOT_REACHED(); - } - - m_socket_fd = socket(AF_INET, SOCK_STREAM, 0); - if (m_socket_fd < 0) { - perror("socket"); - exit(1); - } - - struct sockaddr_in addr; - memset(&addr, 0, sizeof(addr)); - - addr.sin_family = AF_INET; - addr.sin_port = htons(m_port); - int rc = inet_pton(AF_INET, m_hostname.characters(), &addr.sin_addr); - if (rc < 0) { - perror("inet_pton"); - exit(1); - } - printf("Connecting to %s...", m_hostname.characters()); - fflush(stdout); - rc = ::connect(m_socket_fd, (struct sockaddr*)&addr, sizeof(addr)); - if (rc < 0) { - perror("connect"); - exit(1); - } - printf("ok!\n"); + IPv4Address ipv4_address(127, 0, 0, 1); + bool success = m_socket->connect(GSocketAddress(ipv4_address), m_port); + if (!success) + return false; - m_notifier = make(m_socket_fd, GNotifier::Read); + m_notifier = make(m_socket->fd(), GNotifier::Read); m_notifier->on_ready_to_read = [this] (GNotifier&) { receive_from_server(); }; send_user(); @@ -78,35 +57,20 @@ bool IRCClient::connect() void IRCClient::receive_from_server() { - char buffer[4096]; - int nread = recv(m_socket_fd, buffer, sizeof(buffer) - 1, 0); - if (nread < 0) { - perror("recv"); - exit(1); - } - if (nread == 0) { - printf("IRCClient: Connection closed!\n"); - exit(1); - } - buffer[nread] = '\0'; -#if 0 - printf("Received: '%s'\n", buffer); -#endif - - for (int i = 0; i < nread; ++i) { - char ch = buffer[i]; - if (ch == '\r') - continue; - if (ch == '\n') { - process_line(); - m_line_buffer.clear_with_capacity(); - continue; + while (m_socket->can_read_line()) { + auto line = m_socket->read_line(4096); + if (line.is_null()) { + if (!m_socket->is_connected()) { + printf("IRCClient: Connection closed!\n"); + exit(1); + } + ASSERT_NOT_REACHED(); } - m_line_buffer.append(ch); + process_line(move(line)); } } -void IRCClient::process_line() +void IRCClient::process_line(ByteBuffer&& line) { Message msg; Vector prefix; @@ -121,7 +85,12 @@ void IRCClient::process_line() InTrailingParameter, } state = Start; - for (char ch : m_line_buffer) { + for (int i = 0; i < line.size(); ++i) { + char ch = line[i]; + if (ch == '\r') + continue; + if (ch == '\n') + break; switch (state) { case Start: if (ch == ':') { @@ -175,8 +144,7 @@ void IRCClient::process_line() void IRCClient::send(const String& text) { - int rc = ::send(m_socket_fd, text.characters(), text.length(), 0); - if (rc < 0) { + if (!m_socket->send(ByteBuffer::wrap((void*)text.characters(), text.length()))) { perror("send"); exit(1); } diff --git a/Applications/IRCClient/IRCClient.h b/Applications/IRCClient/IRCClient.h index 304353171e..c35f3da10a 100644 --- a/Applications/IRCClient/IRCClient.h +++ b/Applications/IRCClient/IRCClient.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "IRCLogBuffer.h" #include "IRCWindow.h" @@ -12,12 +13,12 @@ class IRCQuery; class IRCWindowListModel; class GNotifier; -class IRCClient { +class IRCClient final : public GObject { friend class IRCChannel; friend class IRCQuery; public: IRCClient(const String& address, int port = 6667); - ~IRCClient(); + virtual ~IRCClient() override; bool connect(); @@ -59,6 +60,8 @@ public: IRCQuery& ensure_query(const String& name); IRCChannel& ensure_channel(const String& name); + const char* class_name() const override { return "IRCClient"; } + private: struct Message { String prefix; @@ -72,7 +75,7 @@ private: void send_nick(); void send_pong(const String& server); void send_privmsg(const String& target, const String&); - void process_line(); + void process_line(ByteBuffer&&); void handle_join(const Message&); void handle_part(const Message&); void handle_ping(const Message&); @@ -84,8 +87,9 @@ private: void handle_user_command(const String&); String m_hostname; - int m_port { 0 }; - int m_socket_fd { -1 }; + int m_port { 6667 }; + + GTCPSocket* m_socket { nullptr }; String m_nickname; Vector m_line_buffer; diff --git a/Kernel/IPv4.h b/Kernel/IPv4.h index 55cac8d78a..dc4e4dff3c 100644 --- a/Kernel/IPv4.h +++ b/Kernel/IPv4.h @@ -4,7 +4,6 @@ #include #include #include -#include enum class IPv4Protocol : word { ICMP = 1, @@ -19,7 +18,10 @@ public: IPv4Address() { } IPv4Address(const byte data[4]) { - memcpy(m_data, data, 4); + m_data[0] = data[0]; + m_data[1] = data[1]; + m_data[2] = data[2]; + m_data[3] = data[3]; } IPv4Address(byte a, byte b, byte c, byte d) { diff --git a/LibGUI/GFile.cpp b/LibGUI/GFile.cpp index 45b083191c..e0ff42e0bb 100644 --- a/LibGUI/GFile.cpp +++ b/LibGUI/GFile.cpp @@ -40,17 +40,3 @@ bool GFile::open(GIODevice::OpenMode mode) set_mode(mode); return true; } - -bool GFile::close() -{ - if (fd() < 0 || mode() == NotOpen) - return false; - int rc = ::close(fd()); - if (rc < 0) { - set_error(rc); - return false; - } - set_fd(-1); - set_mode(GIODevice::NotOpen); - return true; -} diff --git a/LibGUI/GFile.h b/LibGUI/GFile.h index de7325fb93..89aadf0aad 100644 --- a/LibGUI/GFile.h +++ b/LibGUI/GFile.h @@ -13,7 +13,6 @@ public: void set_filename(const String& filename) { m_filename = filename; } virtual bool open(GIODevice::OpenMode) override; - virtual bool close() override; virtual const char* class_name() const override { return "GFile"; } diff --git a/LibGUI/GIODevice.cpp b/LibGUI/GIODevice.cpp index d62d36622f..0b0f8de1d1 100644 --- a/LibGUI/GIODevice.cpp +++ b/LibGUI/GIODevice.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include GIODevice::GIODevice(GObject* parent) : GObject(parent) @@ -44,26 +46,63 @@ ByteBuffer GIODevice::read(int max_size) return buffer; } +bool GIODevice::can_read() const +{ + // FIXME: Can we somehow remove this once GSocket is implemented using non-blocking sockets? + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(m_fd, &rfds); + struct timeval timeout { 0, 0 }; + int rc = select(m_fd + 1, &rfds, nullptr, nullptr, &timeout); + if (rc < 0) { + // NOTE: We don't set m_error here. + perror("GIODevice::can_read: select"); + return false; + } + return FD_ISSET(m_fd, &rfds); +} + +bool GIODevice::can_read_line() +{ + if (m_eof && !m_buffered_data.is_empty()) + return true; + if (m_buffered_data.contains_slow('\n')) + return true; + if (!can_read()) + return false; + populate_read_buffer(); + return m_buffered_data.contains_slow('\n'); +} + ByteBuffer GIODevice::read_line(int max_size) { if (m_fd < 0) return { }; if (!max_size) return { }; - auto line = ByteBuffer::create_uninitialized(max_size); - int line_index = 0; - while (line_index < line.size()) { - if (line_index >= m_buffered_data.size()) { - if (!populate_read_buffer()) - return { }; + if (!can_read_line()) + return { }; + if (m_eof) { + if (m_buffered_data.size() > max_size) { + dbgprintf("GIODevice::read_line: At EOF but there's more than max_size(%d) buffered\n", max_size); + return { }; } + auto buffer = ByteBuffer::copy(m_buffered_data.data(), m_buffered_data.size()); + m_buffered_data.clear(); + return buffer; + } + auto line = ByteBuffer::create_uninitialized(max_size + 1); + int line_index = 0; + while (line_index < max_size) { byte ch = m_buffered_data[line_index]; line[line_index++] = ch; if (ch == '\n') { Vector new_buffered_data; new_buffered_data.append(m_buffered_data.data() + line_index, m_buffered_data.size() - line_index); m_buffered_data = move(new_buffered_data); - line.trim(line_index); + line[line_index] = '\0'; + line.trim(line_index + 1); + dbgprintf("GIODevice::read_line: '%s'\n", line.pointer()); return line; } } @@ -84,6 +123,21 @@ bool GIODevice::populate_read_buffer() set_eof(true); return false; } + buffer.trim(nread); m_buffered_data.append(buffer.pointer(), buffer.size()); return true; } + +bool GIODevice::close() +{ + if (fd() < 0 || mode() == NotOpen) + return false; + int rc = ::close(fd()); + if (rc < 0) { + set_error(rc); + return false; + } + set_fd(-1); + set_mode(GIODevice::NotOpen); + return true; +} diff --git a/LibGUI/GIODevice.h b/LibGUI/GIODevice.h index 744e1997d0..d7d7cd094c 100644 --- a/LibGUI/GIODevice.h +++ b/LibGUI/GIODevice.h @@ -29,8 +29,13 @@ public: ByteBuffer read(int max_size); ByteBuffer read_line(int max_size); + // FIXME: I would like this to be const but currently it needs to call populate_read_buffer(). + bool can_read_line(); + + bool can_read() const; + virtual bool open(GIODevice::OpenMode) = 0; - virtual bool close() = 0; + virtual bool close(); virtual const char* class_name() const override { return "GIODevice"; } diff --git a/LibGUI/GSocket.cpp b/LibGUI/GSocket.cpp new file mode 100644 index 0000000000..78343f2225 --- /dev/null +++ b/LibGUI/GSocket.cpp @@ -0,0 +1,64 @@ +#include +#include +#include +#include +#include + +GSocket::GSocket(Type type, GObject* parent) + : GIODevice(parent) + , m_type(type) +{ +} + +GSocket::~GSocket() +{ +} + +bool GSocket::connect(const GSocketAddress& address, int port) +{ + ASSERT(!is_connected()); + ASSERT(address.type() == GSocketAddress::Type::IPv4); + ASSERT(port > 0 && port <= 65535); + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + auto ipv4_address = address.ipv4_address(); + memcpy(&addr.sin_addr.s_addr, &ipv4_address, sizeof(IPv4Address)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + + printf("Connecting to %s...", address.to_string().characters()); + fflush(stdout); + int rc = ::connect(fd(), (struct sockaddr*)&addr, sizeof(addr)); + if (rc < 0) { + perror("connect"); + exit(1); + } + printf("ok!\n"); + + m_destination_address = address; + m_destination_port = port; + m_connected = true; + return true; +} + +ByteBuffer GSocket::receive(int max_size) +{ + auto buffer = read(max_size); + if (eof()) { + dbgprintf("GSocket{%p}: Connection appears to have closed in receive().\n", this); + m_connected = false; + } + return buffer; +} + +bool GSocket::send(const ByteBuffer& data) +{ + int nsent = ::send(fd(), data.pointer(), data.size(), 0); + if (nsent < 0) { + set_error(nsent); + return false; + } + ASSERT(nsent == data.size()); + return true; +} diff --git a/LibGUI/GSocket.h b/LibGUI/GSocket.h new file mode 100644 index 0000000000..a4871f6480 --- /dev/null +++ b/LibGUI/GSocket.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include + +class GSocketAddress { +public: + enum class Type { Invalid, IPv4, Local }; + + GSocketAddress() { } + GSocketAddress(const IPv4Address& address) + : m_type(Type::IPv4) + , m_ipv4_address(address) + { + } + + Type type() const { return m_type; } + bool is_valid() const { return m_type != Type::Invalid; } + IPv4Address ipv4_address() const { return m_ipv4_address; } + + String to_string() const + { + switch (m_type) { + case Type::IPv4: return m_ipv4_address.to_string(); + default: return "[GSocketAddress]"; + } + } + +private: + Type m_type { Type::Invalid }; + IPv4Address m_ipv4_address; +}; + +class GSocket : public GIODevice { +public: + enum class Type { Invalid, TCP, UDP }; + virtual ~GSocket() override; + + bool connect(const GSocketAddress&, int port); + + ByteBuffer receive(int max_size); + bool send(const ByteBuffer&); + + bool is_connected() const { return m_connected; } + + GSocketAddress source_address() const { return m_source_address; } + int source_port() const { return m_source_port; } + + GSocketAddress destination_address() const { return m_source_address; } + int destination_port() const { return m_destination_port; } + + virtual const char* class_name() const override { return "GSocket"; } + +protected: + GSocket(Type, GObject* parent); + + GSocketAddress m_source_address; + GSocketAddress m_destination_address; + int m_source_port { -1 }; + int m_destination_port { -1 }; + bool m_connected { false }; + +private: + virtual bool open(GIODevice::OpenMode) override { ASSERT_NOT_REACHED(); } + Type m_type { Type::Invalid }; + +}; diff --git a/LibGUI/GTCPSocket.cpp b/LibGUI/GTCPSocket.cpp new file mode 100644 index 0000000000..1194e8adf0 --- /dev/null +++ b/LibGUI/GTCPSocket.cpp @@ -0,0 +1,19 @@ +#include +#include + +GTCPSocket::GTCPSocket(GObject* parent) + : GSocket(GSocket::Type::TCP, parent) +{ + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + set_error(fd); + } else { + set_fd(fd); + set_mode(GIODevice::ReadWrite); + set_error(0); + } +} + +GTCPSocket::~GTCPSocket() +{ +} diff --git a/LibGUI/GTCPSocket.h b/LibGUI/GTCPSocket.h new file mode 100644 index 0000000000..691da1731e --- /dev/null +++ b/LibGUI/GTCPSocket.h @@ -0,0 +1,10 @@ +#include + +class GTCPSocket final : public GSocket { +public: + explicit GTCPSocket(GObject* parent); + virtual ~GTCPSocket() override; + +private: +}; + diff --git a/LibGUI/Makefile b/LibGUI/Makefile index c39478d130..098c3cf779 100644 --- a/LibGUI/Makefile +++ b/LibGUI/Makefile @@ -40,6 +40,8 @@ LIBGUI_OBJS = \ GStackWidget.o \ GEvent.o \ GScrollableWidget.o \ + GSocket.o \ + GTCPSocket.o \ GWindow.o OBJS = $(SHAREDGRAPHICS_OBJS) $(LIBGUI_OBJS) -- cgit v1.2.3