chore: baseline pre-piloto 0120 — apps_subrepo rule + http/sse hardening
WIP previo al lanzamiento de fn-orquestador piloto. Commit como baseline para que /autonomous-task 0120 arranque con master limpio. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,12 +13,36 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
# ifndef NOMINMAX
|
||||
# define NOMINMAX
|
||||
# endif
|
||||
# include <windows.h>
|
||||
# include <io.h>
|
||||
# include <fcntl.h>
|
||||
#else
|
||||
# include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace fn_http {
|
||||
|
||||
namespace {
|
||||
|
||||
// Shell-escape single argument for a POSIX-ish shell. Wraps in single quotes
|
||||
// and escapes embedded single quotes via '\''. Used for URL + header values.
|
||||
// Shell-escape single argument. POSIX uses single quotes; Windows uses
|
||||
// double quotes since cmd.exe doesn't interpret single quotes.
|
||||
#ifdef _WIN32
|
||||
std::string sh_q(const std::string& s) {
|
||||
std::string o;
|
||||
o.reserve(s.size() + 2);
|
||||
o += '"';
|
||||
for (char c : s) {
|
||||
if (c == '"' || c == '\\') o += '\\';
|
||||
o += c;
|
||||
}
|
||||
o += '"';
|
||||
return o;
|
||||
}
|
||||
#else
|
||||
std::string sh_q(const std::string& s) {
|
||||
std::string o;
|
||||
o.reserve(s.size() + 2);
|
||||
@@ -30,6 +54,87 @@ std::string sh_q(const std::string& s) {
|
||||
o += '\'';
|
||||
return o;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Make a cross-platform temp file path. On Windows uses GetTempPathW +
|
||||
// GetTempFileNameW (creates a 0-byte file in %TEMP%). On POSIX uses mkstemp.
|
||||
std::string make_tmp_path(const char* prefix) {
|
||||
#ifdef _WIN32
|
||||
wchar_t dir_w[MAX_PATH];
|
||||
DWORD n = GetTempPathW(MAX_PATH, dir_w);
|
||||
if (n == 0 || n > MAX_PATH) return std::string(prefix) + "_fallback.tmp";
|
||||
|
||||
wchar_t path_w[MAX_PATH];
|
||||
UINT u = GetTempFileNameW(dir_w, L"fn_", 0, path_w);
|
||||
if (u == 0) return std::string(prefix) + "_fallback.tmp";
|
||||
|
||||
int need = WideCharToMultiByte(CP_UTF8, 0, path_w, -1,
|
||||
nullptr, 0, nullptr, nullptr);
|
||||
if (need <= 0) return std::string(prefix) + "_fallback.tmp";
|
||||
std::string out(need - 1, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, path_w, -1, out.data(), need,
|
||||
nullptr, nullptr);
|
||||
return out;
|
||||
#else
|
||||
(void)prefix;
|
||||
char tmpl[] = "/tmp/fn_XXXXXX";
|
||||
int fd = mkstemp(tmpl);
|
||||
if (fd >= 0) ::close(fd);
|
||||
return std::string(tmpl);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// Run a command line without spawning a visible console window. Captures
|
||||
// stdout+stderr combined into out_combined. Returns the child's exit code,
|
||||
// or -1 on spawn failure.
|
||||
int run_no_console(const std::string& cmdline_utf8, std::string& out_combined) {
|
||||
int need = MultiByteToWideChar(CP_UTF8, 0, cmdline_utf8.c_str(), -1,
|
||||
nullptr, 0);
|
||||
if (need <= 0) return -1;
|
||||
std::wstring wcmd(need, L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, cmdline_utf8.c_str(), -1,
|
||||
wcmd.data(), need);
|
||||
|
||||
SECURITY_ATTRIBUTES sa{};
|
||||
sa.nLength = sizeof(sa);
|
||||
sa.bInheritHandle = TRUE;
|
||||
|
||||
HANDLE rd = nullptr, wr = nullptr;
|
||||
if (!CreatePipe(&rd, &wr, &sa, 0)) return -1;
|
||||
SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
|
||||
|
||||
STARTUPINFOW si{};
|
||||
si.cb = sizeof(si);
|
||||
si.dwFlags = STARTF_USESTDHANDLES;
|
||||
si.hStdOutput = wr;
|
||||
si.hStdError = wr;
|
||||
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
||||
|
||||
PROCESS_INFORMATION pi{};
|
||||
BOOL ok = CreateProcessW(nullptr, wcmd.data(), nullptr, nullptr,
|
||||
TRUE, CREATE_NO_WINDOW, nullptr, nullptr,
|
||||
&si, &pi);
|
||||
CloseHandle(wr);
|
||||
if (!ok) {
|
||||
CloseHandle(rd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char buf[4096];
|
||||
DWORD got = 0;
|
||||
while (ReadFile(rd, buf, sizeof(buf), &got, nullptr) && got > 0)
|
||||
out_combined.append(buf, buf + got);
|
||||
CloseHandle(rd);
|
||||
|
||||
WaitForSingleObject(pi.hProcess, INFINITE);
|
||||
DWORD ec = 0;
|
||||
GetExitCodeProcess(pi.hProcess, &ec);
|
||||
CloseHandle(pi.hProcess);
|
||||
CloseHandle(pi.hThread);
|
||||
return (int)ec;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Read entire file into string. Empty on missing.
|
||||
std::string slurp(const std::string& path) {
|
||||
@@ -115,9 +220,9 @@ Response request(const Request& req) {
|
||||
std::string method = req.method.empty() ? std::string("GET") : req.method;
|
||||
|
||||
// Tmp files: request body + response body + response headers.
|
||||
std::string tmp_body_in = std::tmpnam(nullptr);
|
||||
std::string tmp_body_out = std::tmpnam(nullptr);
|
||||
std::string tmp_hdr_out = std::tmpnam(nullptr);
|
||||
std::string tmp_body_in = make_tmp_path("body_in");
|
||||
std::string tmp_body_out = make_tmp_path("body_out");
|
||||
std::string tmp_hdr_out = make_tmp_path("hdr");
|
||||
|
||||
bool have_body = !req.body.empty();
|
||||
if (have_body) {
|
||||
@@ -168,13 +273,19 @@ Response request(const Request& req) {
|
||||
<< " 2>&1";
|
||||
|
||||
// Capture stderr (curl prints transport errors to stderr with -sS).
|
||||
FILE* p = popen(cmd.str().c_str(), "r");
|
||||
std::string curl_stderr;
|
||||
int rc;
|
||||
#ifdef _WIN32
|
||||
// Use CreateProcessW with CREATE_NO_WINDOW: no console pop-up per call.
|
||||
rc = run_no_console(cmd.str(), curl_stderr);
|
||||
#else
|
||||
FILE* p = popen(cmd.str().c_str(), "r");
|
||||
if (p) {
|
||||
char buf[1024];
|
||||
while (fgets(buf, sizeof(buf), p)) curl_stderr.append(buf);
|
||||
}
|
||||
int rc = p ? pclose(p) : -1;
|
||||
rc = p ? pclose(p) : -1;
|
||||
#endif
|
||||
|
||||
// Read response files.
|
||||
r.body = slurp(tmp_body_out);
|
||||
|
||||
@@ -25,6 +25,13 @@
|
||||
#include <fcntl.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#else
|
||||
# ifndef NOMINMAX
|
||||
# define NOMINMAX
|
||||
# endif
|
||||
# include <windows.h>
|
||||
# include <io.h>
|
||||
# include <fcntl.h>
|
||||
#endif
|
||||
|
||||
namespace fn_sse {
|
||||
@@ -142,6 +149,12 @@ struct Client::Impl {
|
||||
|
||||
#ifndef _WIN32
|
||||
std::atomic<pid_t> curl_pid_{0};
|
||||
#else
|
||||
// HANDLEs to the curl child process (and its primary thread) so stop()
|
||||
// can TerminateProcess and the cleanup block can close them. void* to
|
||||
// avoid leaking <windows.h> into the header struct via friend lookup.
|
||||
void* curl_proc_handle_ = nullptr; // HANDLE
|
||||
void* curl_proc_thread_ = nullptr; // HANDLE
|
||||
#endif
|
||||
|
||||
void run(Config cfg, EventHandler on_event, StatusHandler on_status) {
|
||||
@@ -234,20 +247,86 @@ struct Client::Impl {
|
||||
continue;
|
||||
}
|
||||
#else
|
||||
// Windows: popen fallback (no reliable PID, stop() may be slow).
|
||||
std::ostringstream cmd;
|
||||
cmd << "curl -N -sS --max-time 0 --connect-timeout "
|
||||
<< (cfg.connect_timeout_ms / 1000 + 1)
|
||||
<< " -H \"Accept: text/event-stream\""
|
||||
<< " -H \"Cache-Control: no-cache\"";
|
||||
// Windows: CreateProcessW with CREATE_NO_WINDOW so no console
|
||||
// pop-up per call. We keep a HANDLE to the child for stop().
|
||||
std::ostringstream cmd_n;
|
||||
cmd_n << "curl.exe -N -sS --max-time 0 --connect-timeout "
|
||||
<< (cfg.connect_timeout_ms / 1000 + 1)
|
||||
<< " -H \"Accept: text/event-stream\""
|
||||
<< " -H \"Cache-Control: no-cache\"";
|
||||
if (!cfg.bearer_token.empty())
|
||||
cmd << " -H \"Authorization: Bearer " + cfg.bearer_token + "\"";
|
||||
cmd_n << " -H \"Authorization: Bearer " << cfg.bearer_token << "\"";
|
||||
if (!last_id.empty())
|
||||
cmd << " -H \"Last-Event-ID: " + last_id + "\"";
|
||||
cmd << " \"" << cfg.url << "\" 2>NUL";
|
||||
FILE* pipe_file = ::popen(cmd.str().c_str(), "r");
|
||||
cmd_n << " -H \"Last-Event-ID: " << last_id << "\"";
|
||||
cmd_n << " \"" << cfg.url << "\"";
|
||||
|
||||
std::string cmdu = cmd_n.str();
|
||||
int needw = MultiByteToWideChar(CP_UTF8, 0, cmdu.c_str(),
|
||||
-1, nullptr, 0);
|
||||
if (needw <= 0) {
|
||||
emit_status("error: MultiByteToWideChar failed");
|
||||
if (!cfg.auto_reconnect || stop_requested_) break;
|
||||
sleep_ms(backoff_ms);
|
||||
backoff_ms = std::min(backoff_ms * 2, cfg.reconnect_max_ms);
|
||||
continue;
|
||||
}
|
||||
std::wstring wcmd(needw, L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, cmdu.c_str(), -1,
|
||||
wcmd.data(), needw);
|
||||
|
||||
SECURITY_ATTRIBUTES sa{};
|
||||
sa.nLength = sizeof(sa);
|
||||
sa.bInheritHandle = TRUE;
|
||||
|
||||
HANDLE rd = nullptr, wr = nullptr;
|
||||
if (!CreatePipe(&rd, &wr, &sa, 0)) {
|
||||
emit_status("error: CreatePipe failed");
|
||||
if (!cfg.auto_reconnect || stop_requested_) break;
|
||||
sleep_ms(backoff_ms);
|
||||
backoff_ms = std::min(backoff_ms * 2, cfg.reconnect_max_ms);
|
||||
continue;
|
||||
}
|
||||
SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
|
||||
|
||||
STARTUPINFOW si{};
|
||||
si.cb = sizeof(si);
|
||||
si.dwFlags = STARTF_USESTDHANDLES;
|
||||
si.hStdOutput = wr;
|
||||
// Discard stderr to /dev/null equivalent: route to our own
|
||||
// STDERR (parent process) which on Windows GUI apps is detached.
|
||||
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
|
||||
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
||||
|
||||
PROCESS_INFORMATION pi{};
|
||||
BOOL ok = CreateProcessW(nullptr, wcmd.data(), nullptr, nullptr,
|
||||
TRUE, CREATE_NO_WINDOW, nullptr, nullptr,
|
||||
&si, &pi);
|
||||
CloseHandle(wr);
|
||||
if (!ok) {
|
||||
CloseHandle(rd);
|
||||
emit_status("error: CreateProcessW failed");
|
||||
if (!cfg.auto_reconnect || stop_requested_) break;
|
||||
sleep_ms(backoff_ms);
|
||||
backoff_ms = std::min(backoff_ms * 2, cfg.reconnect_max_ms);
|
||||
continue;
|
||||
}
|
||||
|
||||
curl_proc_handle_ = pi.hProcess;
|
||||
curl_proc_thread_ = pi.hThread;
|
||||
|
||||
// Wrap read-end HANDLE as a FILE* so the existing fgets-based
|
||||
// SSE parser loop keeps working unchanged.
|
||||
int fd = _open_osfhandle((intptr_t)rd, _O_RDONLY);
|
||||
FILE* pipe_file = (fd >= 0) ? _fdopen(fd, "r") : nullptr;
|
||||
if (!pipe_file) {
|
||||
emit_status("error: popen() failed");
|
||||
if (fd >= 0) _close(fd);
|
||||
else CloseHandle(rd);
|
||||
TerminateProcess(pi.hProcess, 1);
|
||||
CloseHandle(pi.hProcess);
|
||||
CloseHandle(pi.hThread);
|
||||
curl_proc_handle_ = nullptr;
|
||||
curl_proc_thread_ = nullptr;
|
||||
emit_status("error: _fdopen failed");
|
||||
if (!cfg.auto_reconnect || stop_requested_) break;
|
||||
sleep_ms(backoff_ms);
|
||||
backoff_ms = std::min(backoff_ms * 2, cfg.reconnect_max_ms);
|
||||
@@ -305,7 +384,15 @@ struct Client::Impl {
|
||||
}
|
||||
}
|
||||
#else
|
||||
::pclose(pipe_file);
|
||||
if (curl_proc_handle_) {
|
||||
TerminateProcess((HANDLE)curl_proc_handle_, 0);
|
||||
WaitForSingleObject((HANDLE)curl_proc_handle_, 1000);
|
||||
CloseHandle((HANDLE)curl_proc_handle_);
|
||||
CloseHandle((HANDLE)curl_proc_thread_);
|
||||
curl_proc_handle_ = nullptr;
|
||||
curl_proc_thread_ = nullptr;
|
||||
}
|
||||
fclose(pipe_file); // also closes the underlying HANDLE via fd
|
||||
#endif
|
||||
|
||||
if (stop_requested_) break;
|
||||
@@ -348,6 +435,11 @@ void Client::stop() {
|
||||
#ifndef _WIN32
|
||||
pid_t p = impl_->curl_pid_.exchange(0);
|
||||
if (p > 0) ::kill(p, SIGTERM);
|
||||
#else
|
||||
// TerminateProcess so the blocking ReadFile/fgets returns and the
|
||||
// worker thread can exit promptly. The cleanup block closes handles.
|
||||
if (impl_->curl_proc_handle_)
|
||||
TerminateProcess((HANDLE)impl_->curl_proc_handle_, 0);
|
||||
#endif
|
||||
if (impl_->thread_.joinable())
|
||||
impl_->thread_.join();
|
||||
|
||||
Reference in New Issue
Block a user