35 IgnoreSigpipe() { signal(SIGPIPE, SIG_IGN); }
37static IgnoreSigpipe ignore_sigpipe;
66 out.reserve(
raw.size());
68 if (out.empty() &&
c ==
'/') {
continue; }
69 if (
c ==
'[' ||
c ==
']') {
continue; }
70 if (
static_cast<unsigned char>(
c) < 0x20 ||
c == 0x7f) {
continue; }
73 return out.empty() ? std::string{
"server"} : out;
87 std::vector<std::string> args,
88 std::map<std::string, std::string> env,
112 std::string display_name,
114 std::vector<std::string> args,
115 std::map<std::string, std::string> env,
118 display_name.
empty() ? command :
std::
move(display_name))),
140bool StdioTransport::create_all_pipes(
int (&
fds)[6]) {
141 bool ok = create_pipe(
fds[0],
fds[1]) &&
142 create_pipe(
fds[2],
fds[3]) &&
143 create_pipe(
fds[4],
fds[5]);
145 logger->error(
"Failed to create pipes: {}",
strerror(
errno));
146 for (
auto& fd :
fds) { close_fd(fd); }
162 cancel_flag_.store(
false, std::memory_order_release);
168 if (!open_child_process()) {
173 stderr_thread_ = std::thread(
174 &StdioTransport::stderr_reader_loop,
this);
176 logger->info(
"Spawned child process PID {} for '{}' (cmd: {})",
177 child_pid_, display_name_, command_);
187bool StdioTransport::open_child_process() {
188 int fds[6] = {-1, -1, -1, -1, -1, -1};
189 if (!create_all_pipes(
fds)) {
232 if (stderr_thread_.joinable()) {
233 stderr_thread_.join();
237 close_fd(stdout_fd_);
238 close_fd(stderr_fd_);
240 logger->info(
"Closed stdio transport for '{}'", display_name_);
269 std::lock_guard<std::mutex> lock(io_mutex_);
272 int dropped = drain_orphaned_responses();
274 logger->warn(
"Discarded {} orphaned response line(s) from '{}' "
275 "before sending the next request",
dropped,
282 logger->error(
"Write to child stdin failed: {}",
303int StdioTransport::drain_orphaned_responses() {
309 struct pollfd pfd{stdout_fd_, POLLIN, 0};
312 if (
::read(stdout_fd_, &
ch, 1) <= 0) {
break; }
329 if (child_pid_ > 0 &&
kill(child_pid_, 0) == 0) {
358 cancel_flag_.store(
true, std::memory_order_release);
371 cancel_flag_.store(
false, std::memory_order_release);
381 return cancel_flag_.load(std::memory_order_acquire);
409 return cancel_flag_.load(std::memory_order_acquire)
421static std::vector<char*>
build_argv(
const std::string& command,
422 const std::vector<std::string>& args) {
423 std::vector<char*>
argv;
424 argv.push_back(
const_cast<char*
>(command.c_str()));
425 for (
const auto&
arg : args) {
426 argv.push_back(
const_cast<char*
>(
arg.c_str()));
428 argv.push_back(
nullptr);
440 const std::vector<std::string>&
env_strs) {
441 std::vector<char*>
envp;
443 envp.push_back(
const_cast<char*
>(
e.c_str()));
445 envp.push_back(
nullptr);
465bool StdioTransport::spawn_child(
467 const std::vector<std::string>&
env_strs) {
474 if (!working_dir_.empty()) {
482 &child_pid_, command_.c_str(), &
actions,
488 logger->error(
"posix_spawnp failed for '{}' (cmd: {}, cwd: '{}'): {}",
489 display_name_, command_, working_dir_,
strerror(
err));
493 logger->info(
"Spawned '{}' (pid {}) in '{}'", display_name_, child_pid_,
494 working_dir_.empty() ?
"<host cwd>" : working_dir_);
510std::vector<std::string> StdioTransport::build_env()
const {
511 std::map<std::string, std::string>
merged;
514 for (
char**
ep = environ;
ep && *
ep; ++
ep) {
515 std::string entry(*
ep);
516 auto eq = entry.find(
'=');
517 if (
eq != std::string::npos) {
518 merged[entry.substr(0,
eq)] = entry.substr(
eq + 1);
523 for (
const auto& [key,
val] : env_) {
527 std::vector<std::string> result;
528 result.reserve(
merged.size());
530 result.push_back(key +
"=" +
val);
563int StdioTransport::poll_until_ready(
565 std::chrono::steady_clock::time_point deadline) {
567 auto remaining = std::chrono::duration_cast<
568 std::chrono::milliseconds>(
569 deadline - std::chrono::steady_clock::now());
585 if (std::chrono::steady_clock::now() < deadline) {
607 auto deadline = std::chrono::steady_clock::now() +
613 logger->info(
"Transport read cancelled by interrupt");
616 int ready = poll_until_ready(fd, deadline);
617 if (
ready == -2) {
continue; }
619 if (
ready == 0) { logger->warn(
"Read timeout after {}ms",
timeout_ms); }
623 if (
::read(fd, &
ch, 1) <= 0) {
break; }
624 if (
ch ==
'\n') {
return line; }
635void StdioTransport::stderr_reader_loop() {
638 struct pollfd pfd{stderr_fd_, POLLIN, 0};
650 logger->warn(
"[{}] {}", display_name_,
buf);
659void StdioTransport::terminate_child() {
660 if (child_pid_ <= 0) {
673 if (result == child_pid_) {
677 std::this_thread::sleep_for(
685 logger->warn(
"Force-killed child PID {}", child_pid_);
695void StdioTransport::close_fd(
int& fd) {
bool is_interrupted() const override
Whether an interrupt is currently short-circuiting calls.
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 the child, join the stderr pump, 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.
bool request_cancelled() const
Whether THIS request must abort (gh#158, v2.13.0).
~StdioTransport() override
Destructor — ensures child is cleaned up.
void clear_interrupt() override
Release the cancel flag so this transport works again.
json lines
Lines taken so far.
std::string err
size_exceeded, when set
long bytes
Content bytes taken.
std::size_t remaining
Unexamined count.
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.
@ optional
optional: every escape asks the host's path approver
ENTROPIC_EXPORT bool current_run_cancelled()
Whether THIS thread's run has been cancelled.
@ read
read_file, list_directory
@ write
write_file, edit_file
gh#158: the cancel token of the run executing on THIS thread.
Stdio transport for external MCP servers.