diff --git a/docs/game_data/spel2.lua b/docs/game_data/spel2.lua index f65ec4daa..5db8c70b0 100644 --- a/docs/game_data/spel2.lua +++ b/docs/game_data/spel2.lua @@ -1403,13 +1403,6 @@ function toggle_journal() end ---@param page integer ---@return nil function show_journal(chapter, page) end ----Start an UDP server on specified address and run callback when data arrives. Return a string from the callback to reply. Requires unsafe mode. ----The server will be closed once the handle is released. ----@param host string ----@param port integer ----@param cb function ----@return UdpServer -function udp_listen(host, port, cb) end ---Send data to specified UDP address. Requires unsafe mode. ---@param host string ---@param port integer @@ -6568,6 +6561,13 @@ function Quad:is_point_inside(x, y, epsilon) end ---@field lava_bubbles_positions ScreenArenaScoreLavaBubble[] @size: 15 ---@class UdpServer + ---@field close fun(self): nil @Closes the server. + ---@field is_open fun(self): boolean @Returns true if the port was opened successfully and the server hasn't been closed yet. + ---@field last_error fun(self): string @Returns a string explaining the last error + ---@field host fun(self): string + ---@field port fun(self): integer + ---@field send fun(self, message: string, host: string, port: integer): sinteger @Send message to given host and port, returns numbers of bytes send, or -1 on failure + ---@field read fun(self, fun: function): sinteger @Read message from the socket buffer. Reads maximum of 32KiB at the time. If the message is longer, it will be split to portions of 32KiB.
On failure or empty buffer returns -1, on success calls the function with signature `nil(message, source)` ---@class LogicList ---@field tutorial LogicTutorial @Handles dropping of the torch and rope in intro routine (first time play) @@ -6945,6 +6945,12 @@ function Quad:new(_bottom_left_x, _bottom_left_y, _bottom_right_x, _bottom_right ---@return Quad function Quad:new(aabb) end +UdpServer = nil +---@param host string +---@param port integer +---@return UdpServer +function UdpServer:new(host, port) end + MagmamanSpawnPosition = nil ---@param x_ integer ---@param y_ integer diff --git a/docs/src/includes/_globals.md b/docs/src/includes/_globals.md index 8a6291a92..6cdf9714f 100644 --- a/docs/src/includes/_globals.md +++ b/docs/src/includes/_globals.md @@ -2523,16 +2523,6 @@ Send a synchronous HTTP GET request and return response as a string or nil on an Send an asynchronous HTTP GET request and run the callback when done. If there is an error, response will be nil and vice versa. The callback signature is nil on_data(string response, string error) -### udp_listen - - -> Search script examples for [udp_listen](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=udp_listen) - -#### [UdpServer](#UdpServer) udp_listen(string host, int port, function cb) - -Start an UDP server on specified address and run callback when data arrives. Return a string from the callback to reply. Requires unsafe mode. -The server will be closed once the handle is released. - ### udp_send @@ -4076,6 +4066,16 @@ Use [GuiDrawContext](#GuiDrawContext)`.win_popid` instead `nil win_image(IMAGE image, float width, float height)`
Use [GuiDrawContext](#GuiDrawContext)`.win_image` instead +### udp_listen + + +> Search script examples for [udp_listen](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=udp_listen) + +`nil udp_listen(string host, int port, function cb)`
+Start an UDP server on specified address and run callback when data arrives. Set port to 0 to use a random ephemeral port. Return a string from the callback to reply. +The server will be closed lazily by garbage collection once the handle is released, or immediately by calling close(). Requires unsafe mode. +
The callback signature is optional on_message(string msg, string src) + ### read_prng diff --git a/docs/src/includes/_types.md b/docs/src/includes/_types.md index a026e4caf..0132465d7 100644 --- a/docs/src/includes/_types.md +++ b/docs/src/includes/_types.md @@ -1096,6 +1096,14 @@ tuple<[Vec2](#Vec2), [Vec2](#Vec2), [Vec2](#Vec2)> | [split()](https://git Type | Name | Description ---- | ---- | ----------- +[UdpServer](#UdpServer) | [new(string host, int port)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=UdpServer) | +nil | [close()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=close) | Closes the server. +bool | [is_open()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=is_open) | Returns true if the port was opened successfully and the server hasn't been closed yet. +string | [last_error()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=last_error) | Returns a string explaining the last error +string | [host()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=host) | +int | [port()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=port) | +sint | [send(string message, string host, int port)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=send) | Send message to given host and port, returns numbers of bytes send, or -1 on failure +sint | [read(function fun)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=read) | Read message from the socket buffer. Reads maximum of 32KiB at the time. If the message is longer, it will be split to portions of 32KiB.
On failure or empty buffer returns -1, on success calls the function with signature `nil(message, source)` ### Vec2 diff --git a/examples/udp.lua b/examples/udp.lua new file mode 100644 index 000000000..2ed5b2bf8 --- /dev/null +++ b/examples/udp.lua @@ -0,0 +1,82 @@ +meta = { + name = "UDP echo example", + description = "Opens UDP server on a random port and echoes sent messages, closes the server on 'gg'", + author = "Dregu", + unsafe = true +} + +options = { + host = "127.0.0.1", + port = 0 +} + +function init() + deinit() + + server = { + -- Save messages to queue instead of printing directly, cause that doesn't work from the server thread + queue = {}, + + -- Open server on an ephemeral random port, push messages to queue and echo back to sender + socket = udp_listen(options.host, options.port, function(msg, src) + msg = msg:gsub("%s*$", "") + table.insert(server.queue, { msg = msg, src = src }) + if msg == "gg" then + return "bye!\n" + else + return "echo: " .. msg .. "\n" + end + end) + } + + -- If port was opened successfully, start checking the message queue + if server.socket:open() then + print(F "Listening on {server.socket.port}, please send some UDP datagrams or 'gg' to close") + server.inter = set_global_interval(function() + for _, msg in pairs(server.queue) do + print(F "Received: '{msg.msg}' from {msg.src}") + if msg.msg == "gg" then + server.socket:close() + clear_callback() + print("Server is now closed, have a nice day") + end + end + server.queue = {} + end, 1) + else + print(F "Failed to open server: {server.socket:error()}") + end +end + +function deinit() + if server then + server.socket:close() + if server.inter then clear_callback(server.inter) end + server = nil + end +end + +set_callback(init, ON.LOAD) +set_callback(init, ON.SCRIPT_ENABLE) +set_callback(deinit, ON.SCRIPT_DISABLE) + +register_option_callback("x", nil, function(ctx) + options.host = ctx:win_input_text("Host", options.host) + options.port = ctx:win_input_int("Port", options.port) + if options.port < 0 then options.port = 0 end + if options.port > 65535 then options.port = 65535 end + if ctx:win_button("Start server") then init() end + ctx:win_inline() + if ctx:win_button("Stop server") then deinit() end + if server then + ctx:win_text(server.socket:error()) + end + if server and server.socket:open() then + ctx:win_text(F "Listening on {server.socket.host}:{server.socket.port}\nTry sending something with udp_send:") + if ctx:win_button("Send 'Hello World!'") then udp_send(server.socket.host, server.socket.port, "Hello World!") end + ctx:win_inline() + if ctx:win_button("Send 'gg'") then udp_send(server.socket.host, server.socket.port, "gg") end + else + ctx:win_text("Stopped") + end +end) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index aa84f70d3..e60ae7ac6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -215,6 +215,9 @@ if(BUILD_OVERLUNKY) endif() endif() +set(SOCKPP_BUILD_SHARED OFF CACHE BOOL "No shared") +set(SOCKPP_BUILD_STATIC ON CACHE BOOL "Static") + add_subdirectory(sockpp) target_include_directories(spel2_api PUBLIC sockpp/include) target_link_libraries_system(spel2_api PUBLIC sockpp-static) diff --git a/src/game_api/script/usertypes/socket_lua.cpp b/src/game_api/script/usertypes/socket_lua.cpp index 364c31ef6..b6ae8a4f2 100644 --- a/src/game_api/script/usertypes/socket_lua.cpp +++ b/src/game_api/script/usertypes/socket_lua.cpp @@ -1,6 +1,7 @@ #include "socket_lua.hpp" #include "socket.hpp" +#include // for unique_ptr #include // for global_table, proxy_key_t, function #include // for ssize_t @@ -8,20 +9,46 @@ #include "script/lua_backend.hpp" // for LuaBackend #include "script/safe_cb.hpp" // for make_safe_cb +class UdpServer_lua +{ + std::unique_ptr m_ptr; + + public: + UdpServer_lua(std::unique_ptr ptr) + : m_ptr(std::move(ptr)){}; +}; + namespace NSocket { void register_usertypes(sol::state& lua) { lua.new_usertype( "UdpServer", - sol::no_constructor); - /// Start an UDP server on specified address and run callback when data arrives. Return a string from the callback to reply. Requires unsafe mode. - /// The server will be closed once the handle is released. - lua["udp_listen"] = [](std::string host, in_port_t port, sol::function cb) -> UdpServer* + sol::constructors(), + "close", + &UdpServer::close, + "is_open", + &UdpServer::is_open, + "last_error", + &UdpServer::last_error, + "host", + &UdpServer::get_host, + "port", + &UdpServer::get_port, + "send", + &UdpServer::send, + "read", + &UdpServer::read); + + /// Deprecated + /// Start an UDP server on specified address and run callback when data arrives. Set port to 0 to use a random ephemeral port. Return a string from the callback to reply. + /// The server will be closed lazily by garbage collection once the handle is released, or immediately by calling close(). Requires unsafe mode. + ///
The callback signature is optional on_message(string msg, string src) + lua["udp_listen"] = [](std::string host, in_port_t port, sol::function cb) //-> sol::any { - // TODO: change the return to std::unique_ptr after fixing the dead lock with the destructor - UdpServer* server = new UdpServer(std::move(host), std::move(port), make_safe_cb(std::move(cb))); - return server; + auto server = std::unique_ptr{new UdpServer(std::move(host), port)}; + server->start_callback(make_safe_cb(std::move(cb))); + return UdpServer_lua(std::move(server)); }; /// Send data to specified UDP address. Requires unsafe mode. diff --git a/src/game_api/socket.cpp b/src/game_api/socket.cpp index 5240b5b39..2ec00111c 100644 --- a/src/game_api/socket.cpp +++ b/src/game_api/socket.cpp @@ -1,16 +1,14 @@ #include "socket.hpp" #include // for GetModuleHandleA, GetProcAddress -#include // for max +#include // for chrono #include // for DetourAttach, DetourTransactionBegin -#include // for exception #include // for operator new #include // for inet_address #include // for udp_socket #include // for thread #include // for get #include // for move -#include // for max, min #include // for InternetCloseHandle, InternetOpenA, InternetG... #include // for sockaddr_in, SOCKET #include // for inet_ntop @@ -53,41 +51,89 @@ void dump_network() } } -void udp_data(sockpp::udp_socket socket, UdpServer* server) +void UdpServer::callback(sockpp::udp_socket sock, std::function cb) { - ssize_t n; - char buf[500]; - sockpp::inet_address src; - while (server->kill_thr.test(std::memory_order_acquire) && (n = socket.recv_from(buf, sizeof(buf), &src)) > 0) + static thread_local char buf[32768]; + while (m_opened.load(std::memory_order::relaxed) && m_sock.is_open()) { - std::optional ret = server->cb(std::string(buf, n)); - if (ret) + sockpp::inet_address src; + ssize_t n; + while ((n = sock.recv_from(buf, sizeof(buf), &src)) > 0) { - socket.send_to(ret.value(), src); + std::optional ret = cb(std::string(buf, n), src.to_string()); + if (ret.has_value()) + sock.send_to(ret.value(), src); } + std::this_thread::sleep_for(std::chrono::microseconds(1)); } + m_opened = false; + sock.shutdown(); } -UdpServer::UdpServer(std::string host_, in_port_t port_, std::function cb_) - : host(host_), port(port_), cb(cb_) +UdpServer::UdpServer(std::string host, in_port_t port) + : m_host(host), m_port(port) { - sock.bind(sockpp::inet_address(host, port)); - kill_thr.test_and_set(); - thr = std::thread(udp_data, std::move(sock), this); + if (m_sock.bind(sockpp::inet_address(host, port))) + { + const auto addr = sockpp::inet_address(m_sock.address()); + port = addr.port(); + m_opened = true; + m_sock.set_non_blocking(); + } } -void UdpServer::clear() // TODO: fix and expose: this and the destructor causes deadlock +void UdpServer::close() { - kill_thr.clear(std::memory_order_release); - thr.join(); + m_opened = false; + if (!m_thread.joinable()) + m_sock.close(); } -UdpServer::~UdpServer() +ssize_t UdpServer::send(std::string message, std::string host, in_port_t port) +{ + if (!is_open()) + return -1; + + return m_sock.send_to(message, sockpp::inet_address(host, port)); +} +ssize_t UdpServer::read(std::function fun) +{ + if (!is_open()) + return -1; + + static char buf[32768]; + sockpp::inet_address src; + auto ret = m_sock.recv_from(buf, sizeof(buf), &src); + + if (ret > -1) + fun(std::string(buf, static_cast(ret)), src.to_string()); + + return ret; +} +bool UdpServer::is_open() const { - if (thr.joinable()) + return m_opened.load(std::memory_order::memory_order_relaxed) && m_sock.is_open() && m_sock.last_error() > -1; +} +void UdpServer::start_callback(std::function cb) +{ + if (!m_thread.joinable()) { - kill_thr.clear(std::memory_order_release); - thr.join(); + auto sock_copy = m_sock.clone(); + m_thread = std::thread(&UdpServer::callback, this, std::move(sock_copy), std::move(cb)); } } +std::string UdpServer::last_error() const +{ + auto err = m_sock.last_error_str(); + err.resize(err.size() - 2); + return err; +} +UdpServer::~UdpServer() +{ + close(); + if (m_thread.joinable()) + m_thread.join(); + + m_sock.close(); +} bool http_get(const char* sURL, std::string& out, std::string& err) { @@ -97,7 +143,7 @@ bool http_get(const char* sURL, std::string& out, std::string& err) const char* sHeader = NULL; HINTERNET hInternet; HINTERNET hConnect; - char acBuffer[BUFFER_SIZE]; + static thread_local char acBuffer[BUFFER_SIZE]; DWORD iReadBytes; DWORD iBytesToRead = 0; DWORD iReadBytesOfRq = 4; diff --git a/src/game_api/socket.hpp b/src/game_api/socket.hpp index c6904967a..6f12cb9ee 100644 --- a/src/game_api/socket.hpp +++ b/src/game_api/socket.hpp @@ -11,18 +11,48 @@ class UdpServer { public: - using SocketCb = std::optional(std::string); + using SocketCb = std::optional(std::string, std::string); + using ReadFun = void(std::string, std::string); - UdpServer(std::string host, in_port_t port, std::function cb); + UdpServer(std::string host, in_port_t port); ~UdpServer(); - void clear(); - - std::string host; - in_port_t port; - std::function cb; - std::thread thr; - std::atomic_flag kill_thr; - sockpp::udp_socket sock; + + /// Closes the server. + void close(); + + /// Send message to given host and port, returns numbers of bytes send, or -1 on failure + ssize_t send(std::string message, std::string host, in_port_t port); + + /// Read message from the socket buffer. Reads maximum of 32KiB at the time. If the message is longer, it will be split to portions of 32KiB. + /// On failure or empty buffer returns -1, on success calls the function with signature `nil(message, source)` + ssize_t read(std::function fun); + + /// Returns true if the port was opened successfully and the server hasn't been closed yet. + bool is_open() const; + + /// Returns a string explaining the last error + std::string last_error() const; + + std::string get_host() const + { + return m_host; + } + in_port_t get_port() const + { + return m_port; + } + + // added only for backwards compatibility, do not use or expose + void start_callback(std::function cb_); + + private: + void callback(sockpp::udp_socket sock, std::function cb); + std::string m_host; + in_port_t m_port; + std::thread m_thread; + sockpp::udp_socket m_sock; + + std::atomic m_opened{false}; }; class HttpRequest diff --git a/src/game_api/state.cpp b/src/game_api/state.cpp index 1e9a5aa92..8555cf074 100644 --- a/src/game_api/state.cpp +++ b/src/game_api/state.cpp @@ -1,13 +1,14 @@ #include "state.hpp" -#include // for GetCurrentThread, LONG, NO_ERROR -#include // for abs -#include // for size_t, abs -#include // for DetourAttach, DetourTransactionBegin -#include // for _Func_class, function -#include // for operator new -#include // for allocator, operator""sv, operator""s -#include // for move +#include // for GetCurrentThread, LONG, NO_ERROR +#include // for abs +#include // for size_t, abs +#include // for DetourAttach, DetourTransactionBegin +#include // for _Func_class, function +#include // for operator new +#include // for initialize +#include // for allocator, operator""sv, operator""s +#include // for move #include "bucket.hpp" // for Bucket #include "containers/custom_allocator.hpp" // @@ -986,6 +987,7 @@ void API::init(SoundManager* sound_manager) init_process_input_hook(); init_game_loop_hook(); init_heap_clone_hook(); + sockpp::initialize(); auto bucket = Bucket::get(); bucket->count++; diff --git a/src/sockpp b/src/sockpp index 93855d54e..e6c4688a5 160000 --- a/src/sockpp +++ b/src/sockpp @@ -1 +1 @@ -Subproject commit 93855d54e78ea2684abe0f75caf4dbdf98bffcab +Subproject commit e6c4688a576d95f42dd7628cefe68092f6c5cd0f