common: add subproc.h wrapper, disabled on android/ios (#26102)

* add common/subproc.h|cpp

* add compile flag LLAMA_SUBPROCESS

* disabled by default on android and ios

* test-jinja: use common subproc

* mtmd: disable video if subproc is not set

* disable subproc on wasm

* make is_created atomic

* migrate server-mcp
This commit is contained in:
Xuan-Son Nguyen
2026-07-26 20:54:25 +02:00
committed by GitHub
parent af285020e9
commit d2a818231e
9 changed files with 272 additions and 109 deletions
+9 -25
View File
@@ -1,6 +1,6 @@
#include "server-mcp.h"
#include <sheredom/subprocess.h>
#include "subproc.h"
#include <atomic>
#include <chrono>
@@ -341,7 +341,7 @@ json server_mcp_transport::call_tool(const std::string & tool_name,
//
struct server_mcp_stdio::process_handle {
subprocess_s sp;
common_subproc sp;
FILE * in = nullptr; // child stdin
FILE * out = nullptr; // child stdout
FILE * err = nullptr; // child stderr
@@ -483,30 +483,15 @@ bool server_mcp_stdio::start() {
envp_s = mcp_build_env(config.env);
}
auto to_ptrs = [](std::vector<std::string> & v) {
std::vector<const char *> p;
p.reserve(v.size() + 1);
for (auto & s : v) {
p.push_back(s.c_str());
}
p.push_back(nullptr);
return p;
};
auto argv = to_ptrs(argv_s);
auto envp = to_ptrs(envp_s);
auto handle = std::make_unique<process_handle>();
int rc = subprocess_create_ex(argv.data(), options,
config.env.empty() ? nullptr : envp.data(),
config.cwd.empty() ? nullptr : config.cwd.c_str(),
&handle->sp);
if (rc != 0) {
bool ok = handle->sp.create(argv_s, options, envp_s, config.cwd.empty() ? nullptr : config.cwd.c_str());
if (!ok) {
SRV_WRN("MCP '%s': failed to spawn '%s'\n", config.name.c_str(), config.command.c_str());
return false;
}
handle->in = subprocess_stdin(&handle->sp);
handle->out = subprocess_stdout(&handle->sp);
handle->err = subprocess_stderr(&handle->sp);
handle->in = handle->sp.stdin_file();
handle->out = handle->sp.stdout_file();
handle->err = handle->sp.stderr_file();
proc = std::move(handle);
running.store(true);
@@ -654,14 +639,13 @@ void server_mcp_stdio::join_pumps() {
to_server.close_write(); // wake the writer if it waits for a message
from_server.close_write(); // wake any caller waiting for a reply
subprocess_terminate(&proc->sp); // child death unblocks the blocked fread/fwrite
proc->sp.terminate(); // child death unblocks the blocked fread/fwrite
if (writer.joinable()) writer.join();
if (reader.joinable()) reader.join();
if (errlog.joinable()) errlog.join();
subprocess_join(&proc->sp, nullptr); // reap the child: destroy() never waits, so the pid would stay a zombie for the process lifetime
subprocess_destroy(&proc->sp); // safe now: no thread touches the FILE* anymore
proc->sp.join(); // reap the child: never waiting would leave the pid a zombie for the process lifetime
proc.reset();
}
+16 -50
View File
@@ -8,10 +8,10 @@
#include "preset.h"
#include "download.h"
#include "http.h"
#include "subproc.h"
#include <cpp-httplib/httplib.h> // TODO: remove this once we use HTTP client from download.h
#include <optional>
#include <sheredom/subprocess.h>
#include <functional>
#include <optional>
@@ -49,43 +49,24 @@ extern char **environ;
#define CHILD_ADDR "127.0.0.1"
struct server_subproc {
std::optional<subprocess_s> sproc; // empty while in DOWNLOADING state
common_subproc sproc; // not yet spawned while in DOWNLOADING state
std::atomic<bool> stopped{false}; // set to cancel a download or signal child process exit
subprocess_s & get() {
GGML_ASSERT(sproc.has_value() && "subprocess not initialized");
return sproc.value();
}
bool is_alive() {
return sproc.has_value() && subprocess_alive(&sproc.value());
return sproc.alive();
}
void request_exit() {
if (sproc.has_value()) {
FILE * stdin_file = subprocess_stdin(&sproc.value());
if (stdin_file) {
fprintf(stdin_file, "%s\n", CMD_ROUTER_TO_CHILD_EXIT);
fflush(stdin_file);
}
FILE * stdin_file = sproc.stdin_file();
if (stdin_file) {
fprintf(stdin_file, "%s\n", CMD_ROUTER_TO_CHILD_EXIT);
fflush(stdin_file);
}
stopped.store(true, std::memory_order_relaxed);
}
void terminate() {
if (!sproc.has_value()) {
return;
}
#if defined(_WIN32)
if (sproc->hProcess == NULL) {
return;
}
#else
if (sproc->child <= 0) {
return;
}
#endif
subprocess_terminate(&sproc.value());
sproc.terminate();
}
};
@@ -711,18 +692,6 @@ std::optional<server_model_meta> server_models::get_meta(const std::string & nam
return std::nullopt;
}
// helper to convert vector<string> to char **
// pointers are only valid as long as the original vector is valid
static std::vector<char *> to_char_ptr_array(const std::vector<std::string> & vec) {
std::vector<char *> result;
result.reserve(vec.size() + 1);
for (const auto & s : vec) {
result.push_back(const_cast<char*>(s.c_str()));
}
result.push_back(nullptr);
return result;
}
std::vector<server_model_meta> server_models::get_all_meta() {
std::unique_lock<std::mutex> lk(mutex);
if (need_reload) {
@@ -845,15 +814,10 @@ void server_models::load(const std::string & name, const load_options & opts) {
}
inst.meta.args = child_args; // save for debugging
std::vector<char *> argv = to_char_ptr_array(child_args);
std::vector<char *> envp = to_char_ptr_array(child_env);
// TODO @ngxson : maybe separate stdout and stderr in the future
// so that we can use stdout for commands and stderr for logging
int options = subprocess_option_no_window | subprocess_option_combined_stdout_stderr;
inst.subproc->sproc.emplace();
int result = subprocess_create_ex(argv.data(), options, envp.data(), nullptr, &inst.subproc->get());
if (result != 0) {
if (!inst.subproc->sproc.create(child_args, options, child_env)) {
throw std::runtime_error("failed to spawn server instance");
}
}
@@ -867,8 +831,8 @@ void server_models::load(const std::string & name, const load_options & opts) {
stop_timeout = inst.meta.stop_timeout,
child_mode = opts.mode
]() {
FILE * stdin_file = subprocess_stdin(&child_proc->get());
FILE * stdout_file = subprocess_stdout(&child_proc->get()); // combined stdout/stderr
FILE * stdin_file = child_proc->sproc.stdin_file();
FILE * stdout_file = child_proc->sproc.stdout_file(); // combined stdout/stderr
std::thread log_thread([&]() {
// read stdout/stderr and forward to main server log
@@ -942,9 +906,7 @@ void server_models::load(const std::string & name, const load_options & opts) {
}
// get the exit code
int exit_code = 0;
subprocess_join(&child_proc->get(), &exit_code);
subprocess_destroy(&child_proc->get());
int exit_code = child_proc->sproc.join();
// update status and exit code
if (child_mode == SERVER_CHILD_MODE_DOWNLOAD) {
@@ -1567,6 +1529,10 @@ static std::optional<server_model_meta> resolve_child_for_conv(
}
void server_models_routes::init_routes() {
if (!common_subproc::is_supported()) {
throw std::runtime_error("subprocess is not enabled on this build");
}
this->get_router_props = [this](const server_http_req & req) {
std::string name = req.get_param("model");
if (name.empty()) {
+11 -19
View File
@@ -1,6 +1,6 @@
#include "server-tools.h"
#include <sheredom/subprocess.h>
#include "subproc.h"
#include <filesystem>
#include <fstream>
@@ -138,15 +138,14 @@ public:
const std::function<bool(const std::string &)> & on_chunk = nullptr) const override {
exec_result res;
subprocess_s proc;
auto argv = to_cstr_vec(args);
common_subproc proc;
int options = subprocess_option_no_window
| subprocess_option_combined_stdout_stderr
| subprocess_option_inherit_environment
| subprocess_option_search_user_path;
if (subprocess_create(argv.data(), options, &proc) != 0) {
if (!proc.create(args, options)) {
res.output = "failed to spawn process";
return res;
}
@@ -159,14 +158,14 @@ public:
while (!done.load()) {
if (std::chrono::steady_clock::now() >= deadline) {
timed_out.store(true);
subprocess_terminate(&proc);
proc.terminate();
return;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
});
FILE * f = subprocess_stdout(&proc);
FILE * f = proc.stdout_file();
std::string output;
bool truncated = false;
if (f) {
@@ -177,7 +176,7 @@ public:
if (output.size() + len <= max_output) {
output.append(buf, len);
if (on_chunk && !on_chunk(std::string(buf, len))) {
subprocess_terminate(&proc);
proc.terminate();
break;
}
} else {
@@ -195,8 +194,7 @@ public:
timeout_thread.join();
}
subprocess_join(&proc, &res.exit_code);
subprocess_destroy(&proc);
res.exit_code = proc.join();
res.output = output;
res.timed_out = timed_out.load();
@@ -207,16 +205,6 @@ public:
}
private:
static std::vector<char *> to_cstr_vec(const std::vector<std::string> & v) {
std::vector<char *> r;
r.reserve(v.size() + 1);
for (const auto & s : v) {
r.push_back(const_cast<char *>(s.c_str()));
}
r.push_back(nullptr);
return r;
}
static const std::unordered_set<std::string> & junk_dir_names() {
static const std::unordered_set<std::string> names = {
".git", ".svn", ".hg", "node_modules", "__pycache__",
@@ -1203,6 +1191,10 @@ static std::vector<std::unique_ptr<server_tool>> build_tools() {
void server_tools::setup(const std::vector<std::string> & enabled_tools,
server_mcp & mcp_mgr) {
if (!enabled_tools.empty()) {
if (!common_subproc::is_supported()) {
throw std::runtime_error("subprocess is not enabled on this build");
}
std::unordered_set<std::string> enabled_set(enabled_tools.begin(), enabled_tools.end());
auto all_tools = build_tools();