/*** This file is part of snapcast Copyright (C) 2014-2025 Johannes Pohl This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ***/ // prototype/interface header file #include "process_stream.hpp" // local headers #include "common/aixlog.hpp" #include "common/snap_exception.hpp" #include "common/utils.hpp" #include "common/utils/file_utils.hpp" // standard headers #include using namespace std; namespace streamreader { static constexpr auto LOG_TAG = "ProcessStream"; ProcessStream::ProcessStream(PcmStream::Listener* pcmListener, boost::asio::io_context& ioc, const ServerSettings& server_settings, const StreamUri& uri) : AsioStream(pcmListener, ioc, server_settings, uri) { params_ = uri_.getQuery("params"); wd_timeout_sec_ = cpt::stoul(uri_.getQuery("wd_timeout", "0")); LOG(DEBUG, LOG_TAG) << "Watchdog timeout: " << wd_timeout_sec_ << "\n"; logStderr_ = (uri_.getQuery("log_stderr", "false") == "true"); } std::string ProcessStream::findExe(const std::string& filename) const { /// check if filename exists if (utils::file::exists(filename)) return filename; std::string exe = filename; if (exe.find('/') != string::npos) exe = exe.substr(exe.find_last_of('/') + 1); /// check with "which" string which = execGetOutput("which " + exe); if (!which.empty()) return which; /// check in the same path as this binary char buff[PATH_MAX]; char szTmp[32]; sprintf(szTmp, "/proc/%d/exe", getpid()); ssize_t len = readlink(szTmp, buff, sizeof(buff) - 1); if (len != -1) { buff[len] = '\0'; return string(buff) + "/" + exe; } return ""; } void ProcessStream::initExeAndPath(const std::string& filename) { path_ = ""; exe_ = findExe(filename); if (exe_.find('/') != string::npos) { path_ = exe_.substr(0, exe_.find_last_of('/') + 1); exe_ = exe_.substr(exe_.find_last_of('/') + 1); } if (!utils::file::exists(path_ + exe_)) throw SnapException("file not found: \"" + filename + "\""); } void ProcessStream::connect() { if (!active_) return; initExeAndPath(uri_.path); LOG(DEBUG, LOG_TAG) << "Launching: '" << path_ + exe_ << "', with params: '" << params_ << "', in path: '" << path_ << "'\n"; pipe_stdout_ = bp::pipe(); // could use bp::async_pipe, but this is broken in boost 1.72: // https://github.com/boostorg/process/issues/116 pipe_stderr_ = bp::pipe(); // stdout pipe should not block int flags = fcntl(pipe_stdout_.native_source(), F_GETFL, 0); fcntl(pipe_stdout_.native_source(), F_SETFL, flags | O_NONBLOCK); process_ = bp::child(path_ + exe_ + " " + params_, bp::std_out > pipe_stdout_, bp::std_err > pipe_stderr_, bp::start_dir = path_); stream_ = make_unique(strand_, pipe_stdout_.native_source()); stream_stderr_ = make_unique(strand_, pipe_stderr_.native_source()); on_connect(); if (wd_timeout_sec_ > 0) { watchdog_ = make_unique(strand_, this); watchdog_->start(std::chrono::seconds(wd_timeout_sec_)); } else { watchdog_ = nullptr; } stderrReadLine(); } void ProcessStream::disconnect() { if (process_.running()) ::kill(-process_.native_handle(), SIGINT); AsioStream::disconnect(); } void ProcessStream::onStderrMsg(const std::string& line) { if (logStderr_) { LOG(INFO, LOG_TAG) << "(" << getName() << ") " << line << "\n"; } } void ProcessStream::stderrReadLine() { const std::string delimiter = "\n"; boost::asio::async_read_until(*stream_stderr_, streambuf_stderr_, delimiter, [this, delimiter](const std::error_code& ec, std::size_t bytes_transferred) { if (ec) { LOG(ERROR, LOG_TAG) << "Error while reading from stderr: " << ec.message() << "\n"; return; } if (watchdog_) watchdog_->trigger(); // Extract up to the first delimiter. std::string line{buffers_begin(streambuf_stderr_.data()), buffers_begin(streambuf_stderr_.data()) + bytes_transferred - delimiter.length()}; if (!line.empty()) { if (line.back() == '\r') line.resize(line.size() - 1); onStderrMsg(line); } streambuf_stderr_.consume(bytes_transferred); stderrReadLine(); }); } void ProcessStream::onTimeout(const Watchdog& /*watchdog*/, std::chrono::milliseconds ms) { LOG(ERROR, LOG_TAG) << "Watchdog timeout: " << ms.count() / 1000 << "s\n"; if (process_) ::kill(-process_.native_handle(), SIGINT); } } // namespace streamreader