// ***************************************************************************** // * This file is part of the FreeFileSync project. It is distributed under * // * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * // * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * // ***************************************************************************** #include "process_exec.h" #include #include "guid.h" #include "file_access.h" #include "file_io.h" #include //fork, pipe #include //waitpid #include using namespace zen; Zstring zen::escapeCommandArg(const Zstring& arg) { //*INDENT-OFF* Zstring output; for (const Zchar c : arg) switch (c) { case '"': output += "\\\""; break; //Windows: not needed; " cannot be used as file name case '\\': output += "\\\\"; break; //Windows: path separator! => don't escape case '`': output += "\\`"; break; //yes, used in some paths => Windows: no escaping required default: output += c; break; } //*INDENT-ON* if (contains(output, Zstr(' '))) output = Zstr('"') + output + Zstr('"'); //Windows: escaping a single blank instead would not work return output; } namespace { std::pair processExecuteImpl(const Zstring& filePath, const std::vector& arguments, std::optional timeoutMs) //throw SysError, SysErrorTimeOut { const Zstring tempFilePath = appendSeparator(getTempFolderPath()) + //throw FileError Zstr("FFS-") + utfTo(formatAsHexString(generateGUID())); /* can't use popen(): does NOT return the exit code on Linux (despite the documentation!), although it works correctly on macOS => use pipes instead: https://linux.die.net/man/2/waitpid bonus: no need for "2>&1" to redirect STDERR to STDOUT What about premature exit via SysErrorTimeOut? Linux: child process' end of the pipe *still works* even after the parent process is gone: There does not seem to be any output buffer size limit + no observable strain on system memory or disk space! :) macOS: child process exits if parent end of pipe is closed: fuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu.......... => solution: buffer output in temporary file Unresolved problem: premature exit via SysErrorTimeOut (=> no waitpid()) creates zombie proceses: "As long as a zombie is not removed from the system via a wait, it will consume a slot in the kernel process table, and if this table fills, it will not be possible to create further processes." */ const int EC_CHILD_LAUNCH_FAILED = 120; //avoid 127: used by the system, e.g. failure to execute due to missing .so file //use O_TMPFILE? sounds nice, but support is probably crap: https://github.com/libvips/libvips/issues/1151 const int fdTempFile = ::open(tempFilePath.c_str(), O_CREAT | O_EXCL | O_RDWR | O_CLOEXEC, S_IRUSR | S_IWUSR); //0600 if (fdTempFile == -1) THROW_LAST_SYS_ERROR("open"); auto guardTmpFile = makeGuard([&] { ::close(fdTempFile); }); //"deleting while handle is open" == FILE_FLAG_DELETE_ON_CLOSE if (::unlink(tempFilePath.c_str()) != 0) THROW_LAST_SYS_ERROR("unlink"); //-------------------------------------------------------------- //waitpid() is a useless pile of garbage without time out => check EOF from dummy pipe instead int pipe[2] = {}; if (::pipe2(pipe, O_CLOEXEC) != 0) THROW_LAST_SYS_ERROR("pipe2"); const int fdLifeSignR = pipe[0]; //for parent process const int fdLifeSignW = pipe[1]; //for child process ZEN_ON_SCOPE_EXIT(::close(fdLifeSignR)); auto guardFdLifeSignW = makeGuard([&] { ::close(fdLifeSignW ); }); //-------------------------------------------------------------- //follow implemenation of ::system(): https://github.com/lattera/glibc/blob/master/sysdeps/posix/system.c const pid_t pid = ::fork(); if (pid < 0) //pids are never negative, empiric proof: https://linux.die.net/man/2/wait THROW_LAST_SYS_ERROR("fork"); if (pid == 0) //child process try { //first task: set STDOUT redirection in case an error needs to be reported if (::dup2(fdTempFile, STDOUT_FILENO) != STDOUT_FILENO) //O_CLOEXEC does NOT propagate with dup2() THROW_LAST_SYS_ERROR("dup2(STDOUT)"); if (::dup2(fdTempFile, STDERR_FILENO) != STDERR_FILENO) //O_CLOEXEC does NOT propagate with dup2() THROW_LAST_SYS_ERROR("dup2(STDERR)"); //avoid blocking scripts waiting for user input // => appending " < /dev/null" is not good enough! e.g. hangs for: read -p "still hanging here"; echo fuuuuu... const int fdDevNull = ::open("/dev/null", O_RDONLY | O_CLOEXEC); if (fdDevNull == -1) //don't check "< 0" -> docu seems to allow "-2" to be a valid file handle THROW_LAST_SYS_ERROR("open(/dev/null)"); ZEN_ON_SCOPE_EXIT(::close(fdDevNull)); if (::dup2(fdDevNull, STDIN_FILENO) != STDIN_FILENO) //O_CLOEXEC does NOT propagate with dup2() THROW_LAST_SYS_ERROR("dup2(STDIN)"); //*leak* the fd and have it closed automatically on child process exit after execv() if (::dup(fdLifeSignW) == -1) //O_CLOEXEC does NOT propagate with dup() THROW_LAST_SYS_ERROR("dup(fdLifeSignW)"); std::vector argv{filePath.c_str()}; for (const Zstring& arg : arguments) argv.push_back(arg.c_str()); argv.push_back(nullptr); /*int rv =*/::execv(argv[0], const_cast(&argv[0])); //only returns if an error occurred //safe to cast away const: https://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html // "The statement about argv[] and envp[] being constants is included to make explicit to future // writers of language bindings that these objects are completely constant. Due to a limitation of // the ISO C standard, it is not possible to state that idea in standard C." THROW_LAST_SYS_ERROR("execv"); } catch (const SysError& e) { ::puts(utfTo(e.toString()).c_str()); ::fflush(stdout); //note: stderr is unbuffered by default ::_exit(EC_CHILD_LAUNCH_FAILED); //[!] avoid flushing I/O buffers or doing other clean up from child process like with "exit()"! } //else: parent process if (timeoutMs) { guardFdLifeSignW.dismiss(); ::close(fdLifeSignW); //[!] make sure we get EOF when fd is closed by child! const int flags = ::fcntl(fdLifeSignR, F_GETFL); if (flags == -1) THROW_LAST_SYS_ERROR("fcntl(F_GETFL)"); //fcntl() success: Linux: 0 // macOS: "Value other than -1." if (::fcntl(fdLifeSignR, F_SETFL, flags | O_NONBLOCK) == -1) THROW_LAST_SYS_ERROR("fcntl(F_SETFL, O_NONBLOCK)"); const auto endTime = std::chrono::steady_clock::now() + std::chrono::milliseconds(*timeoutMs); for (;;) //EINTR handling? => allow interruption!? { //read until EAGAIN char buf[16]; const ssize_t bytesRead = ::read(fdLifeSignR, buf, sizeof(buf)); if (bytesRead < 0) { if (errno != EAGAIN) THROW_LAST_SYS_ERROR("read"); } else if (bytesRead > 0) throw SysError(formatSystemError("read", L"", L"Unexpected data.")); else //bytesRead == 0: EOF break; //wait for stream input const auto now = std::chrono::steady_clock::now(); if (now > endTime) throw SysErrorTimeOut(_P("Operation timed out after 1 second.", "Operation timed out after %x seconds.", *timeoutMs / 1000)); const auto waitTimeMs = std::chrono::duration_cast(endTime - now).count(); timeval tv = {}; tv.tv_sec = static_cast(waitTimeMs / 1000); tv.tv_usec = static_cast(waitTimeMs - tv.tv_sec * 1000) * 1000; fd_set rfd = {}; //includes FD_ZERO FD_SET(fdLifeSignR, &rfd); if (const int rv = ::select(fdLifeSignR + 1, //int nfds &rfd, //fd_set* readfds nullptr, //fd_set* writefds nullptr, //fd_set* exceptfds &tv); //struct timeval* timeout rv < 0) THROW_LAST_SYS_ERROR("select"); else if (rv == 0) throw SysErrorTimeOut(_P("Operation timed out after 1 second.", "Operation timed out after %x seconds.", *timeoutMs / 1000)); } } //https://linux.die.net/man/2/waitpid int statusCode = 0; if (::waitpid(pid, //pid_t pid &statusCode, //int* status 0) != pid) //int options THROW_LAST_SYS_ERROR("waitpid"); if (::lseek(fdTempFile, 0, SEEK_SET) != 0) THROW_LAST_SYS_ERROR("lseek"); guardTmpFile.dismiss(); FileInput streamIn(fdTempFile, tempFilePath, nullptr /*notifyUnbufferedIO*/); //takes ownership! std::string output = bufferedLoad(streamIn); //throw FileError if (!WIFEXITED(statusCode)) //signalled, crashed? throw SysError(formatSystemError("waitpid", WIFSIGNALED(statusCode) ? L"Killed by signal " + numberTo(WTERMSIG(statusCode)) : L"Exit status " + numberTo(statusCode), utfTo(trimCpy(output)))); const int exitCode = WEXITSTATUS(statusCode); //precondition: "WIFEXITED() == true" if (exitCode == EC_CHILD_LAUNCH_FAILED || //child process should already have provided details to STDOUT exitCode == 127) //details should have been streamed to STDERR: used by /bin/sh, e.g. failure to execute due to missing .so file throw SysError(utfTo(trimCpy(output))); return {exitCode, output}; } } std::pair zen::consoleExecute(const Zstring& cmdLine, std::optional timeoutMs) //throw SysError, SysErrorTimeOut { const auto& [exitCode, output] = processExecuteImpl("/bin/sh", {"-c", cmdLine.c_str()}, timeoutMs); //throw SysError, SysErrorTimeOut return {exitCode, copyStringTo(output)}; } void zen::openWithDefaultApp(const Zstring& itemPath) //throw FileError { try { std::optional timeoutMs; const Zstring cmdTemplate = R"(xdg-open "%x")"; //*might* block! timeoutMs = 0; //e.g. on Lubuntu if Firefox is started and not already running => no need for time out! https://freefilesync.org/forum/viewtopic.php?t=8260 const Zstring cmdLine = replaceCpy(cmdTemplate, Zstr("%x"), itemPath); if (const auto& [exitCode, output] = consoleExecute(cmdLine, timeoutMs); //throw SysError, SysErrorTimeOut exitCode != 0) throw SysError(formatSystemError(utfTo(cmdTemplate), replaceCpy(_("Exit code %x"), L"%x", numberTo(exitCode)), utfTo(output))); } catch (SysErrorTimeOut&) {} //child process not failed yet => probably fine :> catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(itemPath)), e.toString()); } }