From 6979664bbd29ec0cb5c23748dd15def014174516 Mon Sep 17 00:00:00 2001 From: Pablo Andres Fuente Date: Fri, 7 Nov 2025 00:50:34 -0300 Subject: [PATCH] Add LSP stdio mode Closes godotengine/godot-proposals#464 Now Godot can be started as a headless LSP server that reads from stdin and writes to stdout with the `--lsp` CLI parameter. --- core/io/stream_peer_stdio.cpp | 117 ++++++++++++++ core/io/stream_peer_stdio.h | 51 ++++++ main/main.cpp | 8 + .../gdscript_language_protocol.cpp | 78 +++++---- .../gdscript_language_protocol.h | 6 +- .../gdscript_language_server.cpp | 28 +++- .../gdscript_language_server.h | 1 + .../language_server/gdscript_workspace.cpp | 1 + tests/core/io/test_stream_peer_stdio.h | 149 ++++++++++++++++++ tests/test_main.cpp | 1 + 10 files changed, 403 insertions(+), 37 deletions(-) create mode 100644 core/io/stream_peer_stdio.cpp create mode 100644 core/io/stream_peer_stdio.h create mode 100644 tests/core/io/test_stream_peer_stdio.h diff --git a/core/io/stream_peer_stdio.cpp b/core/io/stream_peer_stdio.cpp new file mode 100644 index 000000000000..52ae060d2274 --- /dev/null +++ b/core/io/stream_peer_stdio.cpp @@ -0,0 +1,117 @@ +/**************************************************************************/ +/* stream_peer_stdio.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "stream_peer_stdio.h" + +#include +#include +#include + +#ifdef WINDOWS_ENABLED +#include +#define READ_FUNCTION _read +#define WRITE_FUNCTION _write +#else +#include +#define READ_FUNCTION read +#define WRITE_FUNCTION write +#endif + +StreamPeerStdio::StreamPeerStdio() { + // Set stdin to non-blocking mode and binary mode +#ifdef WINDOWS_ENABLED + stdin_fileno = _fileno(stdin); + stdout_fileno = _fileno(stdout); + + _setmode(stdin_fileno, _O_BINARY); + _setmode(stdout_fileno, _O_BINARY); +#else + stdin_fileno = STDIN_FILENO; + stdout_fileno = STDOUT_FILENO; + + int flags = fcntl(stdin_fileno, F_GETFL, 0); + fcntl(stdin_fileno, F_SETFL, flags | O_NONBLOCK); +#endif +} + +StreamPeerStdio::~StreamPeerStdio() { + // TODO: see if make sense to revert the non-blocking +} + +Error StreamPeerStdio::put_data(const uint8_t *p_data, int p_bytes) { + int sent; + return put_partial_data(p_data, p_bytes, sent); +} + +Error StreamPeerStdio::put_partial_data(const uint8_t *p_data, int p_bytes, int &r_sent) { + int sent = WRITE_FUNCTION(stdout_fileno, p_data, p_bytes); + if (sent < 0) { + r_sent = 0; + return FAILED; + } + + r_sent = sent; + fflush(stdout); + + return OK; +} + +Error StreamPeerStdio::get_data(uint8_t *p_buffer, int p_bytes) { + int received; + return get_partial_data(p_buffer, p_bytes, received); +} + +GODOT_GCC_WARNING_PUSH_AND_IGNORE("-Wlogical-op") // Silence a false positive. See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=69602 + +Error StreamPeerStdio::get_partial_data(uint8_t *p_buffer, int p_bytes, int &r_received) { + int received = READ_FUNCTION(stdin_fileno, p_buffer, p_bytes); + if (received < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + r_received = 0; + return ERR_BUSY; + } + r_received = 0; + return FAILED; + } else if (received == 0) { // EOF + r_received = 0; + return FAILED; + } + + r_received = received; + return OK; +} + +GODOT_GCC_WARNING_POP + +int StreamPeerStdio::get_available_bytes() const { + // Return 1 to indicate data might be available + // The actual read will handle EAGAIN/EWOULDBLOCK + return 1; +} diff --git a/core/io/stream_peer_stdio.h b/core/io/stream_peer_stdio.h new file mode 100644 index 000000000000..09f15db6044c --- /dev/null +++ b/core/io/stream_peer_stdio.h @@ -0,0 +1,51 @@ +/**************************************************************************/ +/* stream_peer_stdio.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "core/io/stream_peer.h" + +class StreamPeerStdio : public StreamPeer { + GDCLASS(StreamPeerStdio, StreamPeer); + +private: + int stdin_fileno = 0; + int stdout_fileno = 1; + +public: + Error put_data(const uint8_t *p_data, int p_bytes) override; + Error put_partial_data(const uint8_t *p_data, int p_bytes, int &r_sent) override; + Error get_data(uint8_t *p_buffer, int p_bytes) override; + Error get_partial_data(uint8_t *p_buffer, int p_bytes, int &r_received) override; + int get_available_bytes() const override; + + StreamPeerStdio(); + virtual ~StreamPeerStdio(); +}; diff --git a/main/main.cpp b/main/main.cpp index 1dc73e600404..d560eb99437a 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -550,6 +550,7 @@ void Main::print_help(const char *p_binary) { print_help_option("--dap-port ", "Use the specified port for the GDScript Debug Adapter Protocol. Recommended port range [1024, 49151].\n", CLI_OPTION_AVAILABILITY_EDITOR); #if defined(MODULE_GDSCRIPT_ENABLED) && !defined(GDSCRIPT_NO_LSP) print_help_option("--lsp-port ", "Use the specified port for the GDScript Language Server Protocol. Recommended port range [1024, 49151].\n", CLI_OPTION_AVAILABILITY_EDITOR); + print_help_option("--lsp", "Start the GDScript Language Server in headless mode (stdio). Useful for integration with external editors.\n", CLI_OPTION_AVAILABILITY_EDITOR); #endif // MODULE_GDSCRIPT_ENABLED && !GDSCRIPT_NO_LSP #endif print_help_option("--quit", "Quit after the first iteration.\n"); @@ -1881,6 +1882,13 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph OS::get_singleton()->print("Missing argument for --lsp-port .\n"); goto error; } + } else if (arg == "--lsp") { + editor = true; + GDScriptLanguageServer::use_stdio = true; + + // `--lsp` implies `--headless` to avoid spawning an unnecessary window + audio_driver = NULL_AUDIO_DRIVER; + display_driver = NULL_DISPLAY_DRIVER; #endif // TOOLS_ENABLED && MODULE_GDSCRIPT_ENABLED && !GDSCRIPT_NO_LSP #if defined(TOOLS_ENABLED) } else if (arg == "--dap-port") { diff --git a/modules/gdscript/language_server/gdscript_language_protocol.cpp b/modules/gdscript/language_server/gdscript_language_protocol.cpp index 0bf73a1ed62a..865ab215380a 100644 --- a/modules/gdscript/language_server/gdscript_language_protocol.cpp +++ b/modules/gdscript/language_server/gdscript_language_protocol.cpp @@ -31,6 +31,7 @@ #include "gdscript_language_protocol.h" #include "core/config/project_settings.h" +#include "core/io/stream_peer_stdio.h" #include "editor/doc/doc_tools.h" #include "editor/doc/editor_help.h" #include "editor/editor_log.h" @@ -49,10 +50,10 @@ Error GDScriptLanguageProtocol::LSPeer::handle_data() { ERR_FAIL_V_MSG(ERR_OUT_OF_MEMORY, "Response header too big"); } Error err = connection->get_partial_data(&req_buf[req_pos], 1, read); - if (err != OK) { + if (err == ERR_BUSY || read != 1) { + return ERR_BUSY; // Busy, wait until next poll + } else if (err != OK) { return FAILED; - } else if (read != 1) { // Busy, wait until next poll - return ERR_BUSY; } char *r = (char *)req_buf; int l = req_pos; @@ -77,10 +78,10 @@ Error GDScriptLanguageProtocol::LSPeer::handle_data() { ERR_FAIL_COND_V_MSG(req_pos >= LSP_MAX_BUFFER_SIZE, ERR_OUT_OF_MEMORY, "Response content too big"); } Error err = connection->get_partial_data(&req_buf[req_pos], 1, read); - if (err != OK) { + if (err == ERR_BUSY || read != 1) { + return ERR_BUSY; // Busy, wait until next poll + } else if (err != OK) { return FAILED; - } else if (read != 1) { - return ERR_BUSY; } req_pos++; } @@ -259,35 +260,38 @@ void GDScriptLanguageProtocol::poll(int p_limit_usec) { HashMap>::Iterator E = clients.begin(); while (E != clients.end()) { Ref peer = E->value; - peer->connection->poll(); - StreamPeerTCP::Status status = peer->connection->get_status(); - if (status == StreamPeerTCP::STATUS_NONE || status == StreamPeerTCP::STATUS_ERROR) { - on_client_disconnected(E->key); - E = clients.begin(); - continue; - } else { - Error err = OK; - while (peer->connection->get_available_bytes() > 0) { - latest_client_id = E->key; - err = peer->handle_data(); - if (err != OK || OS::get_singleton()->get_ticks_usec() >= target_ticks) { - break; - } - } - - if (err != OK && err != ERR_BUSY) { + Ref tcp_peer = peer->connection; + if (tcp_peer.is_valid()) { + tcp_peer->poll(); + StreamPeerTCP::Status status = tcp_peer->get_status(); + if (status == StreamPeerTCP::STATUS_NONE || status == StreamPeerTCP::STATUS_ERROR) { on_client_disconnected(E->key); E = clients.begin(); continue; } + } - err = peer->send_data(); - if (err != OK && err != ERR_BUSY) { - on_client_disconnected(E->key); - E = clients.begin(); - continue; + Error err = OK; + while (peer->connection->get_available_bytes() > 0) { + latest_client_id = E->key; + err = peer->handle_data(); + if (err != OK || OS::get_singleton()->get_ticks_usec() >= target_ticks) { + break; } } + + if (err != OK && err != ERR_BUSY) { + on_client_disconnected(E->key); + E = clients.begin(); + continue; + } + + err = peer->send_data(); + if (err != OK && err != ERR_BUSY) { + on_client_disconnected(E->key); + E = clients.begin(); + continue; + } ++E; } } @@ -296,10 +300,26 @@ Error GDScriptLanguageProtocol::start(int p_port, const IPAddress &p_bind_ip) { return server->listen(p_port, p_bind_ip); } +Error GDScriptLanguageProtocol::start_stdio() { + Ref peer = memnew(LSPeer); + Ref stdio_stream; + stdio_stream.instantiate(); + peer->connection = stdio_stream; + + clients.insert(next_client_id, peer); + next_client_id++; + + OS::get_singleton()->print("[LSP] Started in stdio mode\n"); + return OK; +} + void GDScriptLanguageProtocol::stop() { for (const KeyValue> &E : clients) { Ref peer = clients.get(E.key); - peer->connection->disconnect_from_host(); + Ref tcp_peer = peer->connection; + if (tcp_peer.is_valid()) { + tcp_peer->disconnect_from_host(); + } } server->stop(); diff --git a/modules/gdscript/language_server/gdscript_language_protocol.h b/modules/gdscript/language_server/gdscript_language_protocol.h index afdd849b8d9b..5a5f9ad8f0da 100644 --- a/modules/gdscript/language_server/gdscript_language_protocol.h +++ b/modules/gdscript/language_server/gdscript_language_protocol.h @@ -33,7 +33,7 @@ #include "gdscript_text_document.h" #include "gdscript_workspace.h" -#include "core/io/stream_peer_tcp.h" +#include "core/io/stream_peer.h" #include "core/io/tcp_server.h" #include "modules/jsonrpc/jsonrpc.h" @@ -46,12 +46,11 @@ class GDScriptLanguageProtocol : public JSONRPC { private: struct LSPeer : RefCounted { - Ref connection; + Ref connection; uint8_t req_buf[LSP_MAX_BUFFER_SIZE]; int req_pos = 0; bool has_header = false; - bool has_content = false; int content_length = 0; Vector res_queue; int res_sent = 0; @@ -99,6 +98,7 @@ class GDScriptLanguageProtocol : public JSONRPC { void poll(int p_limit_usec); Error start(int p_port, const IPAddress &p_bind_ip); + Error start_stdio(); void stop(); void notify_client(const String &p_method, const Variant &p_params = Variant(), int p_client_id = -1); diff --git a/modules/gdscript/language_server/gdscript_language_server.cpp b/modules/gdscript/language_server/gdscript_language_server.cpp index aeff4d4e5a71..2b1ebcdb01a8 100644 --- a/modules/gdscript/language_server/gdscript_language_server.cpp +++ b/modules/gdscript/language_server/gdscript_language_server.cpp @@ -36,6 +36,7 @@ #include "editor/settings/editor_settings.h" int GDScriptLanguageServer::port_override = -1; +bool GDScriptLanguageServer::use_stdio = false; GDScriptLanguageServer::GDScriptLanguageServer() { // TODO: Move to editor_settings.cpp @@ -94,12 +95,25 @@ void GDScriptLanguageServer::thread_main(void *p_userdata) { } void GDScriptLanguageServer::start() { - host = String(_EDITOR_GET("network/language_server/remote_host")); - port = (GDScriptLanguageServer::port_override > -1) ? GDScriptLanguageServer::port_override : (int)_EDITOR_GET("network/language_server/remote_port"); use_thread = (bool)_EDITOR_GET("network/language_server/use_thread"); poll_limit_usec = (int)_EDITOR_GET("network/language_server/poll_limit_usec"); - if (protocol.start(port, IPAddress(host)) == OK) { - EditorNode::get_log()->add_message("--- GDScript language server started on port " + itos(port) + " ---", EditorLog::MSG_TYPE_EDITOR); + + Error err; + if (GDScriptLanguageServer::use_stdio) { + err = protocol.start_stdio(); + if (err == OK) { + OS::get_singleton()->print("--- GDScript language server started in stdio mode ---\n"); + } + } else { + host = String(_EDITOR_GET("network/language_server/remote_host")); + port = (GDScriptLanguageServer::port_override > -1) ? GDScriptLanguageServer::port_override : (int)_EDITOR_GET("network/language_server/remote_port"); + err = protocol.start(port, IPAddress(host)); + if (err == OK) { + EditorNode::get_log()->add_message("--- GDScript language server started on port " + itos(port) + " ---", EditorLog::MSG_TYPE_EDITOR); + } + } + + if (err == OK) { if (use_thread) { thread_running = true; thread.start(GDScriptLanguageServer::thread_main, this); @@ -117,7 +131,11 @@ void GDScriptLanguageServer::stop() { } protocol.stop(); started = false; - EditorNode::get_log()->add_message("--- GDScript language server stopped ---", EditorLog::MSG_TYPE_EDITOR); + if (GDScriptLanguageServer::use_stdio) { + OS::get_singleton()->print("--- GDScript language server stopped ---\n"); + } else { + EditorNode::get_log()->add_message("--- GDScript language server stopped ---", EditorLog::MSG_TYPE_EDITOR); + } } void register_lsp_types() { diff --git a/modules/gdscript/language_server/gdscript_language_server.h b/modules/gdscript/language_server/gdscript_language_server.h index 12541ec58077..5c35b9504fbf 100644 --- a/modules/gdscript/language_server/gdscript_language_server.h +++ b/modules/gdscript/language_server/gdscript_language_server.h @@ -55,6 +55,7 @@ class GDScriptLanguageServer : public EditorPlugin { public: static int port_override; + static bool use_stdio; GDScriptLanguageServer(); void start(); void stop(); diff --git a/modules/gdscript/language_server/gdscript_workspace.cpp b/modules/gdscript/language_server/gdscript_workspace.cpp index 6d44b9dd38cc..8b7bb0d5e801 100644 --- a/modules/gdscript/language_server/gdscript_workspace.cpp +++ b/modules/gdscript/language_server/gdscript_workspace.cpp @@ -437,6 +437,7 @@ Error GDScriptWorkspace::initialize() { EditorNode *editor_node = EditorNode::get_singleton(); editor_node->connect("script_add_function_request", callable_mp(this, &GDScriptWorkspace::apply_new_signal)); + initialized = true; return OK; } diff --git a/tests/core/io/test_stream_peer_stdio.h b/tests/core/io/test_stream_peer_stdio.h new file mode 100644 index 000000000000..0bc46e837a24 --- /dev/null +++ b/tests/core/io/test_stream_peer_stdio.h @@ -0,0 +1,149 @@ +/**************************************************************************/ +/* test_stream_peer_stdio.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "core/io/stream_peer_stdio.h" +#include "tests/test_macros.h" + +#include +#include + +#ifdef WINDOWS_ENABLED +#include +#include +#define READ_FUNCTION _read +#define WRITE_FUNCTION _write +#define PIPE_FUNCTION _pipe +#define DUP_FUNCTION _dup +#define DUP2_FUNCTION _dup2 +#define CLOSE_FUNCTION _close +#else +#include +#define READ_FUNCTION read +#define WRITE_FUNCTION write +#define PIPE_FUNCTION pipe +#define DUP_FUNCTION dup +#define DUP2_FUNCTION dup2 +#define CLOSE_FUNCTION close +#endif + +namespace TestStreamPeerStdio { + +TEST_CASE("[StreamPeerStdio] Write and read through pipes") { + int stdin_pipe[2]; // stdin_pipe[0] = read, stdin_pipe[1] = write + int stdout_pipe[2]; // stdout_pipe[0] = read, stdout_pipe[1] = write + +#ifdef WINDOWS_ENABLED + int stdin_fileno = _fileno(stdin); + int stdout_fileno = _fileno(stdout); + + CHECK(_pipe(stdin_pipe, 4096, _O_BINARY) == 0); + CHECK(_pipe(stdout_pipe, 4096, _O_BINARY) == 0); + + // _setmode(stdin_fileno, _O_BINARY); + // _setmode(stdout_fileno, _O_BINARY); +#else + int stdin_fileno = STDIN_FILENO; + int stdout_fileno = STDOUT_FILENO; + + CHECK(pipe(stdin_pipe) == 0); + CHECK(pipe(stdout_pipe) == 0); + + // int flags = fcntl(stdin_fileno, F_GETFL, 0); + // fcntl(stdin_fileno, F_SETFL, flags | O_NONBLOCK); +#endif + + int original_stdin = DUP_FUNCTION(stdin_fileno); + int original_stdout = DUP_FUNCTION(stdout_fileno); + + // This will duplicate our "stdin" read pipe into stdin + DUP2_FUNCTION(stdin_pipe[0], stdin_fileno); + CLOSE_FUNCTION(stdin_pipe[0]); + + // This will duplicate our "stdout" write pipe into stdout + DUP2_FUNCTION(stdout_pipe[1], stdout_fileno); + CLOSE_FUNCTION(stdout_pipe[1]); + + // Create StreamPeerStdio (it will use the redirected stdin/stdout) + Ref stdio; + stdio.instantiate(); + + // Test 1: Write to stdin pipe, read using StreamPeerStdio + SUBCASE("Read from stdin using StreamPeerStdio") { + const char *test_input = "Hello from stdin!"; + int input_len = strlen(test_input); + + // Write directly to the stdin pipe + size_t written = WRITE_FUNCTION(stdin_pipe[1], test_input, input_len); + CHECK_EQ(written, input_len); + + // Read using StreamPeerStdio + uint8_t read_buffer[256]; + memset(read_buffer, 0, sizeof(read_buffer)); + int received = 0; + + Error read_err = stdio->get_partial_data(read_buffer, input_len, received); + CHECK(read_err == OK); + CHECK_EQ(received, input_len); + CHECK_EQ(memcmp(read_buffer, test_input, input_len), 0); + } + + // Test 2: Write using StreamPeerStdio, read from stdout pipe + SUBCASE("Write to stdout using StreamPeerStdio") { + const char *test_output = "Hello to stdout!"; + int output_len = strlen(test_output); + int sent = 0; + + // Write using StreamPeerStdio + Error write_err = stdio->put_partial_data((const uint8_t *)test_output, output_len, sent); + CHECK(write_err == OK); + CHECK_EQ(sent, output_len); + + // Read directly from the stdout pipe + uint8_t read_buffer[256]; + memset(read_buffer, 0, sizeof(read_buffer)); + size_t read_bytes = READ_FUNCTION(stdout_pipe[0], read_buffer, output_len); + CHECK_EQ(read_bytes, output_len); + CHECK_EQ(memcmp(read_buffer, test_output, output_len), 0); + } + + // Cleanup + CLOSE_FUNCTION(stdin_pipe[1]); + CLOSE_FUNCTION(stdout_pipe[0]); + + // Restore original stdin/stdout + DUP2_FUNCTION(original_stdin, stdin_fileno); + DUP2_FUNCTION(original_stdout, stdout_fileno); + CLOSE_FUNCTION(original_stdin); + CLOSE_FUNCTION(original_stdout); +} + +} // namespace TestStreamPeerStdio diff --git a/tests/test_main.cpp b/tests/test_main.cpp index 5951aa1c6eb7..778d739b9ae0 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -60,6 +60,7 @@ #include "tests/core/io/test_stream_peer.h" #include "tests/core/io/test_stream_peer_buffer.h" #include "tests/core/io/test_stream_peer_gzip.h" +#include "tests/core/io/test_stream_peer_stdio.h" #include "tests/core/io/test_stream_peer_tcp.h" #include "tests/core/io/test_tcp_server.h" #include "tests/core/io/test_udp_server.h"