Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ else()
set(LLAMA_TOOLS_INSTALL_DEFAULT ${LLAMA_STANDALONE})
endif()

# subprocess spawning isn't a supported/sandbox-friendly operation on mobile OSes or in WASM
if (CMAKE_SYSTEM_NAME STREQUAL "iOS" OR CMAKE_SYSTEM_NAME STREQUAL "Android" OR ANDROID
OR CMAKE_SYSTEM_NAME STREQUAL "Emscripten" OR EMSCRIPTEN)
set(LLAMA_SUBPROCESS_DEFAULT OFF)
else()
set(LLAMA_SUBPROCESS_DEFAULT ON)
endif()

#
# option list
#
Expand Down Expand Up @@ -117,6 +125,7 @@ option(LLAMA_TESTS_INSTALL "llama: install tests" ON)

# 3rd party libs
option(LLAMA_OPENSSL "llama: use openssl to support HTTPS" ON)
option(LLAMA_SUBPROCESS "llama-common: use subprocess, required by server tools and server router mode" ${LLAMA_SUBPROCESS_DEFAULT})
option(LLAMA_LLGUIDANCE "llama-common: include LLGuidance library for structured output in common utils" OFF)


Expand Down
6 changes: 6 additions & 0 deletions common/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ add_library(${TARGET}
sampling.h
speculative.cpp
speculative.h
subproc.cpp
subproc.h
trie.cpp
trie.h
unicode.cpp
Expand Down Expand Up @@ -127,6 +129,10 @@ set_target_properties(${TARGET} PROPERTIES
target_include_directories(${TARGET} PUBLIC . ../vendor)
target_compile_features (${TARGET} PUBLIC cxx_std_17)

if (LLAMA_SUBPROCESS)
target_compile_definitions(${TARGET} PUBLIC LLAMA_SUBPROCESS)
endif()

if (BUILD_SHARED_LIBS)
set_target_properties(${TARGET} PROPERTIES POSITION_INDEPENDENT_CODE ON)

Expand Down
143 changes: 143 additions & 0 deletions common/subproc.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#include "subproc.h"

bool common_subproc::is_supported() {
#ifdef LLAMA_SUBPROCESS
return true;
#else
return false;
#endif
}

#ifdef LLAMA_SUBPROCESS

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;
}

common_subproc::~common_subproc() {
if (is_created) {
subprocess_destroy(&proc);
is_created = false;
}
}

bool common_subproc::create(
const std::vector<std::string> & args,
int options,
const std::vector<std::string> & env,
const char * cwd) {
auto argv = to_cstr_vec(args);

int result;
if (env.empty() && cwd == nullptr) {
result = subprocess_create(argv.data(), options, &proc);
} else {
auto envp = to_cstr_vec(env);
result = subprocess_create_ex(argv.data(), options, env.empty() ? nullptr : envp.data(), cwd, &proc);
}

is_created = result == 0;
return is_created;
}

bool common_subproc::has_handle() const {
if (!is_created) {
return false;
}
#if defined(_WIN32)
return proc.hProcess != nullptr;
#else
return proc.child > 0;
#endif
}

bool common_subproc::alive() {
return is_created && subprocess_alive(&proc);
}

FILE * common_subproc::stdin_file() {
return is_created ? subprocess_stdin(&proc) : nullptr;
}

FILE * common_subproc::stdout_file() {
return is_created ? subprocess_stdout(&proc) : nullptr;
}

FILE * common_subproc::stderr_file() {
return is_created ? subprocess_stderr(&proc) : nullptr;
}

void common_subproc::close_stdin() {
if (is_created && proc.stdin_file) {
fclose(proc.stdin_file);
proc.stdin_file = nullptr;
}
}

void common_subproc::terminate() {
if (has_handle()) {
subprocess_terminate(&proc);
}
}

int common_subproc::join() {
int exit_code = -1;
if (is_created) {
subprocess_join(&proc, &exit_code);
subprocess_destroy(&proc);
is_created = false;
}
return exit_code;
}

#else // !LLAMA_SUBPROCESS

common_subproc::~common_subproc() = default;

bool common_subproc::create(
const std::vector<std::string> &,
int,
const std::vector<std::string> &,
const char *) {
(void)(proc);
(void)(is_created);
return false;
}

bool common_subproc::has_handle() const {
return false;
}

bool common_subproc::alive() {
return false;
}

FILE * common_subproc::stdin_file() {
return nullptr;
}

FILE * common_subproc::stdout_file() {
return nullptr;
}

FILE * common_subproc::stderr_file() {
return nullptr;
}

void common_subproc::close_stdin() {
}

void common_subproc::terminate() {
}

int common_subproc::join() {
return -1;
}

#endif // LLAMA_SUBPROCESS
59 changes: 59 additions & 0 deletions common/subproc.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#pragma once

#include <atomic>
#include <cstdio>
#include <string>
#include <vector>

#ifdef LLAMA_SUBPROCESS
#include <sheredom/subprocess.h>
#else
// dummy values to allow compilation when subprocess is disabled
struct subprocess_s {};
static constexpr int subprocess_option_no_window = 0;
static constexpr int subprocess_option_combined_stdout_stderr = 0;
static constexpr int subprocess_option_inherit_environment = 0;
static constexpr int subprocess_option_search_user_path = 0;
#endif

// RAII-style wrapper around https://github.com/sheredom/subprocess.h,
// exposing method calls instead of free functions operating on subprocess_s.
struct common_subproc {
common_subproc() = default;
~common_subproc();

common_subproc(const common_subproc &) = delete;
common_subproc & operator=(const common_subproc &) = delete;

// spawn a child process; if env is non-empty it replaces the child's environment
// (do not combine with subprocess_option_inherit_environment)
bool create(
const std::vector<std::string> & args,
int options,
const std::vector<std::string> & env = {},
const char * cwd = nullptr);

bool alive();

// true if LLAMA_SUBPROCESS was enabled at build time; when false, create() always fails
static bool is_supported();

FILE * stdin_file();
FILE * stdout_file();
FILE * stderr_file();

// close stdin and detach it from the process, so a later join()/destroy() won't double-close it;
// use this after writing all input to signal EOF to the child while it's still running
void close_stdin();

void terminate();

// wait for the process to exit, release the underlying handle and return its exit code
int join();

private:
subprocess_s proc {};
std::atomic<bool> is_created{false};

bool has_handle() const;
};
25 changes: 11 additions & 14 deletions tests/test-jinja.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#include <cstdlib>

#include <nlohmann/json.hpp>
#include <sheredom/subprocess.h>
#include "subproc.h"

#include "jinja/runtime.h"
#include "jinja/parser.h"
Expand Down Expand Up @@ -2135,46 +2135,43 @@ static void test_template_py(testing & t, const std::string & name, const std::s
const char * python_executable = "python3";
#endif

const char * command_line[] = {python_executable, "-c", py_script.c_str(), NULL};
std::vector<std::string> args = {python_executable, "-c", py_script, };

struct subprocess_s subprocess;
common_subproc subprocess;
int options = subprocess_option_combined_stdout_stderr
| subprocess_option_no_window
| subprocess_option_inherit_environment
| subprocess_option_search_user_path;
int result = subprocess_create(command_line, options, &subprocess);

if (result != 0) {
t.log("Failed to create subprocess, error code: " + std::to_string(result));
if (!subprocess.create(args, options)) {
t.log("Failed to create subprocess");
t.assert_true("subprocess creation", false);
return;
}
FILE * p_stdin = subprocess_stdin(&subprocess);
FILE * p_stdin = subprocess.stdin_file();

// Write input
std::string input = merged.dump();
auto written = fwrite(input.c_str(), 1, input.size(), p_stdin);
if (written != input.size()) {
t.log("Failed to write complete input to subprocess stdin");
t.assert_true("subprocess stdin write", false);
subprocess_destroy(&subprocess);
subprocess.close_stdin();
subprocess.join();
return;
}
fflush(p_stdin);
fclose(p_stdin); // Close stdin to signal EOF to the Python process
subprocess.stdin_file = nullptr;
subprocess.close_stdin(); // Close stdin to signal EOF to the Python process

// Read output
std::string output;
char buffer[1024];
FILE * p_stdout = subprocess_stdout(&subprocess);
FILE * p_stdout = subprocess.stdout_file();
while (fgets(buffer, sizeof(buffer), p_stdout)) {
output += buffer;
}

int process_return;
subprocess_join(&subprocess, &process_return);
subprocess_destroy(&subprocess);
int process_return = subprocess.join();

if (process_return != 0) {
t.log("Python script failed with exit code: " + std::to_string(process_return));
Expand Down
9 changes: 8 additions & 1 deletion tools/mtmd/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
# mtmd

set(MTMD_VIDEO ON CACHE BOOL "enable video support in mtmd (requires ffmpeg binary in PATH)")
set(MTMD_VIDEO_HELP "enable video support in mtmd (requires ffmpeg binary in PATH)")

set(MTMD_VIDEO ON CACHE BOOL "${MTMD_VIDEO_HELP}")
# TODO: add MTMD_VIDEO_METHOD in the future to select between ffmpeg and other backends

if (MTMD_VIDEO AND NOT LLAMA_SUBPROCESS)
message(STATUS "Disabling MTMD_VIDEO because LLAMA_SUBPROCESS is OFF")
set(MTMD_VIDEO OFF CACHE BOOL "${MTMD_VIDEO_HELP}" FORCE)
endif()

find_package(Threads REQUIRED)

add_library(mtmd
Expand Down
34 changes: 9 additions & 25 deletions tools/server/server-mcp.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#include "server-mcp.h"

#include <sheredom/subprocess.h>
#include "subproc.h"

#include <atomic>
#include <chrono>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
}

Expand Down
Loading
Loading