From d299ddd2f27a437f0fc0cb49abdfd6dd8e3d94f8 Mon Sep 17 00:00:00 2001 From: B Stack Date: Tue, 2 Feb 2021 11:44:31 -0500 Subject: add upstream 11.6 --- zen/basic_math.h | 74 ++++++++----- zen/dir_watcher.cpp | 2 +- zen/file_access.cpp | 28 +++-- zen/file_io.cpp | 4 +- zen/file_io.h | 2 + zen/format_unit.cpp | 24 ++--- zen/json.h | 9 +- zen/process_exec.cpp | 249 ++++++++++++++++++++++++++++++++++++++++++ zen/process_exec.h | 28 +++++ zen/resolve_path.cpp | 293 ++++++++++++++++++++++++++++++++++++++++++++++++++ zen/resolve_path.h | 34 ++++++ zen/shell_execute.cpp | 256 ------------------------------------------- zen/shell_execute.h | 26 ----- zen/shutdown.cpp | 16 +-- zen/socket.h | 12 +-- zen/string_tools.h | 6 +- zen/symlink_target.h | 2 +- zen/sys_error.h | 2 +- zen/sys_info.cpp | 47 +++++--- zen/sys_info.h | 5 +- zen/sys_version.cpp | 12 ++- zen/zstring.h | 8 +- 22 files changed, 759 insertions(+), 380 deletions(-) create mode 100644 zen/process_exec.cpp create mode 100644 zen/process_exec.h create mode 100644 zen/resolve_path.cpp create mode 100644 zen/resolve_path.h delete mode 100644 zen/shell_execute.cpp delete mode 100644 zen/shell_execute.h (limited to 'zen') diff --git a/zen/basic_math.h b/zen/basic_math.h index 2171ee24..a4feb83e 100644 --- a/zen/basic_math.h +++ b/zen/basic_math.h @@ -16,7 +16,6 @@ namespace numeric { -template T abs(T value); template auto dist(T a, T b); template int sign(T value); //returns one of {-1, 0, 1} template bool isNull(T value); //...definitively fishy... @@ -24,10 +23,9 @@ template bool isNull(T value); //...definitively fishy... template //precondition: range must be sorted! auto nearMatch(const T& val, InputIterator first, InputIterator last); -int64_t round(double d); //"little rounding function" - -template -auto integerDivideRoundUp(N numerator, D denominator); +template auto intDivRound(N numerator, D denominator); +template auto intDivCeil (N numerator, D denominator); +template auto intDivFloor(N numerator, D denominator); template T power(T value); @@ -66,16 +64,6 @@ double norm2(InputIterator first, InputIterator last); //################# inline implementation ######################### -template inline -T abs(T value) -{ - //static_assert(std::is_signed_v); - if (value < 0) - return -value; //operator "?:" caveat: may be different type than "value" - else - return value; -} - template inline auto dist(T a, T b) //return type might be different than T, e.g. std::chrono::duration instead of std::chrono::time_point { @@ -160,23 +148,59 @@ bool isNull(T value) } -inline -int64_t round(double d) +template inline +auto intDivRound(N num, D den) { - assert(d - 0.5 >= std::numeric_limits::min() && //if double is larger than what int can represent: - d + 0.5 <= std::numeric_limits::max()); //=> undefined behavior! + using namespace zen; + static_assert(IsInteger::value && IsInteger::value); + static_assert(IsSignedInt::value == IsSignedInt::value); //until further + assert(den != 0); + if constexpr (IsSignedInt::value) + { + if ((num < 0) != (den < 0)) + return (num - den / 2) / den; + } + return (num + den / 2) / den; +} + - return static_cast(d < 0 ? d - 0.5 : d + 0.5); +template inline +auto intDivCeil(N num, D den) +{ + using namespace zen; + static_assert(IsInteger::value && IsInteger::value); + static_assert(IsSignedInt::value == IsSignedInt::value); //until further + assert(den != 0); + if constexpr (IsSignedInt::value) + { + if ((num < 0) != (den < 0)) + return num / den; + + if (num < 0 && den < 0) + num += 2; //return (num + den + 1) / den + } + return (num + den - 1) / den; } template inline -auto integerDivideRoundUp(N numerator, D denominator) +auto intDivFloor(N num, D den) { - static_assert(zen::IsInteger::value); - static_assert(zen::IsInteger::value); - assert(numerator >= 0 && denominator > 0); - return (numerator + denominator - 1) / denominator; + using namespace zen; + static_assert(IsInteger::value && IsInteger::value); + static_assert(IsSignedInt::value == IsSignedInt::value); //until further + assert(den != 0); + if constexpr (IsSignedInt::value) + { + if ((num < 0) != (den < 0)) + { + if (num < 0) + num += 2; //return (num - den + 1) / den + + return (num - den - 1) / den; + } + } + return num / den; } diff --git a/zen/dir_watcher.cpp b/zen/dir_watcher.cpp index 62493084..dc416b34 100644 --- a/zen/dir_watcher.cpp +++ b/zen/dir_watcher.cpp @@ -9,7 +9,7 @@ #include #include "thread.h" #include "scope_guard.h" -#include "basic_math.h" +//#include "basic_math.h" #include #include diff --git a/zen/file_access.cpp b/zen/file_access.cpp index 3269bef4..4c9af652 100644 --- a/zen/file_access.cpp +++ b/zen/file_access.cpp @@ -66,8 +66,12 @@ std::optional zen::parsePathComponents(const Zstring& itemPath) pc = doParse(5 /*sepCountVolumeRoot*/, false /*rootWithSep*/); if (!pc && startsWith(itemPath, "/run/user/")) //Ubuntu, e.g.: /run/user/1000/gvfs/smb-share:server=192.168.62.145,share=folder - if (startsWith(itemPath, "/run/user/" + numberTo(::getuid()) + "/gvfs/")) //::getuid() never fails - pc = doParse(6 /*sepCountVolumeRoot*/, false /*rootWithSep*/); + { + Zstring tmp(itemPath.begin() + strLength("/run/user/"), itemPath.end()); + tmp = beforeFirst(tmp, "/gvfs/", IfNotFoundReturn::none); + if (!tmp.empty() && std::all_of(tmp.begin(), tmp.end(), [](char c) { return isDigit(c); })) + /**/pc = doParse(6 /*sepCountVolumeRoot*/, false /*rootWithSep*/); + } if (!pc && startsWith(itemPath, "/")) @@ -187,6 +191,8 @@ VolumeId zen::getVolumeId(const Zstring& itemPath) //throw FileError if (::stat(itemPath.c_str(), &fileInfo) != 0) THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(itemPath)), "stat"); + warn_static("NOT STABLE!") + return fileInfo.st_dev; } @@ -203,8 +209,8 @@ uint64_t zen::getFileSize(const Zstring& filePath) //throw FileError Zstring zen::getTempFolderPath() //throw FileError { - if (const char* buf = ::getenv("TMPDIR")) //no extended error reporting - return buf; + if (const char* tempPath = ::getenv("TMPDIR")) //no extended error reporting + return tempPath; //TMPDIR not set on CentOS 7, WTF! return P_tmpdir; //usually resolves to "/tmp" } @@ -520,19 +526,21 @@ void zen::createDirectory(const Zstring& dirPath) //throw FileError, ErrorTarget { auto getErrorMsg = [&] { return replaceCpy(_("Cannot create directory %x."), L"%x", fmtPath(dirPath)); }; - //don't allow creating irregular folders like "...." https://social.technet.microsoft.com/Forums/windows/en-US/ffee2322-bb6b-4fdf-86f9-8f93cf1fa6cb/ + //don't allow creating irregular folders! const Zstring dirName = afterLast(dirPath, FILE_NAME_SEPARATOR, IfNotFoundReturn::all); + + //e.g. "...." https://social.technet.microsoft.com/Forums/windows/en-US/ffee2322-bb6b-4fdf-86f9-8f93cf1fa6cb/ if (std::all_of(dirName.begin(), dirName.end(), [](Zchar c) { return c == Zstr('.'); })) /**/throw FileError(getErrorMsg(), replaceCpy(L"Invalid folder name %x.", L"%x", fmtPath(dirName))); #if 0 //not appreciated: https://freefilesync.org/forum/viewtopic.php?t=7509 - //not critical, but will visually confuse user sooner or later: - if (startsWith(dirName, Zstr(' ')) || - endsWith (dirName, Zstr(' '))) - throw FileError(getErrorMsg(), replaceCpy(L"Folder name %x starts/ends with space character.", L"%x", fmtPath(dirName))); + if (startsWith(dirName, Zstr(' ')) || //Windows can access these just fine once created! + endsWith (dirName, Zstr(' '))) // + throw FileError(getErrorMsg(), replaceCpy(L"Invalid folder name. %x starts/ends with space character.", L"%x", fmtPath(dirName))); #endif - const mode_t mode = S_IRWXU | S_IRWXG | S_IRWXO; //0777, default for newly created directories + + const mode_t mode = S_IRWXU | S_IRWXG | S_IRWXO; //0777 => consider umask! if (::mkdir(dirPath.c_str(), mode) != 0) { diff --git a/zen/file_io.cpp b/zen/file_io.cpp index 33c41fbc..e081335d 100644 --- a/zen/file_io.cpp +++ b/zen/file_io.cpp @@ -167,7 +167,7 @@ FileBase::FileHandle openHandleForWrite(const Zstring& filePath) //throw FileErr //checkForUnsupportedType(filePath); -> not needed, open() + O_WRONLY should fail fast const int fdFile = ::open(filePath.c_str(), O_WRONLY | O_CREAT | O_CLOEXEC | /*access == FileOutput::ACC_OVERWRITE ? O_TRUNC : */ O_EXCL, - S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); //0666 + S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); //0666 => umask will be applied implicitly! if (fdFile == -1) { const int ec = errno; //copy before making other system calls! @@ -296,7 +296,7 @@ void FileOutput::reserveSpace(uint64_t expectedSize) //throw FileError if (expectedSize < 1024) //https://www.sciencedirect.com/topics/computer-science/master-file-table return; - //don't use ::posix_fallocate! horribly inefficient if FS doesn't support it + changes file size + //don't use ::posix_fallocate which uses horribly inefficient fallback if FS doesn't support it (EOPNOTSUPP) and changes files size! //FALLOC_FL_KEEP_SIZE => allocate only, file size is NOT changed! if (::fallocate(getHandle(), //int fd, FALLOC_FL_KEEP_SIZE, //int mode, diff --git a/zen/file_io.h b/zen/file_io.h index 4210cc57..a7385241 100644 --- a/zen/file_io.h +++ b/zen/file_io.h @@ -110,6 +110,8 @@ public: void write(const void* buffer, size_t bytesToWrite) { tmpFile_.write(buffer, bytesToWrite); } //throw FileError, X + FileOutput& refTempFile() { return tmpFile_; } + void commit() //throw FileError, X { tmpFile_.finalize(); //throw FileError, X diff --git a/zen/format_unit.cpp b/zen/format_unit.cpp index 0d75a9d4..28943de7 100644 --- a/zen/format_unit.cpp +++ b/zen/format_unit.cpp @@ -26,20 +26,20 @@ using namespace zen; std::wstring zen::formatTwoDigitPrecision(double value) { //print two digits: 0,1 | 1,1 | 11 - if (numeric::abs(value) < 9.95) //9.99 must not be formatted as "10.0" + if (std::abs(value) < 9.95) //9.99 must not be formatted as "10.0" return printNumber(L"%.1f", value); - return numberTo(numeric::round(value)); + return numberTo(std::llround(value)); } std::wstring zen::formatThreeDigitPrecision(double value) { //print three digits: 0,01 | 0,11 | 1,11 | 11,1 | 111 - if (numeric::abs(value) < 9.995) //9.999 must not be formatted as "10.00" + if (std::abs(value) < 9.995) //9.999 must not be formatted as "10.00" return printNumber(L"%.2f", value); - if (numeric::abs(value) < 99.95) //99.99 must not be formatted as "100.0" + if (std::abs(value) < 99.95) //99.99 must not be formatted as "100.0" return printNumber(L"%.1f", value); - return numberTo(numeric::round(value)); + return numberTo(std::llround(value)); } @@ -47,7 +47,7 @@ std::wstring zen::formatFilesizeShort(int64_t size) { //if (size < 0) return _("Error"); -> really? - if (numeric::abs(size) <= 999) + if (std::abs(size) <= 999) return _P("1 byte", "%x bytes", static_cast(size)); double sizeInUnit = static_cast(size); @@ -55,19 +55,19 @@ std::wstring zen::formatFilesizeShort(int64_t size) auto formatUnit = [&](const std::wstring& unitTxt) { return replaceCpy(unitTxt, L"%x", formatThreeDigitPrecision(sizeInUnit)); }; sizeInUnit /= 1024; - if (numeric::abs(sizeInUnit) < 999.5) + if (std::abs(sizeInUnit) < 999.5) return formatUnit(_("%x KB")); sizeInUnit /= 1024; - if (numeric::abs(sizeInUnit) < 999.5) + if (std::abs(sizeInUnit) < 999.5) return formatUnit(_("%x MB")); sizeInUnit /= 1024; - if (numeric::abs(sizeInUnit) < 999.5) + if (std::abs(sizeInUnit) < 999.5) return formatUnit(_("%x GB")); sizeInUnit /= 1024; - if (numeric::abs(sizeInUnit) < 999.5) + if (std::abs(sizeInUnit) < 999.5) return formatUnit(_("%x TB")); sizeInUnit /= 1024; @@ -116,7 +116,7 @@ std::wstring roundToBlock(double timeInHigh, const int blockSizeLow = granularity * timeInHigh < 1 ? numeric::nearMatch(granularity * timeInLow, std::begin(stepsLow), std::end(stepsLow)): numeric::nearMatch(granularity * timeInHigh, std::begin(stepsHigh), std::end(stepsHigh)) * unitLowPerHigh; - const int roundedtimeInLow = static_cast(numeric::round(timeInLow / blockSizeLow) * blockSizeLow); + const int roundedtimeInLow = std::lround(timeInLow / blockSizeLow) * blockSizeLow; std::wstring output = formatUnitTime(roundedtimeInLow / unitLowPerHigh, unitHigh); if (unitLowPerHigh > blockSizeLow) @@ -192,7 +192,7 @@ WeekDay impl::getFirstDayOfWeekImpl() //throw SysError /* testing: change locale via command line --------------------------------------- LC_TIME=en_DK.utf8 => Monday - LC_TIME=en_US.utf8 => Sunday */ + LC_TIME=en_US.utf8 => Sunday */ const char* firstDay = ::nl_langinfo(_NL_TIME_FIRST_WEEKDAY); //[1-Sunday, 7-Saturday] ASSERT_SYSERROR(firstDay && 1 <= *firstDay && *firstDay <= 7); diff --git a/zen/json.h b/zen/json.h index 82e5e271..a3740664 100644 --- a/zen/json.h +++ b/zen/json.h @@ -95,15 +95,15 @@ namespace json_impl { namespace { -std::string jsonEscape(const std::string& str) +[[nodiscard]] std::string jsonEscape(const std::string& str) { std::string output; for (const char c : str) switch (c) { //*INDENT-OFF* - case '"': output += "\\\""; break; //escaping mandatory case '\\': output += "\\\\"; break; // + case '"': output += "\\\""; break; //escaping mandatory case '\b': output += "\\b"; break; // case '\f': output += "\\f"; break; // @@ -128,7 +128,7 @@ std::string jsonEscape(const std::string& str) } -std::string jsonUnescape(const std::string& str) +[[nodiscard]] std::string jsonUnescape(const std::string& str) { std::string output; std::basic_string utf16Buf; @@ -152,7 +152,6 @@ std::string jsonUnescape(const std::string& str) for (auto it = str.begin(); it != str.end(); ++it) { const char c = *it; - if (c == '\\') { ++it; @@ -166,8 +165,8 @@ std::string jsonUnescape(const std::string& str) switch (c2) { //*INDENT-OFF* - case '"': case '\\': + case '"': case '/': writeOut(c2); break; case 'b': writeOut('\b'); break; case 'f': writeOut('\f'); break; diff --git a/zen/process_exec.cpp b/zen/process_exec.cpp new file mode 100644 index 00000000..bbc87c51 --- /dev/null +++ b/zen/process_exec.cpp @@ -0,0 +1,249 @@ +// ***************************************************************************** +// * 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)"); + + 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(); + + struct ::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 + { + const Zstring cmdTemplate = R"(xdg-open "%x")"; //doesn't block => no need for time out! + const Zstring cmdLine = replaceCpy(cmdTemplate, Zstr("%x"), itemPath); + + if (const auto& [exitCode, output] = consoleExecute(cmdLine, std::nullopt /*timeoutMs*/); //throw SysError, (SysErrorTimeOut) + exitCode != 0) + throw SysError(formatSystemError(utfTo(cmdTemplate), + replaceCpy(_("Exit code %x"), L"%x", numberTo(exitCode)), utfTo(output))); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(itemPath)), e.toString()); } +} + + diff --git a/zen/process_exec.h b/zen/process_exec.h new file mode 100644 index 00000000..1c18c3f4 --- /dev/null +++ b/zen/process_exec.h @@ -0,0 +1,28 @@ +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef SHELL_EXECUTE_H_23482134578134134 +#define SHELL_EXECUTE_H_23482134578134134 + +#include "file_error.h" + + +namespace zen +{ +Zstring escapeCommandArg(const Zstring& arg); + + +DEFINE_NEW_SYS_ERROR(SysErrorTimeOut) +[[nodiscard]] std::pair consoleExecute(const Zstring& cmdLine, std::optional timeoutMs); //throw SysError, SysErrorTimeOut +/* Windows: - cmd.exe returns exit code 1 if file not found (instead of throwing SysError) => nodiscard! + - handles elevation when CreateProcess() would fail with ERROR_ELEVATION_REQUIRED! + - no support for UNC path and Unicode on Win7; apparently no issue on Win10! + Linux/macOS: SysErrorTimeOut leaves zombie process behind if timeoutMs is used */ + +void openWithDefaultApp(const Zstring& itemPath); //throw FileError +} + +#endif //SHELL_EXECUTE_H_23482134578134134 diff --git a/zen/resolve_path.cpp b/zen/resolve_path.cpp new file mode 100644 index 00000000..76999500 --- /dev/null +++ b/zen/resolve_path.cpp @@ -0,0 +1,293 @@ +// ***************************************************************************** +// * 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 "resolve_path.h" +//#include //not necessarily included by ! +//#include +#include "time.h" +#include "thread.h" +//#include "utf.h" +//#include "scope_guard.h" +//#include "globals.h" +#include "file_access.h" + + #include //getenv() + #include //getcwd + +using namespace zen; + + +namespace +{ +std::optional getEnvironmentVar(const Zstring& name) +{ + assert(runningOnMainThread()); //getenv() is not thread-safe! + + const char* buffer = ::getenv(name.c_str()); //no extended error reporting + if (!buffer) + return {}; + Zstring value(buffer); + + //some postprocessing: + trim(value); //remove leading, trailing blanks + + //remove leading, trailing double-quotes + if (startsWith(value, Zstr('"')) && + endsWith (value, Zstr('"')) && + value.length() >= 2) + value = Zstring(value.c_str() + 1, value.length() - 2); + + return value; +} + + +Zstring resolveRelativePath(const Zstring& relativePath) +{ + assert(runningOnMainThread()); //GetFullPathName() is documented to NOT be thread-safe! + /* MSDN: "Multithreaded applications and shared library code should not use the GetFullPathName function + and should avoid using relative path names. + The current directory state written by the SetCurrentDirectory function is stored as a global variable in each process, */ + + if (relativePath.empty()) + return relativePath; + + Zstring pathTmp = relativePath; + //https://linux.die.net/man/2/path_resolution + if (!startsWith(pathTmp, FILE_NAME_SEPARATOR)) //absolute names are exactly those starting with a '/' + { + /* basic support for '~': strictly speaking this is a shell-layer feature, so "realpath()" won't handle it + https://www.gnu.org/software/bash/manual/html_node/Tilde-Expansion.html + + https://linux.die.net/man/3/getpwuid: An application that wants to determine its user's home directory + should inspect the value of HOME (rather than the value getpwuid(getuid())->pw_dir) since this allows + the user to modify their notion of "the home directory" during a login session. */ + if (startsWith(pathTmp, "~/") || pathTmp == "~") + { + if (const std::optional homeDir = getEnvironmentVar("HOME")) + { + if (startsWith(pathTmp, "~/")) + pathTmp = appendSeparator(*homeDir) + afterFirst(pathTmp, '/', IfNotFoundReturn::none); + else //pathTmp == "~" + pathTmp = *homeDir; + } + //else: error! no further processing! + } + else + { + //we cannot use ::realpath() which only resolves *existing* relative paths! + if (char* dirPath = ::getcwd(nullptr, 0)) + { + ZEN_ON_SCOPE_EXIT(::free(dirPath)); + pathTmp = appendSeparator(dirPath) + pathTmp; + } + } + } + //get rid of some cruft (just like GetFullPathName()) + replace(pathTmp, "/./", '/'); + if (endsWith(pathTmp, "/.")) + pathTmp.pop_back(); //keep the "/" => consider pathTmp == "/." + + //what about "/../"? might be relative to symlinks => preserve! + + return pathTmp; +} + + + + +//returns value if resolved +std::optional tryResolveMacro(const Zstring& macro) //macro without %-characters +{ + Zstring timeStr; + auto resolveTimePhrase = [&](const Zchar* phrase, const Zchar* format) -> bool + { + if (!equalAsciiNoCase(macro, phrase)) + return false; + + timeStr = formatTime(format); + return true; + }; + + //https://en.cppreference.com/w/cpp/chrono/c/strftime + //there exist environment variables named %TIME%, %DATE% so check for our internal macros first! + if (resolveTimePhrase(Zstr("Date"), Zstr("%Y-%m-%d"))) return timeStr; + if (resolveTimePhrase(Zstr("Time"), Zstr("%H%M%S"))) return timeStr; + if (resolveTimePhrase(Zstr("TimeStamp"), Zstr("%Y-%m-%d %H%M%S"))) return timeStr; //e.g. "2012-05-15 131513" + if (resolveTimePhrase(Zstr("Year"), Zstr("%Y"))) return timeStr; + if (resolveTimePhrase(Zstr("Month"), Zstr("%m"))) return timeStr; + if (resolveTimePhrase(Zstr("MonthName"), Zstr("%b"))) return timeStr; //e.g. "Jan" + if (resolveTimePhrase(Zstr("Day"), Zstr("%d"))) return timeStr; + if (resolveTimePhrase(Zstr("Hour"), Zstr("%H"))) return timeStr; + if (resolveTimePhrase(Zstr("Min"), Zstr("%M"))) return timeStr; + if (resolveTimePhrase(Zstr("Sec"), Zstr("%S"))) return timeStr; + if (resolveTimePhrase(Zstr("WeekDayName"), Zstr("%a"))) return timeStr; //e.g. "Mon" + if (resolveTimePhrase(Zstr("Week"), Zstr("%V"))) return timeStr; //ISO 8601 week of the year + + if (equalAsciiNoCase(macro, Zstr("WeekDay"))) + { + const int weekDayStartSunday = stringTo(formatTime(Zstr("%w"))); //[0 (Sunday), 6 (Saturday)] => not localized! + //alternative 1: use "%u": ISO 8601 weekday as number with Monday as 1 (1-7) => newer standard than %w + //alternative 2: ::mktime() + std::tm::tm_wday + + const int weekDayStartMonday = (weekDayStartSunday + 6) % 7; //+6 == -1 in Z_7 + // [0-Monday, 6-Sunday] + + const int weekDayStartLocal = ((weekDayStartMonday + 7 - static_cast(getFirstDayOfWeek())) % 7) + 1; + //[1 (local first day of week), 7 (local last day of week)] + + return numberTo(weekDayStartLocal); + } + + //try to resolve as environment variables + if (std::optional value = getEnvironmentVar(macro)) + return *value; + + + return {}; +} + +const Zchar MACRO_SEP = Zstr('%'); +} + + +//returns expanded or original string +Zstring zen::expandMacros(const Zstring& text) +{ + if (contains(text, MACRO_SEP)) + { + Zstring prefix = beforeFirst(text, MACRO_SEP, IfNotFoundReturn::none); + Zstring rest = afterFirst (text, MACRO_SEP, IfNotFoundReturn::none); + if (contains(rest, MACRO_SEP)) + { + Zstring potentialMacro = beforeFirst(rest, MACRO_SEP, IfNotFoundReturn::none); + Zstring postfix = afterFirst (rest, MACRO_SEP, IfNotFoundReturn::none); //text == prefix + MACRO_SEP + potentialMacro + MACRO_SEP + postfix + + if (std::optional value = tryResolveMacro(potentialMacro)) + return prefix + *value + expandMacros(postfix); + else + return prefix + MACRO_SEP + potentialMacro + expandMacros(MACRO_SEP + postfix); + } + } + return text; +} + + +namespace +{ + + +//expand volume name if possible, return original input otherwise +Zstring expandVolumeName(Zstring pathPhrase) // [volname]:\folder [volname]\folder [volname]folder -> C:\folder +{ + //we only expect the [.*] pattern at the beginning => do not touch dir names like "C:\somedir\[stuff]" + trim(pathPhrase, true, false); + if (startsWith(pathPhrase, Zstr('['))) + { + const size_t posEnd = pathPhrase.find(Zstr(']')); + if (posEnd != Zstring::npos) + { + Zstring volName = Zstring(pathPhrase.c_str() + 1, posEnd - 1); + Zstring relPath = Zstring(pathPhrase.c_str() + posEnd + 1); + + if (startsWith(relPath, FILE_NAME_SEPARATOR)) + relPath = afterFirst(relPath, FILE_NAME_SEPARATOR, IfNotFoundReturn::none); + else if (startsWith(relPath, Zstr(":\\"))) //Win-only + relPath = afterFirst(relPath, Zstr('\\'), IfNotFoundReturn::none); + return "/.../[" + volName + "]/" + relPath; + } + } + return pathPhrase; +} + + +void getFolderAliasesRecursive(const Zstring& pathPhrase, std::set& output) +{ + + //3. environment variables: C:\Users\ -> %UserProfile% + { + std::vector> macroList; + + //get list of useful variables + auto addEnvVar = [&](const Zstring& envName) + { + if (std::optional value = getEnvironmentVar(envName)) + macroList.emplace_back(envName, *value); + }; + addEnvVar("HOME"); //Linux: /home/ Mac: /Users/ + //addEnvVar("USER"); -> any benefit? + //substitute paths by symbolic names + for (const auto& [macroName, macroPath] : macroList) + { + //should use a replaceCpy() that considers "local path" case-sensitivity (if only we had one...) + const Zstring pathSubst = replaceCpyAsciiNoCase(pathPhrase, macroPath, MACRO_SEP + macroName + MACRO_SEP); + if (pathSubst != pathPhrase) + output.insert(pathSubst); + } + } + + //4. replace (all) macros: %UserProfile% -> C:\Users\ + { + const Zstring pathExp = expandMacros(pathPhrase); + if (pathExp != pathPhrase) + if (output.insert(pathExp).second) + getFolderAliasesRecursive(pathExp, output); //recurse! + } +} +} + + +std::vector zen::getFolderPathAliases(const Zstring& folderPathPhrase) +{ + const Zstring dirPath = trimCpy(folderPathPhrase); + if (dirPath.empty()) + return {}; + + std::set tmp; + getFolderAliasesRecursive(dirPath, tmp); + + tmp.erase(dirPath); + tmp.erase(Zstring()); + + return { tmp.begin(), tmp.end() }; +} + + +//coordinate changes with acceptsFolderPathPhraseNative()! +Zstring zen::getResolvedFilePath(const Zstring& pathPhrase) //noexcept +{ + Zstring path = pathPhrase; + + path = expandMacros(path); //expand before trimming! + + //remove leading/trailing whitespace before allowing misinterpretation in applyLongPathPrefix() + trim(path); //attention: don't remove all whitespace from right, e.g. 0xa0 may be used as part of a folder name + + + path = expandVolumeName(path); //may block for slow USB sticks and idle HDDs! + + /* need to resolve relative paths: + WINDOWS: + - \\?\-prefix requires absolute names + - Volume Shadow Copy: volume name needs to be part of each file path + - file icon buffer (at least for extensions that are actually read from disk, like "exe") + - Use of relative path names is not thread safe! (e.g. SHFileOperation) + WINDOWS/LINUX: + - detection of dependent directories, e.g. "\" and "C:\test" */ + path = resolveRelativePath(path); + + //remove trailing slash, unless volume root: + if (std::optional pc = parsePathComponents(path)) + { + if (pc->relPath.empty()) + path = pc->rootPath; + else + path = appendSeparator(pc->rootPath) + pc->relPath; + } //keep this brace for GCC: -Wparentheses + + return path; +} + + diff --git a/zen/resolve_path.h b/zen/resolve_path.h new file mode 100644 index 00000000..f2c427f1 --- /dev/null +++ b/zen/resolve_path.h @@ -0,0 +1,34 @@ +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef RESOLVE_PATH_H_817402834713454 +#define RESOLVE_PATH_H_817402834713454 + +#include +#include "zstring.h" + + +namespace zen +{ +/* + - expand macros + - trim whitespace + - expand volume path by name + - convert relative paths into absolute + + => may block for slow USB sticks and idle HDDs + => not thread-safe, see ::GetFullPathName()! +*/ +Zstring getResolvedFilePath(const Zstring& pathPhrase); //noexcept + +//macro substitution only +Zstring expandMacros(const Zstring& text); + +std::vector getFolderPathAliases(const Zstring& folderPathPhrase); //may block for slow USB sticks when resolving [] + +} + +#endif //RESOLVE_PATH_H_817402834713454 diff --git a/zen/shell_execute.cpp b/zen/shell_execute.cpp deleted file mode 100644 index 241b9786..00000000 --- a/zen/shell_execute.cpp +++ /dev/null @@ -1,256 +0,0 @@ -// ***************************************************************************** -// * 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 "shell_execute.h" -#include -#include "guid.h" -#include "file_access.h" -#include "file_io.h" - - #include //fork, pipe - #include //waitpid - #include - -using namespace zen; - - -std::vector zen::parseCommandline(const Zstring& cmdLine) -{ - std::vector args; - //"Parsing C++ Command-Line Arguments": https://docs.microsoft.com/en-us/cpp/cpp/parsing-cpp-command-line-arguments - //we do the job ourselves! both wxWidgets and ::CommandLineToArgvW() parse "C:\" "D:\" as single line C:\" D:\" - //-> "solution": we just don't support protected quotation mark! - - auto itStart = cmdLine.end(); //end() means: no token - for (auto it = cmdLine.begin(); it != cmdLine.end(); ++it) - if (*it == Zstr(' ')) //space commits token - { - if (itStart != cmdLine.end()) - { - args.emplace_back(itStart, it); - itStart = cmdLine.end(); //expect consecutive blanks! - } - } - else - { - //start new token - if (itStart == cmdLine.end()) - itStart = it; - - if (*it == Zstr('"')) - { - it = std::find(it + 1, cmdLine.end(), Zstr('"')); - if (it == cmdLine.end()) - break; - } - } - if (itStart != cmdLine.end()) - args.emplace_back(itStart, cmdLine.end()); - - for (Zstring& str : args) - if (str.size() >= 2 && startsWith(str, Zstr('"')) && endsWith(str, Zstr('"'))) - str = Zstring(str.c_str() + 1, str.size() - 2); - - return args; -} - - - - -std::pair zen::consoleExecute(const Zstring& cmdLine, 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)"); - - - const char* argv[] = { "sh", "-c", cmdLine.c_str(), nullptr }; - /*int rv =*/::execv("/bin/sh", const_cast(argv)); //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)"); - - 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(); - - struct ::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! - const std::wstring output = utfTo(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 }; -} - - -void zen::openWithDefaultApp(const Zstring& itemPath) //throw FileError -{ - try - { - const Zstring cmdTemplate = R"(xdg-open "%x")"; //doesn't block => no need for time out! - const Zstring cmdLine = replaceCpy(cmdTemplate, Zstr("%x"), itemPath); - - if (const auto& [exitCode, output] = consoleExecute(cmdLine, std::nullopt /*timeoutMs*/); //throw SysError, (SysErrorTimeOut) - exitCode != 0) - throw SysError(formatSystemError(utfTo(cmdTemplate), replaceCpy(_("Exit code %x"), L"%x", numberTo(exitCode)), output)); - } - catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(itemPath)), e.toString()); } -} - - diff --git a/zen/shell_execute.h b/zen/shell_execute.h deleted file mode 100644 index b80cf2ba..00000000 --- a/zen/shell_execute.h +++ /dev/null @@ -1,26 +0,0 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef SHELL_EXECUTE_H_23482134578134134 -#define SHELL_EXECUTE_H_23482134578134134 - -#include "file_error.h" - - -namespace zen -{ -std::vector parseCommandline(const Zstring& cmdLine); - - -DEFINE_NEW_SYS_ERROR(SysErrorTimeOut) -[[nodiscard]] std::pair consoleExecute(const Zstring& cmdLine, std::optional timeoutMs); //throw SysError, SysErrorTimeOut -/* limitations: Windows: cmd.exe returns exit code 1 if file not found (instead of throwing SysError) => nodiscard! - Linux/macOS: SysErrorTimeOut leaves zombie process behind */ - -void openWithDefaultApp(const Zstring& itemPath); //throw FileError -} - -#endif //SHELL_EXECUTE_H_23482134578134134 diff --git a/zen/shutdown.cpp b/zen/shutdown.cpp index 8e8456df..f46caf6b 100644 --- a/zen/shutdown.cpp +++ b/zen/shutdown.cpp @@ -5,7 +5,7 @@ // ***************************************************************************** #include "shutdown.h" - #include + #include using namespace zen; @@ -19,9 +19,10 @@ void zen::shutdownSystem() //throw FileError { //https://linux.die.net/man/2/reboot => needs admin rights! //"systemctl" should work without admin rights: - const auto& [exitCode, output] = consoleExecute("systemctl poweroff", std::nullopt /*timeoutMs*/); //throw SysError, (SysErrorTimeOut) - if (!trimCpy(output).empty()) //see comment in suspendSystem() - throw SysError(output); + auto [exitCode, output] = consoleExecute("systemctl poweroff", std::nullopt /*timeoutMs*/); //throw SysError, (SysErrorTimeOut) + trim(output); + if (!output.empty()) //see comment in suspendSystem() + throw SysError(utfTo(output)); } catch (const SysError& e) { throw FileError(_("Unable to shut down the system."), e.toString()); } @@ -33,10 +34,11 @@ void zen::suspendSystem() //throw FileError try { //"systemctl" should work without admin rights: - const auto& [exitCode, output] = consoleExecute("systemctl suspend", std::nullopt /*timeoutMs*/); //throw SysError, (SysErrorTimeOut) + auto [exitCode, output] = consoleExecute("systemctl suspend", std::nullopt /*timeoutMs*/); //throw SysError, (SysErrorTimeOut) + trim(output); //why does "systemctl suspend" return exit code 1 despite apparent success!?? - if (!trimCpy(output).empty()) //at least we can assume "no output" on success - throw SysError(output); + if (!output.empty()) //at least we can assume "no output" on success + throw SysError(utfTo(output)); } catch (const SysError& e) { throw FileError(_("Unable to shut down the system."), e.toString()); } diff --git a/zen/socket.h b/zen/socket.h index 62386801..2dd1ff4c 100644 --- a/zen/socket.h +++ b/zen/socket.h @@ -94,9 +94,9 @@ size_t tryReadSocket(SocketType socket, void* buffer, size_t bytesToRead) //thro int bytesReceived = 0; for (;;) { - bytesReceived = ::recv(socket, //_In_ SOCKET s, - static_cast(buffer), //_Out_ char *buf, - static_cast(bytesToRead), //_In_ int len, + bytesReceived = ::recv(socket, //_In_ SOCKET s + static_cast(buffer), //_Out_ char* buf + static_cast(bytesToRead), //_In_ int len 0); //_In_ int flags if (bytesReceived >= 0 || errno != EINTR) break; @@ -119,9 +119,9 @@ size_t tryWriteSocket(SocketType socket, const void* buffer, size_t bytesToWrite int bytesWritten = 0; for (;;) { - bytesWritten = ::send(socket, //_In_ SOCKET s, - static_cast(buffer), //_In_ const char *buf, - static_cast(bytesToWrite), //_In_ int len, + bytesWritten = ::send(socket, //_In_ SOCKET s + static_cast(buffer), //_In_ const char* buf + static_cast(bytesToWrite), //_In_ int len 0); //_In_ int flags if (bytesWritten >= 0 || errno != EINTR) break; diff --git a/zen/string_tools.h b/zen/string_tools.h index 83d87244..883c45b8 100644 --- a/zen/string_tools.h +++ b/zen/string_tools.h @@ -70,12 +70,12 @@ enum class SplitOnEmpty }; template std::vector split(const S& str, const T& delimiter, SplitOnEmpty soe); -template S trimCpy(S str, bool fromLeft = true, bool fromRight = true); +template [[nodiscard]] S trimCpy(S str, bool fromLeft = true, bool fromRight = true); template void trim (S& str, bool fromLeft = true, bool fromRight = true); template void trim(S& str, bool fromLeft, bool fromRight, Function trimThisChar); -template void replace (S& str, const T& oldTerm, const U& newTerm, bool replaceAll = true); -template S replaceCpy(S str, const T& oldTerm, const U& newTerm, bool replaceAll = true); +template [[nodiscard]] S replaceCpy(S str, const T& oldTerm, const U& newTerm, bool replaceAll = true); +template void replace (S& str, const T& oldTerm, const U& newTerm, bool replaceAll = true); //high-performance conversion between numbers and strings template S numberTo(const Num& number); diff --git a/zen/symlink_target.h b/zen/symlink_target.h index 59003284..42010fd2 100644 --- a/zen/symlink_target.h +++ b/zen/symlink_target.h @@ -48,7 +48,7 @@ zen::SymlinkRawContent getSymlinkRawContent_impl(const Zstring& linkPath) //thro const ssize_t bytesWritten = ::readlink(linkPath.c_str(), &buffer[0], BUFFER_SIZE); if (bytesWritten < 0) THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(linkPath)), "readlink"); - if (bytesWritten >= static_cast(BUFFER_SIZE)) //detect truncation, not an error for readlink! + if (bytesWritten >= static_cast(BUFFER_SIZE)) //detect truncation; not an error for readlink! throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(linkPath)), formatSystemError("readlink", L"", L"Buffer truncated.")); return {Zstring(&buffer[0], bytesWritten)}; //readlink does not append 0-termination! diff --git a/zen/sys_error.h b/zen/sys_error.h index f4b867be..99cf9316 100644 --- a/zen/sys_error.h +++ b/zen/sys_error.h @@ -43,7 +43,7 @@ private: #define THROW_LAST_SYS_ERROR(functionName) \ - do { const ErrorCode ecInternal = getLastError(); throw SysError(formatSystemError(functionName, ecInternal)); } while (false) + do { const ErrorCode ecInternal = getLastError(); throw zen::SysError(formatSystemError(functionName, ecInternal)); } while (false) /* Example: ASSERT_SYSERROR(expr); diff --git a/zen/sys_info.cpp b/zen/sys_info.cpp index 70658a68..f6045f7e 100644 --- a/zen/sys_info.cpp +++ b/zen/sys_info.cpp @@ -8,14 +8,15 @@ #include "crc.h" #include "file_access.h" #include "sys_version.h" +#include "symlink_target.h" - #include "symlink_target.h" #include "file_io.h" #include #include //IFF_LOOPBACK #include //sockaddr_ll + #include "process_exec.h" #include //getuid() #include //getpwuid_r() @@ -24,7 +25,7 @@ using namespace zen; std::wstring zen::getUserName() //throw FileError { - const uid_t userIdNo = ::getuid(); //never fails + const uid_t userIdNo = ::getuid(); //"real user ID"; never fails std::vector buffer(std::max(10000, ::sysconf(_SC_GETPW_R_SIZE_MAX))); //::sysconf may return long(-1) struct passwd buffer2 = {}; @@ -110,26 +111,38 @@ std::wstring zen::getOsDescription() //throw FileError -Zstring zen::getDesktopPath() //throw FileError +Zstring zen::getRealProcessPath() //throw FileError { - try - { - const char* path = ::getenv("HOME"); //no extended error reporting - if (!path) - throw SysError(L"Cannot find HOME environment variable."); - - return appendSeparator(path) + "Desktop"; - } - catch (const SysError& e) - { - throw FileError(_("Cannot get process information."), e.toString() ); - } + return getSymlinkRawContent("/proc/self/exe").targetPath; //throw FileError + //path does not contain symlinks => no need for ::realpath() } -Zstring zen::getProcessPath() //throw FileError +Zstring zen::getUserDownloadsPath() //throw FileError { - return getSymlinkRawContent("/proc/self/exe").targetPath; //throw FileError + try + { + Zstring cmdLine; + if (getuid() == 0) //nofail; root(0) => consider as request for elevation, NOT impersonation + { + const char* loginUser = getlogin(); //https://linux.die.net/man/3/getlogin + if (!loginUser) + THROW_LAST_SYS_ERROR("getlogin"); + + cmdLine = Zstring("sudo -u ") + loginUser + " xdg-user-dir DOWNLOAD"; //sudo better be installed :> + } + else + cmdLine = "xdg-user-dir DOWNLOAD"; + + const auto& [exitCode, output] = consoleExecute(cmdLine, std::nullopt /*timeoutMs*/); //throw SysError + if (exitCode != 0) + throw SysError(formatSystemError(cmdLine.c_str(), + replaceCpy(_("Exit code %x"), L"%x", numberTo(exitCode)), utfTo(output))); + const Zstring& downloadsPath = trimCpy(output); + ASSERT_SYSERROR(!downloadsPath.empty()); + return downloadsPath; + } + catch (const SysError& e) { throw FileError(_("Cannot get process information."), e.toString()); } } diff --git a/zen/sys_info.h b/zen/sys_info.h index 9cd328bb..1b046fb6 100644 --- a/zen/sys_info.h +++ b/zen/sys_info.h @@ -28,8 +28,9 @@ ComputerModel getComputerModel(); //throw FileError std::wstring getOsDescription(); //throw FileError -Zstring getDesktopPath(); //throw FileError -Zstring getProcessPath(); //throw FileError +Zstring getRealProcessPath(); //throw FileError + +Zstring getUserDownloadsPath(); //throw FileError } diff --git a/zen/sys_version.cpp b/zen/sys_version.cpp index d07bbc33..f7e4ffc8 100644 --- a/zen/sys_version.cpp +++ b/zen/sys_version.cpp @@ -7,7 +7,7 @@ #include "sys_version.h" #include #include "file_io.h" - #include "shell_execute.h" + #include "process_exec.h" using namespace zen; @@ -25,15 +25,17 @@ OsVersionDetail zen::getOsVersionDetail() //throw SysError { if (const auto [exitCode, output] = consoleExecute("lsb_release --id -s", std::nullopt); //throw SysError exitCode != 0) - throw SysError(formatSystemError("lsb_release --id", replaceCpy(_("Exit code %x"), L"%x", numberTo(exitCode)), output)); + throw SysError(formatSystemError("lsb_release --id", + replaceCpy(_("Exit code %x"), L"%x", numberTo(exitCode)), utfTo(output))); else - osName = trimCpy(output); + osName = utfTo(trimCpy(output)); if (const auto [exitCode, output] = consoleExecute("lsb_release --release -s", std::nullopt); //throw SysError exitCode != 0) - throw SysError(formatSystemError("lsb_release --release", replaceCpy(_("Exit code %x"), L"%x", numberTo(exitCode)), output)); + throw SysError(formatSystemError("lsb_release --release", + replaceCpy(_("Exit code %x"), L"%x", numberTo(exitCode)), utfTo(output))); else - osVersion = trimCpy(output); + osVersion = utfTo(trimCpy(output)); } //lsb_release not available on some systems: https://freefilesync.org/forum/viewtopic.php?t=7191 catch (SysError&) // => fall back to /etc/os-release: https://www.freedesktop.org/software/systemd/man/os-release.html diff --git a/zen/zstring.h b/zen/zstring.h index 234a398d..de90c324 100644 --- a/zen/zstring.h +++ b/zen/zstring.h @@ -140,12 +140,18 @@ const wchar_t EM_DASH = L'\u2014'; const wchar_t EN_DASH = L'\u2013'; const wchar_t* const SPACED_DASH = L" \u2013 "; //using 'EN DASH' const wchar_t LTR_MARK = L'\u200E'; //UTF-8: E2 80 8E -const wchar_t RTL_MARK = L'\u200F'; //UTF-8: E2 80 8F const wchar_t* const ELLIPSIS = L"\u2026"; //"..." const wchar_t MULT_SIGN = L'\u00D7'; //fancy "x" //const wchar_t NOBREAK_SPACE = L'\u00A0'; const wchar_t ZERO_WIDTH_SPACE = L'\u200B'; +const wchar_t RTL_MARK = L'\u200F'; //UTF-8: E2 80 8F https://www.w3.org/International/questions/qa-bidi-unicode-controls +const wchar_t BIDI_DIR_ISOLATE_RTL = L'\u2067'; //UTF-8: E2 81 A7 => not working on Win 10 +const wchar_t BIDI_POP_DIR_ISOLATE = L'\u2069'; //UTF-8: E2 81 A9 => not working on Win 10 +const wchar_t BIDI_DIR_EMBEDDING_RTL = L'\u202B'; //UTF-8: E2 80 AB => not working on Win 10 +const wchar_t BIDI_POP_DIR_FORMATTING = L'\u202C'; //UTF-8: E2 80 AC => not working on Win 10 + + //--------------------------------------------------------------------------- -- cgit