--- id: 0071f title: Extraer `subprocess_streamer` a cpp/functions/core/ (sub-issue de 0071) status: pending priority: media created: 2026-05-10 parent: 0071 related_apps: [graph_explorer] --- ## Contexto Sub-issue derivado de 0071 tras auditar paneles C++. La regla "rule of three" se cumple HOY para una primitiva Tier 4: spawn de subprocesos con captura de stdout/stderr en stream, sin wrapper compartido. Tres reimplementaciones del mismo patron en `projects/osint_graph/apps/graph_explorer/`: | Sitio | Linea | Mecanismo | |---|---|---| | `chat.cpp` | 295 | `popen(...)` + `fgets` loop | | `chat.cpp` | 412 | `execvp(...)` con pipes manuales | | `jobs.cpp` | 817 | `execvp(...)` con pipes + read-thread | | `extract_panel.cpp` | 507 | `execvp(...)` para Python enricher | Cada una hace: setup pipes, fork/exec, read loop, parse JSON line-by-line, push a UI queue. Codigo duplicado ~60-80 LoC en cada sitio. ## Objetivo Funcion `subprocess_streamer` en `cpp/functions/core/` que encapsule: - Spawn (POSIX `fork+execvp`, Windows `CreateProcess`) - Pipes para stdin/stdout/stderr - Read-thread con callback por linea - Cancel/kill - Wait + exit code ## API propuesta ```cpp namespace fn_core { struct SubprocessConfig { std::vector argv; // [exe, arg1, arg2, ...] std::vector env; // ["KEY=VAL", ...]; vacio = heredar std::string cwd; // vacio = heredar bool merge_stderr_to_stdout = false; std::function on_stdout_line; std::function on_stderr_line; std::function on_exit; }; struct SubprocessHandle; // opaco // Lanza subproceso. on_* se llaman desde un read-thread interno. SubprocessHandle* subprocess_spawn(const SubprocessConfig& cfg); // Escribe en stdin. Thread-safe. Devuelve bytes escritos o -1. int subprocess_write_stdin(SubprocessHandle* h, const char* data, size_t n); // Cierra stdin (EOF al child). void subprocess_close_stdin(SubprocessHandle* h); // Mata el proceso (SIGTERM en POSIX, TerminateProcess en Win). void subprocess_kill(SubprocessHandle* h); // Espera fin. Devuelve exit_code. Si ya termino, retorna inmediato. int subprocess_wait(SubprocessHandle* h); // Libera recursos. Si el proceso sigue vivo, lo mata primero. void subprocess_destroy(SubprocessHandle* h); } // namespace fn_core ``` ## Tests - Spawn `echo hello` → on_stdout_line recibe "hello", exit 0. - Spawn `cat` con stdin "abc\n" → on_stdout_line recibe "abc", exit 0 tras close_stdin. - Spawn `false` → exit 1. - Spawn comando inexistente → handle nullable o exit code distintivo. - Kill proceso vivo → wait retorna codigo de señal. `cpp/apps/primitives_gallery/` añade demo "Subprocess Streamer" con boton "spawn echo hello" + lista de lineas recibidas. ## Migracion de consumidores (orden) 1. **Crear** `cpp/functions/core/subprocess_streamer.{h,cpp}` + `.md` + tests. 2. **Migrar `chat.cpp`** primero (mas critico, define el flujo de error). Si funciona en chat → patron validado. 3. **Migrar `jobs.cpp`**. 4. **Migrar `extract_panel.cpp`**. Cada migracion en su propio commit, validando por inspeccion manual que el panel sigue funcionando. ## Definicion de hecho - `subprocess_streamer.{h,cpp,md}` registrado en `registry.db` (`fn index`). - Tests pasan (al menos los 5 listados). - 3 consumidores migrados (chat, jobs, extract_panel). - `app.md` de graph_explorer actualizado: `uses_functions` añade `subprocess_streamer_cpp_core`. - LoC eliminadas en graph_explorer: ~150-200 (3 reimplementaciones colapsadas a 1 import). ## Anti-patrones a evitar - Async/promesa con `std::future` — over-engineering. Callbacks bastan. - Buffering "smart" line-aware sobre bytes raw — usar `std::getline` simple. - Soporte ptys (terminal real) — fuera de scope. Si una app necesita pty, abre issue propio. - Prematura abstraccion de "shell vs exec". Solo `argv` directo (execvp). Si alguien quiere shell pipes, lo hace en `bash -c "..."`.