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:
+1
-1
@@ -21,7 +21,7 @@ Cualquier decision tecnica que choque con estos objetivos esta mal priorizada. E
|
|||||||
|
|
||||||
**Sync entre PCs:** `fn sync` sincroniza datos no regenerables (proposals, apps, projects, analysis, vaults, pc_locations) contra `registry_api` en `https://registry.organic-machine.com`. Config: `~/.fn_pc` (identidad del PC), `FN_REGISTRY_API` (URL con basicAuth), `REGISTRY_API_TOKEN` (token).
|
**Sync entre PCs:** `fn sync` sincroniza datos no regenerables (proposals, apps, projects, analysis, vaults, pc_locations) contra `registry_api` en `https://registry.organic-machine.com`. Config: `~/.fn_pc` (identidad del PC), `FN_REGISTRY_API` (URL con basicAuth), `REGISTRY_API_TOKEN` (token).
|
||||||
|
|
||||||
**Sub-repos:** cada app y cada analysis es su propio repo Gitea en `dataforge/<basename>` con branch `master` (ver ADR 0002). Los slash commands `/full-git-push` y `/full-git-pull` orquestan push/pull/clone de fn_registry + todos los sub-repos + `fn sync`. `/full-git-push` auto-inicializa apps/analyses sin `.git` via `ensure_repo_synced_bash_infra`. Los `vaults/` y `subrepos/` NO entran en este flujo.
|
**Sub-repos:** cada app y cada analysis es su propio repo Gitea en `dataforge/<basename>` con branch `master` (ver ADR 0002). `apps/*` y `analysis/*` estan en el `.gitignore` del repo padre — el codigo de cada app vive en `apps/<name>/.git/`. Los slash commands `/full-git-push` y `/full-git-pull` orquestan push/pull/clone de fn_registry + todos los sub-repos + `fn sync`. `/full-git-push` auto-inicializa apps/analyses sin `.git` via `ensure_repo_synced_bash_infra`. Los `vaults/` y `subrepos/` NO entran en este flujo. **Gotcha worktrees**: si creas una app nueva dentro de un git worktree del repo padre, haz `git init` dentro de `apps/<name>/` ANTES de limpiar el worktree, sino el codigo se pierde (apps/* gitignored). Ver `.claude/rules/apps_subrepo.md`.
|
||||||
|
|
||||||
**Artefactos:** termino paraguas para apps, analysis, vaults, projects y playgrounds — todo lo que NO es codigo reutilizable. Usa "artefacto" cuando una afirmacion aplica a varios tipos a la vez para no repetir la lista. Ver `.claude/rules/artefactos.md` y `.claude/rules/playgrounds.md`.
|
**Artefactos:** termino paraguas para apps, analysis, vaults, projects y playgrounds — todo lo que NO es codigo reutilizable. Usa "artefacto" cuando una afirmacion aplica a varios tipos a la vez para no repetir la lista. Ver `.claude/rules/artefactos.md` y `.claude/rules/playgrounds.md`.
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
|
|||||||
| 15 | [projects.md](projects.md) | Projects: agrupar apps, analysis y vaults bajo un tema |
|
| 15 | [projects.md](projects.md) | Projects: agrupar apps, analysis y vaults bajo un tema |
|
||||||
| 16 | [kiss.md](kiss.md) | KISS en proyectos y apps: cuestionar herramientas externas, sin abstracciones especulativas |
|
| 16 | [kiss.md](kiss.md) | KISS en proyectos y apps: cuestionar herramientas externas, sin abstracciones especulativas |
|
||||||
| 17 | [apps_tbd.md](apps_tbd.md) | Trunk-based development obligatorio en apps generadas con `fn` (registry exento) |
|
| 17 | [apps_tbd.md](apps_tbd.md) | Trunk-based development obligatorio en apps generadas con `fn` (registry exento) |
|
||||||
|
| 17b | [apps_subrepo.md](apps_subrepo.md) | Apps son sub-repos Gitea (apps/* gitignored). `git init` dentro de cada app nueva ANTES de limpiar worktree, sino se pierde el codigo |
|
||||||
| 18 | [uses_functions.md](uses_functions.md) | Convencion de uses_functions para C++: el .md del consumidor declara las dependencias |
|
| 18 | [uses_functions.md](uses_functions.md) | Convencion de uses_functions para C++: el .md del consumidor declara las dependencias |
|
||||||
| 19 | [cpp_apps.md](cpp_apps.md) | Estandarizacion de apps C++: estructura, CMake, app.md, sub-repo, runtime — apunta a cpp/PATTERNS.md y cpp/DESIGN_SYSTEM.md como autoritativas |
|
| 19 | [cpp_apps.md](cpp_apps.md) | Estandarizacion de apps C++: estructura, CMake, app.md, sub-repo, runtime — apunta a cpp/PATTERNS.md y cpp/DESIGN_SYSTEM.md como autoritativas |
|
||||||
| 20 | [artefactos.md](artefactos.md) | Termino paraguas para apps, analysis, vaults, projects y playgrounds (todo lo que no es codigo reutilizable) |
|
| 20 | [artefactos.md](artefactos.md) | Termino paraguas para apps, analysis, vaults, projects y playgrounds (todo lo que no es codigo reutilizable) |
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
## Apps son sub-repos Gitea independientes — gotcha al usar worktrees
|
||||||
|
|
||||||
|
**Regla operativa critica** descubierta el 2026-05-18 durante implementacion del flow 0008.
|
||||||
|
|
||||||
|
### El gotcha
|
||||||
|
|
||||||
|
`apps/*/` esta en `.gitignore` del repo `fn_registry`. Cada app es **su propio repo Gitea** en `dataforge/<app_name>` con su `.git/` dentro de `apps/<app_name>/`. Esto significa:
|
||||||
|
|
||||||
|
- Cuando un agente trabaja en un git **worktree** del repo padre y crea `apps/<nueva_app>/`, los archivos viven SOLO en el working directory del worktree.
|
||||||
|
- Como `apps/*/` esta gitignored en el repo padre, los archivos **no se pueden commitear** al worktree del repo padre.
|
||||||
|
- Cuando se hace `git worktree remove --force worktrees/<slug>/`, el working directory entero se borra — **el codigo de la app desaparece**.
|
||||||
|
|
||||||
|
**Consecuencia**: una app creada dentro de un worktree del repo padre se pierde al limpiar el worktree salvo que se haya promovido a su propio sub-repo Gitea ANTES.
|
||||||
|
|
||||||
|
### El patron correcto al crear apps en worktrees
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Agente trabaja en worktree del repo padre
|
||||||
|
cd /home/lucas/fn_registry/worktrees/<slug>
|
||||||
|
|
||||||
|
# 2. Scaffold la app via pipeline canonico
|
||||||
|
./fn run init_cpp_app <name> # apps C++
|
||||||
|
# o ./fn run init_jupyter_analysis ... # analysis
|
||||||
|
# o crear apps/<name>/ a mano (Go service, etc.)
|
||||||
|
|
||||||
|
# 3. ANTES de salir del worktree: inicializa la app como sub-repo
|
||||||
|
cd apps/<name>
|
||||||
|
git init -b master
|
||||||
|
git add -A
|
||||||
|
git -c user.email="agent@fn_registry" -c user.name="agent" \
|
||||||
|
commit -m "feat: initial scaffold of <name>"
|
||||||
|
|
||||||
|
# 4. Trabajo continua en sub-repo (commits dentro de apps/<name>/.git)
|
||||||
|
# 5. Cerrar issue en repo padre (mv .md a completed/), commit del padre con cambios en cpp/CMakeLists.txt, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
Cuando el humano corre `/full-git-push` despues del merge, el script `ensure_repo_synced_bash_infra` detecta que `apps/<name>/.git` existe + no tiene remote + crea repo Gitea en `dataforge/<name>` + pushea master.
|
||||||
|
|
||||||
|
### Que ESTA SI versionado en el repo padre
|
||||||
|
|
||||||
|
- `cpp/CMakeLists.txt` (el `if(EXISTS ...) add_subdirectory(apps/<name>) endif()`).
|
||||||
|
- `dev/issues/completed/<NNNN>-<slug>.md` (cierre del issue).
|
||||||
|
- `docs/capabilities/*.md` si la app aporta a un capability group.
|
||||||
|
- `dev/feature_flags.json` si introduce flags.
|
||||||
|
|
||||||
|
Todo lo demas (codigo de la app + app.md + appicon + service unit + tests propios de la app) vive en `apps/<name>/.git` independiente.
|
||||||
|
|
||||||
|
### Sintomas de la perdida
|
||||||
|
|
||||||
|
Si limpias el worktree y luego corres `ls apps/<name>/`, devuelve "No such file or directory" pese a que el issue aparece cerrado en `dev/issues/completed/`. **Patron** = scaffold sin sub-repo init = trabajo perdido.
|
||||||
|
|
||||||
|
### Recovery si pasa
|
||||||
|
|
||||||
|
1. Re-crear worktree desde master.
|
||||||
|
2. Re-spawn agente con instruccion explicita: **`git init` dentro de la app antes de terminar**.
|
||||||
|
3. NO eliminar el worktree hasta confirmar que `apps/<name>/.git` esta inicializado con al menos un commit.
|
||||||
|
|
||||||
|
### Aplica tambien a analysis
|
||||||
|
|
||||||
|
`analysis/*/` y `projects/*/analysis/*/` siguen mismo patron (cada analysis es repo Gitea). El pipeline `init_jupyter_analysis_bash_pipelines` ya hace `git init` automatico — por eso no hubo perdidas alli. Las apps C++/Go scaffolded a mano NO inicializan el sub-repo automaticamente — es responsabilidad del agente.
|
||||||
|
|
||||||
|
### Lo que aprende `parallel-fix-issues`
|
||||||
|
|
||||||
|
El template del prompt de cada agente DEBE incluir la instruccion:
|
||||||
|
|
||||||
|
> "Si tu issue crea una app nueva en `apps/<name>/`, inicializa el sub-repo (`cd apps/<name> && git init -b master && git add -A && git commit ...`) antes de terminar. Sin esto, `apps/*` esta gitignored y el codigo se perdera cuando el orquestador limpie el worktree."
|
||||||
|
|
||||||
|
Aplicar este parrafo al template del skill — ver `.claude/skills/parallel-fix-issues/SKILL.md` (o equivalente).
|
||||||
|
|
||||||
|
### Relacion con otras reglas
|
||||||
|
|
||||||
|
- [[apps_tbd]] — TBD en apps, esta regla complementa con el patron de sub-repo init.
|
||||||
|
- [[artefactos]] — apps son artefactos, esta regla especifica gotcha de su sub-repo.
|
||||||
|
- [[apps_vs_functions]] — apps en `apps/`, esta regla refuerza por que apps/* gitignored.
|
||||||
@@ -19,16 +19,14 @@ if [ $# -eq 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Asegurar que master está actualizado
|
# Verificar master (NO pull --rebase: rompe merges locales convirtiendolos
|
||||||
echo "=== Actualizando master ==="
|
# en cherry-picks contra origin/master viejo). Detectado 2026-05-18.
|
||||||
|
echo "=== Verificando master ==="
|
||||||
CURRENT_BRANCH="$(git branch --show-current)"
|
CURRENT_BRANCH="$(git branch --show-current)"
|
||||||
git checkout master 2>/dev/null
|
|
||||||
git pull --rebase 2>/dev/null || echo "WARN: no se pudo pull (sin remote o sin conexión)"
|
|
||||||
|
|
||||||
# Volver a la rama original si no era master
|
|
||||||
if [ "$CURRENT_BRANCH" != "master" ] && [ -n "$CURRENT_BRANCH" ]; then
|
if [ "$CURRENT_BRANCH" != "master" ] && [ -n "$CURRENT_BRANCH" ]; then
|
||||||
git checkout "$CURRENT_BRANCH" 2>/dev/null
|
echo "WARN: estas en branch '${CURRENT_BRANCH}', no master. Worktrees nuevos saldran de master ref de todos modos."
|
||||||
fi
|
fi
|
||||||
|
# NO auto-pull. Usuario decide sync con remote.
|
||||||
|
|
||||||
mkdir -p "$WORKTREE_DIR"
|
mkdir -p "$WORKTREE_DIR"
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,36 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#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 fn_http {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Shell-escape single argument for a POSIX-ish shell. Wraps in single quotes
|
// Shell-escape single argument. POSIX uses single quotes; Windows uses
|
||||||
// and escapes embedded single quotes via '\''. Used for URL + header values.
|
// 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 sh_q(const std::string& s) {
|
||||||
std::string o;
|
std::string o;
|
||||||
o.reserve(s.size() + 2);
|
o.reserve(s.size() + 2);
|
||||||
@@ -30,6 +54,87 @@ std::string sh_q(const std::string& s) {
|
|||||||
o += '\'';
|
o += '\'';
|
||||||
return 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.
|
// Read entire file into string. Empty on missing.
|
||||||
std::string slurp(const std::string& path) {
|
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;
|
std::string method = req.method.empty() ? std::string("GET") : req.method;
|
||||||
|
|
||||||
// Tmp files: request body + response body + response headers.
|
// Tmp files: request body + response body + response headers.
|
||||||
std::string tmp_body_in = std::tmpnam(nullptr);
|
std::string tmp_body_in = make_tmp_path("body_in");
|
||||||
std::string tmp_body_out = std::tmpnam(nullptr);
|
std::string tmp_body_out = make_tmp_path("body_out");
|
||||||
std::string tmp_hdr_out = std::tmpnam(nullptr);
|
std::string tmp_hdr_out = make_tmp_path("hdr");
|
||||||
|
|
||||||
bool have_body = !req.body.empty();
|
bool have_body = !req.body.empty();
|
||||||
if (have_body) {
|
if (have_body) {
|
||||||
@@ -168,13 +273,19 @@ Response request(const Request& req) {
|
|||||||
<< " 2>&1";
|
<< " 2>&1";
|
||||||
|
|
||||||
// Capture stderr (curl prints transport errors to stderr with -sS).
|
// Capture stderr (curl prints transport errors to stderr with -sS).
|
||||||
FILE* p = popen(cmd.str().c_str(), "r");
|
|
||||||
std::string curl_stderr;
|
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) {
|
if (p) {
|
||||||
char buf[1024];
|
char buf[1024];
|
||||||
while (fgets(buf, sizeof(buf), p)) curl_stderr.append(buf);
|
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.
|
// Read response files.
|
||||||
r.body = slurp(tmp_body_out);
|
r.body = slurp(tmp_body_out);
|
||||||
|
|||||||
@@ -25,6 +25,13 @@
|
|||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#else
|
||||||
|
# ifndef NOMINMAX
|
||||||
|
# define NOMINMAX
|
||||||
|
# endif
|
||||||
|
# include <windows.h>
|
||||||
|
# include <io.h>
|
||||||
|
# include <fcntl.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace fn_sse {
|
namespace fn_sse {
|
||||||
@@ -142,6 +149,12 @@ struct Client::Impl {
|
|||||||
|
|
||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
std::atomic<pid_t> curl_pid_{0};
|
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
|
#endif
|
||||||
|
|
||||||
void run(Config cfg, EventHandler on_event, StatusHandler on_status) {
|
void run(Config cfg, EventHandler on_event, StatusHandler on_status) {
|
||||||
@@ -234,20 +247,86 @@ struct Client::Impl {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// Windows: popen fallback (no reliable PID, stop() may be slow).
|
// Windows: CreateProcessW with CREATE_NO_WINDOW so no console
|
||||||
std::ostringstream cmd;
|
// pop-up per call. We keep a HANDLE to the child for stop().
|
||||||
cmd << "curl -N -sS --max-time 0 --connect-timeout "
|
std::ostringstream cmd_n;
|
||||||
<< (cfg.connect_timeout_ms / 1000 + 1)
|
cmd_n << "curl.exe -N -sS --max-time 0 --connect-timeout "
|
||||||
<< " -H \"Accept: text/event-stream\""
|
<< (cfg.connect_timeout_ms / 1000 + 1)
|
||||||
<< " -H \"Cache-Control: no-cache\"";
|
<< " -H \"Accept: text/event-stream\""
|
||||||
|
<< " -H \"Cache-Control: no-cache\"";
|
||||||
if (!cfg.bearer_token.empty())
|
if (!cfg.bearer_token.empty())
|
||||||
cmd << " -H \"Authorization: Bearer " + cfg.bearer_token + "\"";
|
cmd_n << " -H \"Authorization: Bearer " << cfg.bearer_token << "\"";
|
||||||
if (!last_id.empty())
|
if (!last_id.empty())
|
||||||
cmd << " -H \"Last-Event-ID: " + last_id + "\"";
|
cmd_n << " -H \"Last-Event-ID: " << last_id << "\"";
|
||||||
cmd << " \"" << cfg.url << "\" 2>NUL";
|
cmd_n << " \"" << cfg.url << "\"";
|
||||||
FILE* pipe_file = ::popen(cmd.str().c_str(), "r");
|
|
||||||
|
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) {
|
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;
|
if (!cfg.auto_reconnect || stop_requested_) break;
|
||||||
sleep_ms(backoff_ms);
|
sleep_ms(backoff_ms);
|
||||||
backoff_ms = std::min(backoff_ms * 2, cfg.reconnect_max_ms);
|
backoff_ms = std::min(backoff_ms * 2, cfg.reconnect_max_ms);
|
||||||
@@ -305,7 +384,15 @@ struct Client::Impl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#else
|
#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
|
#endif
|
||||||
|
|
||||||
if (stop_requested_) break;
|
if (stop_requested_) break;
|
||||||
@@ -348,6 +435,11 @@ void Client::stop() {
|
|||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
pid_t p = impl_->curl_pid_.exchange(0);
|
pid_t p = impl_->curl_pid_.exchange(0);
|
||||||
if (p > 0) ::kill(p, SIGTERM);
|
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
|
#endif
|
||||||
if (impl_->thread_.joinable())
|
if (impl_->thread_.joinable())
|
||||||
impl_->thread_.join();
|
impl_->thread_.join();
|
||||||
|
|||||||
Reference in New Issue
Block a user