34 IgnoreSigpipe() { signal(SIGPIPE, SIG_IGN); }
36static IgnoreSigpipe ignore_sigpipe;
65 out.reserve(raw.size());
67 if (out.empty() && c ==
'/') {
continue; }
68 if (c ==
'[' || c ==
']') {
continue; }
69 if (
static_cast<unsigned char>(c) < 0x20 || c == 0x7f) {
continue; }
72 return out.empty() ? std::string{
"server"} : out;
86 std::vector<std::string> args,
87 std::map<std::string, std::string> env,
88 uint32_t default_timeout_ms)
90 command_(std::move(command)),
91 args_(std::move(args)),
93 default_timeout_ms_(default_timeout_ms) {}
111 std::string display_name,
113 std::vector<std::string> args,
114 std::map<std::string, std::string> env,
115 uint32_t default_timeout_ms)
117 display_name.empty() ? command : std::move(display_name))),
118 command_(std::move(command)),
119 args_(std::move(args)),
120 env_(std::move(env)),
121 default_timeout_ms_(default_timeout_ms) {}
139bool StdioTransport::create_all_pipes(
int (&fds)[6]) {
140 bool ok = create_pipe(fds[0], fds[1]) &&
141 create_pipe(fds[2], fds[3]) &&
142 create_pipe(fds[4], fds[5]);
144 logger->error(
"Failed to create pipes: {}", strerror(errno));
145 for (
auto& fd : fds) { close_fd(fd); }
161 if (!open_child_process()) {
166 stderr_thread_ = std::thread(
167 &StdioTransport::stderr_reader_loop,
this);
169 logger->info(
"Spawned child process PID {} for '{}' (cmd: {})",
170 child_pid_, display_name_, command_);
180bool StdioTransport::open_child_process() {
181 int fds[6] = {-1, -1, -1, -1, -1, -1};
182 if (!create_all_pipes(fds)) {
186 auto env_strs = build_env();
187 bool ok = spawn_child(fds[0], fds[3], fds[5], env_strs);
217 close_fd(stdout_fd_);
218 close_fd(stderr_fd_);
220 if (stderr_thread_.joinable()) {
221 stderr_thread_.join();
224 logger->info(
"Closed stdio transport for '{}'", display_name_);
240 const std::string& request_json,
241 uint32_t timeout_ms) {
243 if (!connected_ || cancel_flag_.load(std::memory_order_acquire)) {
247 uint32_t actual_timeout = timeout_ms > 0
248 ? timeout_ms : default_timeout_ms_;
250 std::lock_guard<std::mutex> lock(io_mutex_);
252 std::string msg = request_json +
"\n";
253 ssize_t written = ::write(stdin_fd_, msg.data(), msg.size());
254 if (written < 0 ||
static_cast<size_t>(written) != msg.size()) {
255 logger->error(
"Write to child stdin failed: {}",
261 return read_line(stdout_fd_, actual_timeout);
274 if (child_pid_ > 0 && kill(child_pid_, 0) == 0) {
295 cancel_flag_.store(
true, std::memory_order_release);
306static std::vector<char*>
build_argv(
const std::string& command,
307 const std::vector<std::string>& args) {
308 std::vector<char*> argv;
309 argv.push_back(
const_cast<char*
>(command.c_str()));
310 for (
const auto& arg : args) {
311 argv.push_back(
const_cast<char*
>(arg.c_str()));
313 argv.push_back(
nullptr);
325 const std::vector<std::string>& env_strs) {
326 std::vector<char*> envp;
327 for (
const auto& e : env_strs) {
328 envp.push_back(
const_cast<char*
>(e.c_str()));
330 envp.push_back(
nullptr);
344bool StdioTransport::spawn_child(
345 int stdin_r,
int stdout_w,
int stderr_w,
346 const std::vector<std::string>& env_strs) {
348 posix_spawn_file_actions_t actions;
349 posix_spawn_file_actions_init(&actions);
350 posix_spawn_file_actions_adddup2(&actions, stdin_r, STDIN_FILENO);
351 posix_spawn_file_actions_adddup2(&actions, stdout_w, STDOUT_FILENO);
352 posix_spawn_file_actions_adddup2(&actions, stderr_w, STDERR_FILENO);
357 int err = posix_spawnp(
358 &child_pid_, command_.c_str(), &actions,
359 nullptr, argv.data(), envp.data());
361 posix_spawn_file_actions_destroy(&actions);
364 logger->error(
"posix_spawnp failed for '{}' (cmd: {}): {}",
365 display_name_, command_, strerror(err));
384std::vector<std::string> StdioTransport::build_env()
const {
385 std::map<std::string, std::string> merged;
388 for (
char** ep = environ; ep && *ep; ++ep) {
389 std::string entry(*ep);
390 auto eq = entry.find(
'=');
391 if (eq != std::string::npos) {
392 merged[entry.substr(0, eq)] = entry.substr(eq + 1);
397 for (
const auto& [key, val] : env_) {
401 std::vector<std::string> result;
402 result.reserve(merged.size());
403 for (
const auto& [key, val] : merged) {
404 result.push_back(key +
"=" + val);
417bool StdioTransport::create_pipe(
int& read_fd,
int& write_fd) {
419 if (::pipe(fds) != 0) {
422 ::fcntl(fds[0], F_SETFD, FD_CLOEXEC);
423 ::fcntl(fds[1], F_SETFD, FD_CLOEXEC);
437int StdioTransport::poll_until_ready(
439 std::chrono::steady_clock::time_point deadline) {
441 auto remaining = std::chrono::duration_cast<
442 std::chrono::milliseconds>(
443 deadline - std::chrono::steady_clock::now());
445 if (remaining.count() <= 0) {
451 constexpr int kSliceMs = 100;
452 int slice =
static_cast<int>(remaining.count());
453 if (slice > kSliceMs) { slice = kSliceMs; }
455 struct pollfd pfd{fd, POLLIN, 0};
456 int rc = ::poll(&pfd, 1, slice);
459 if (std::chrono::steady_clock::now() < deadline) {
479std::string StdioTransport::read_line(
int fd, uint32_t timeout_ms) {
481 auto deadline = std::chrono::steady_clock::now() +
482 std::chrono::milliseconds(timeout_ms);
486 if (cancel_flag_.load(std::memory_order_acquire)) {
487 logger->info(
"Transport read cancelled by interrupt");
490 int ready = poll_until_ready(fd, deadline);
491 if (ready == -2) {
continue; }
493 if (ready == 0) { logger->warn(
"Read timeout after {}ms", timeout_ms); }
497 if (::read(fd, &ch, 1) <= 0) {
break; }
498 if (ch ==
'\n') {
return line; }
509void StdioTransport::stderr_reader_loop() {
510 constexpr int poll_timeout_ms = 500;
512 struct pollfd pfd{stderr_fd_, POLLIN, 0};
513 int ret = ::poll(&pfd, 1, poll_timeout_ms);
519 ssize_t n = ::read(stderr_fd_, buf,
sizeof(buf) - 1);
524 logger->warn(
"[{}] {}", display_name_, buf);
533void StdioTransport::terminate_child() {
534 if (child_pid_ <= 0) {
538 ::kill(child_pid_, SIGTERM);
541 constexpr int max_wait_ms = 3000;
542 constexpr int poll_interval_ms = 50;
544 while (waited < max_wait_ms) {
546 pid_t result = ::waitpid(child_pid_, &status, WNOHANG);
547 if (result == child_pid_) {
551 std::this_thread::sleep_for(
552 std::chrono::milliseconds(poll_interval_ms));
553 waited += poll_interval_ms;
557 ::kill(child_pid_, SIGKILL);
558 ::waitpid(child_pid_,
nullptr, 0);
559 logger->warn(
"Force-killed child PID {}", child_pid_);
569void StdioTransport::close_fd(
int& fd) {
bool open() override
Spawn child process and open pipes.
bool is_connected() const override
Check if child process is alive.
void interrupt() override
Abort any blocking read and mark transport uncancellable.
std::string send_request(const std::string &request_json, uint32_t timeout_ms=0) override
Send JSON-RPC request via stdin, read response from stdout.
void close() override
Send SIGTERM, reap child, close pipes.
StdioTransport(std::string command, std::vector< std::string > args, std::map< std::string, std::string > env={}, uint32_t default_timeout_ms=30000)
Construct with command, arguments, and environment.
~StdioTransport() override
Destructor — ensures child is cleaned up.
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Activate model on GPU (WARM → ACTIVE).
static std::vector< char * > build_envp(const std::vector< std::string > &env_strs)
Build a NULL-terminated envp from env strings.
static std::string sanitize_display_name(const std::string &raw)
Sanitize a display_name for use as a log-line bracket label.
static std::vector< char * > build_argv(const std::string &command, const std::vector< std::string > &args)
Build a NULL-terminated argv from command + args.
@ ok
Tool dispatched, returned non-empty content.
Stdio transport for external MCP servers.