/* * Copyright (C) 2020 Emeric Poupon * * This file is part of LMS. * * LMS 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. * * LMS 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 LMS. If not, see . */ #include "ChildProcess.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include "core/ILogger.hpp" namespace lms::core { namespace { class SystemException : public ChildProcessException { public: SystemException(std::error_code err, const std::string& errMsg) : ChildProcessException{ errMsg + ": " + err.message() } { } SystemException(boost::system::error_code ec, const std::string& errMsg) : ChildProcessException{ errMsg + ": " + ec.message() } { } }; } // namespace ChildProcess::ChildProcess(boost::asio::io_context& ioContext, const std::filesystem::path& path, const Args& args) : _ioContext{ ioContext } , _childStdout{ _ioContext } { // make sure only one thread is executing this part of code static std::mutex mutex; const std::scoped_lock lock{ mutex }; int pipefd[2]; // Use 'pipe' instead of 'pipe2', more portable if (pipe(pipefd) == -1) throw SystemException{ std::error_code{ errno, std::generic_category() }, "pipe failed!" }; // Only set O_NONBLOCK on read end - usually programs don't expect stdout to be non-blocking if (fcntl(pipefd[0], F_SETFL, O_NONBLOCK) == -1) throw SystemException{ std::error_code{ errno, std::generic_category() }, "fcntl failed to set O_NONBLOCK!" }; #if defined(__linux__) && defined(F_SETPIPE_SZ) for (const int fd : { pipefd[0], pipefd[1] }) { constexpr int targetPipeSize{ 65'536 * 4 }; int currentPipeSize{ 65'536 }; // common default value #if defined(F_GETPIPE_SZ) const int pipeSizeRes{ fcntl(fd, F_GETPIPE_SZ) }; if (pipeSizeRes == -1) { const int err{ errno }; LMS_LOG(CHILDPROCESS, DEBUG, "F_GETPIPE_SZ failed: " << (std::error_code{ err, std::generic_category() }.message())); } else { currentPipeSize = pipeSizeRes; } #endif if (currentPipeSize < targetPipeSize) { if (::fcntl(fd, F_SETPIPE_SZ, targetPipeSize) == -1) { const int err{ errno }; LMS_LOG(CHILDPROCESS, DEBUG, "F_SETPIPE_SZ failed: " << (std::error_code{ err, std::generic_category() }.message())); } } } #endif const int res{ fork() }; if (res == -1) throw SystemException{ std::error_code{ errno, std::generic_category() }, "fork failed!" }; if (res == 0) // CHILD { // Never close stdin/out/err, most programs expect these to exist; // rather connect them to /dev/null if unwanted const int nullFd{ open("/dev/null", O_RDWR) }; // Ignore errors, worst thing is stderr writes to the same fd as lms if (nullFd != -1) { dup2(nullFd, STDIN_FILENO); dup2(nullFd, STDERR_FILENO); close(nullFd); } // Replace stdout with pipe write if (dup2(pipefd[1], STDOUT_FILENO) == -1) exit(-1); // Close pipe: read end not needed, write end was dup2ed close(pipefd[0]); close(pipefd[1]); std::vector execArgs; std::transform(std::cbegin(args), std::cend(args), std::back_inserter(execArgs), [](const std::string& arg) { return arg.c_str(); }); execArgs.push_back(nullptr); execv(path.string().c_str(), (char* const*)&execArgs[0]); exit(-1); } else // PARENT { close(pipefd[1]); { boost::system::error_code assignError; _childStdout.assign(pipefd[0], assignError); if (assignError) throw SystemException{ assignError, "assigning read end of pipe to asio stream failed!" }; } _childPID = res; } } ChildProcess::~ChildProcess() { LMS_LOG(CHILDPROCESS, DEBUG, "Closing child process..."); { boost::system::error_code closeError; _childStdout.close(closeError); if (closeError) LMS_LOG(CHILDPROCESS, ERROR, "Closed failed: " << closeError.message()); } if (!_finished) kill(); wait(true); } void ChildProcess::kill() { // process may already have finished LMS_LOG(CHILDPROCESS, DEBUG, "Killing child process..."); if (::kill(_childPID, SIGKILL) == -1) { const int err{ errno }; LMS_LOG(CHILDPROCESS, DEBUG, "Kill failed: " << (std::error_code{ err, std::generic_category() }.message())); } } bool ChildProcess::wait(bool block) { assert(!_waited); int wstatus{}; const pid_t pid{ waitpid(_childPID, &wstatus, block ? 0 : WNOHANG) }; if (pid == -1) throw SystemException{ std::error_code{ errno, std::generic_category() }, "waitpid failed!" }; if (pid == 0) return false; if (WIFEXITED(wstatus)) { _exitCode = WEXITSTATUS(wstatus); LMS_LOG(CHILDPROCESS, DEBUG, "Exit code = " << *_exitCode); } _waited = true; return true; } void ChildProcess::asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback) { assert(!finished()); LMS_LOG(CHILDPROCESS, DEBUG, "Async read, bufferSize = " << bufferSize); boost::asio::async_read(_childStdout, boost::asio::buffer(data, bufferSize), [this, callback{ std::move(callback) }](const boost::system::error_code& error, std::size_t bytesTransferred) { LMS_LOG(CHILDPROCESS, DEBUG, "Async read cb - ec = '" << error.message() << "' (" << error.value() << "), bytesTransferred = " << bytesTransferred); ReadResult readResult{ ReadResult::Success }; if (error) { if (error != boost::asio::error::eof) { // forbidden to read any captured param here as the ChildProcess instance may already have been killed return; } readResult = ReadResult::EndOfFile; _finished = true; } callback(readResult, bytesTransferred); }); } std::size_t ChildProcess::readSome(std::byte* data, std::size_t bufferSize) { boost::system::error_code ec; const std::size_t res{ _childStdout.read_some(boost::asio::buffer(data, bufferSize), ec) }; LMS_LOG(CHILDPROCESS, DEBUG, "read some " << res << " bytes, ec = " << ec.message()); if (ec) _childStdout.close(ec); return res; } bool ChildProcess::finished() const { return _finished; } } // namespace lms::core