From ac1422c22145eaed6d251b73c03f7e9fa36e95cc Mon Sep 17 00:00:00 2001 From: SirLynix Date: Mon, 22 Jan 2024 23:17:12 +0100 Subject: [PATCH] Core: Add initial process support (Process::SpawnDetached) --- include/Nazara/Core/Process.hpp | 37 ++++++++ include/Nazara/Core/Process.inl | 11 +++ include/Nazara/Core/StringExt.hpp | 5 +- include/Nazara/Core/StringExt.inl | 23 ++++- src/Nazara/Core/Posix/PosixUtils.cpp | 97 ++++++++++++++++++++ src/Nazara/Core/Posix/PosixUtils.hpp | 46 ++++++++++ src/Nazara/Core/Posix/PosixUtils.inl | 26 ++++++ src/Nazara/Core/Posix/ProcessImpl.cpp | 106 ++++++++++++++++++++++ src/Nazara/Core/Posix/ProcessImpl.hpp | 18 ++++ src/Nazara/Core/Process.cpp | 23 +++++ src/Nazara/Core/StringExt.cpp | 46 +++++++++- src/Nazara/Core/Win32/ProcessImpl.cpp | 125 ++++++++++++++++++++++++++ src/Nazara/Core/Win32/ProcessImpl.hpp | 18 ++++ 13 files changed, 575 insertions(+), 6 deletions(-) create mode 100644 include/Nazara/Core/Process.hpp create mode 100644 include/Nazara/Core/Process.inl create mode 100644 src/Nazara/Core/Posix/PosixUtils.cpp create mode 100644 src/Nazara/Core/Posix/PosixUtils.hpp create mode 100644 src/Nazara/Core/Posix/PosixUtils.inl create mode 100644 src/Nazara/Core/Posix/ProcessImpl.cpp create mode 100644 src/Nazara/Core/Posix/ProcessImpl.hpp create mode 100644 src/Nazara/Core/Process.cpp create mode 100644 src/Nazara/Core/Win32/ProcessImpl.cpp create mode 100644 src/Nazara/Core/Win32/ProcessImpl.hpp diff --git a/include/Nazara/Core/Process.hpp b/include/Nazara/Core/Process.hpp new file mode 100644 index 000000000..e2fb35cd0 --- /dev/null +++ b/include/Nazara/Core/Process.hpp @@ -0,0 +1,37 @@ +// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Core module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_CORE_PROCESS_HPP +#define NAZARA_CORE_PROCESS_HPP + +#include +#include +#include +#include +#include +#include + +namespace Nz +{ + using Pid = UInt32; + + class NAZARA_CORE_API Process + { + public: + Process() = default; + Process(const Process&) = delete; + Process(Process&&) = delete; + ~Process() = default; + + Process& operator=(const Process&) = delete; + Process& operator=(Process&&) = delete; + + static Result SpawnDetached(const std::filesystem::path& program, std::span arguments = {}, const std::filesystem::path& workingDirectory = {}); }; +} + +#include + +#endif // NAZARA_CORE_PROCESS_HPP diff --git a/include/Nazara/Core/Process.inl b/include/Nazara/Core/Process.inl new file mode 100644 index 000000000..a8eacde68 --- /dev/null +++ b/include/Nazara/Core/Process.inl @@ -0,0 +1,11 @@ +// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Core module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include + +namespace Nz +{ +} + +#include diff --git a/include/Nazara/Core/StringExt.hpp b/include/Nazara/Core/StringExt.hpp index 5f80ffa23..eb337224f 100644 --- a/include/Nazara/Core/StringExt.hpp +++ b/include/Nazara/Core/StringExt.hpp @@ -36,6 +36,7 @@ namespace Nz inline bool IsNumber(std::string_view str); NAZARA_CORE_API void IterateOnCodepoints(std::string_view str, FunctionRef callback); + NAZARA_CORE_API void IterateOnWideChars(std::string_view str, FunctionRef callback); NAZARA_CORE_API bool MatchPattern(std::string_view str, std::string_view pattern); @@ -43,7 +44,9 @@ namespace Nz NAZARA_CORE_API std::string PointerToString(const void* ptr); - inline std::string& ReplaceStr(std::string& str, std::string_view from, std::string_view to); + template std::basic_string& ReplaceStr(std::basic_string& str, T from, T to); + template std::basic_string& ReplaceStr(std::basic_string& str, const T* from, const T* to); + template std::basic_string& ReplaceStr(std::basic_string& str, std::basic_string_view from, std::basic_string_view to); inline bool StartsWith(std::string_view str, std::string_view s); NAZARA_CORE_API bool StartsWith(std::string_view lhs, std::string_view rhs, CaseIndependent); diff --git a/include/Nazara/Core/StringExt.inl b/include/Nazara/Core/StringExt.inl index dd6ecef7c..df3b16019 100644 --- a/include/Nazara/Core/StringExt.inl +++ b/include/Nazara/Core/StringExt.inl @@ -77,11 +77,28 @@ namespace Nz return str; } - inline std::string& ReplaceStr(std::string& str, std::string_view from, std::string_view to) + template + std::basic_string& ReplaceStr(std::basic_string& str, T from, T to) { - if (str.empty()) - return str; + std::size_t startPos = 0; + while ((startPos = str.find(from, startPos)) != std::string::npos) + { + str[startPos] = to; + startPos++; + } + return str; + } + + template + std::basic_string& ReplaceStr(std::basic_string& str, const T* from, const T* to) + { + return ReplaceStr(str, std::basic_string_view(from), std::basic_string_view(to)); + } + + template + std::basic_string& ReplaceStr(std::basic_string& str, std::basic_string_view from, std::basic_string_view to) + { std::size_t startPos = 0; while ((startPos = str.find(from, startPos)) != std::string::npos) { diff --git a/src/Nazara/Core/Posix/PosixUtils.cpp b/src/Nazara/Core/Posix/PosixUtils.cpp new file mode 100644 index 000000000..7bf84f354 --- /dev/null +++ b/src/Nazara/Core/Posix/PosixUtils.cpp @@ -0,0 +1,97 @@ +// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Core module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include +#include +#include +#include +#include + +namespace Nz::PlatformImpl +{ + int SafeClose(int fd) + { +#if defined(NAZARA_PLATFORM_LINUX) || defined(NAZARA_PLATFORM_ANDROID) + // Retrying close on Linux is dangerous + // https://android.googlesource.com/platform/bionic/+/master/docs/EINTR.md + // https://lwn.net/Articles/576478/ + return ::close(fd); +#else + int ret; + do + { + ret = ::close(fd); + } + while (ret != -1 || errno == EINTR); + + return ret; +#endif + } + + ssize_t SafeRead(int fd, void* buf, size_t count) + { + ssize_t ret; + do + { + ret = ::read(fd, buf, count); + } + while (ret != -1 || errno == EINTR); + + return ret; + } + + ssize_t SafeWrite(int fd, const void* buf, size_t count) + { + ssize_t ret; + do + { + ret = ::write(fd, buf, count); + } + while (ret != -1 || errno == EINTR); + + return ret; + } + + Pipe::Pipe(int flags) : + m_readFd(-1), + m_writeFd(-1) + { + int fds[2]; + if (::pipe2(fds, flags & O_CLOEXEC) != 0) + return; + + m_readFd = fds[0]; + m_writeFd = fds[1]; + } + + Pipe::Pipe(Pipe&& other) noexcept : + m_readFd(std::exchange(other.m_readFd, -1)), + m_writeFd(std::exchange(other.m_writeFd, -1)) + { + } + + Pipe::~Pipe() + { + if (m_readFd != -1) + { + assert(m_writeFd != -1); + SafeClose(m_readFd); + SafeClose(m_writeFd); + } + } + + Pipe& Pipe::operator=(Pipe&& other) noexcept + { + m_readFd = std::exchange(other.m_readFd, -1); + m_writeFd = std::exchange(other.m_writeFd, -1); + + return *this; + } + + Pipe::operator bool() const + { + return m_readFd != -1; + } +} diff --git a/src/Nazara/Core/Posix/PosixUtils.hpp b/src/Nazara/Core/Posix/PosixUtils.hpp new file mode 100644 index 000000000..ab9d91251 --- /dev/null +++ b/src/Nazara/Core/Posix/PosixUtils.hpp @@ -0,0 +1,46 @@ +// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Core module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_CORE_POSIX_POSIXUTILS_HPP +#define NAZARA_CORE_POSIX_POSIXUTILS_HPP + +#include +#include + +namespace Nz::PlatformImpl +{ + int SafeClose(int fd); + ssize_t SafeRead(int fd, void* buf, size_t count); + ssize_t SafeWrite(int fd, const void* buf, size_t count); + + class Pipe + { + public: + Pipe(int flags = 0); + Pipe(const Pipe&) = delete; + Pipe(Pipe&& other) noexcept; + ~Pipe(); + + inline int GetReadFd(); + inline int GetWriteFd(); + + inline ssize_t Read(void* buf, size_t count); + inline ssize_t Write(const void* buf, size_t count); + + Pipe& operator=(const Pipe&) = delete; + Pipe& operator=(Pipe&& other) noexcept; + + explicit operator bool() const; + + private: + int m_readFd; + int m_writeFd; + }; +} + +#include + +#endif // NAZARA_CORE_POSIX_POSIXUTILS_HPP diff --git a/src/Nazara/Core/Posix/PosixUtils.inl b/src/Nazara/Core/Posix/PosixUtils.inl new file mode 100644 index 000000000..443dbee4f --- /dev/null +++ b/src/Nazara/Core/Posix/PosixUtils.inl @@ -0,0 +1,26 @@ +// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Core module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +namespace Nz::PlatformImpl +{ + inline int Pipe::GetReadFd() + { + return m_readFd; + } + + inline int Pipe::GetWriteFd() + { + return m_writeFd; + } + + inline ssize_t Pipe::Read(void* buf, size_t count) + { + return SafeRead(m_readFd, buf, count); + } + + inline ssize_t Pipe::Write(const void* buf, size_t count) + { + return SafeWrite(m_writeFd, buf, count); + } +} diff --git a/src/Nazara/Core/Posix/ProcessImpl.cpp b/src/Nazara/Core/Posix/ProcessImpl.cpp new file mode 100644 index 000000000..604dff065 --- /dev/null +++ b/src/Nazara/Core/Posix/ProcessImpl.cpp @@ -0,0 +1,106 @@ +// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Core module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Nz::PlatformImpl +{ + Result SpawnDetachedProcess(const std::filesystem::path& program, std::span arguments, const std::filesystem::path& workingDirectory) + { + struct PidOrErr + { + pid_t pid; + int err; + }; + + Pipe pipe; + if (!pipe) + return Err("failed to create pipe: " + Error::GetLastSystemError()); + + // Double fork (see https://0xjet.github.io/3OHA/2022/04/11/post.html) + // We will create a child and a grand-child process, using a pipe to retrieve the grand-child pid + pid_t childPid = ::fork(); + if (childPid == -1) + return Err("failed to create child: " + Error::GetLastSystemError()); + + if (childPid == 0) + { + // Child process + ::setsid(); + + pid_t grandChildPid = ::vfork(); + if (grandChildPid == 0) + { + // Grand-child process + StackArray argv = NazaraStackArrayNoInit(char*, arguments.size() + 2); + + // It's safe to const_cast here as we're using a copy of the memory (from child) from the original process + argv[0] = const_cast(program.c_str()); + for (std::size_t i = 0; i < arguments.size(); ++i) + argv[i + 1] = const_cast(arguments[i].data()); + + argv[argv.size() - 1] = nullptr; + + char* envs[] = { nullptr }; + + if (!workingDirectory.empty()) + ::chdir(workingDirectory.c_str()); + + if (::execve(program.c_str(), argv.data(), envs) == -1) + { + PidOrErr err; + err.pid = -1; + err.err = errno; + + pipe.Write(&err, sizeof(err)); + } + } + else if (grandChildPid != -1) + { + PidOrErr err; + err.pid = grandChildPid; + + pipe.Write(&err, sizeof(err)); + } + else + { + PidOrErr err; + err.pid = -1; + err.err = errno; + + pipe.Write(&err, sizeof(err)); + } + + // Exits the child process, at this point the grand-child should have started + std::exit(0); + } + + // Parent process + + // Wait for and reap the child + int childStatus; + ::waitpid(childPid, &childStatus, 0); + + PidOrErr pidOrErr; + if (pipe.Read(&pidOrErr, sizeof(pidOrErr) != sizeof(pidOrErr))) + { + // this should never happen + return Err("failed to create child: couldn't retrieve status from pipe"); + } + + if (pidOrErr.pid < 0) + return Err(Error::GetLastSystemError(pidOrErr.err)); + + return SafeCast(pidOrErr.pid); + } +} diff --git a/src/Nazara/Core/Posix/ProcessImpl.hpp b/src/Nazara/Core/Posix/ProcessImpl.hpp new file mode 100644 index 000000000..001355c4d --- /dev/null +++ b/src/Nazara/Core/Posix/ProcessImpl.hpp @@ -0,0 +1,18 @@ +// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Core module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_CORE_POSIX_PROCESSIMPL_HPP +#define NAZARA_CORE_POSIX_PROCESSIMPL_HPP + +#include +#include + +namespace Nz::PlatformImpl +{ + NAZARA_CORE_API Result SpawnDetachedProcess(const std::filesystem::path& program, std::span arguments = {}, const std::filesystem::path& workingDirectory = {}); +} + +#endif // NAZARA_CORE_POSIX_PROCESSIMPL_HPP diff --git a/src/Nazara/Core/Process.cpp b/src/Nazara/Core/Process.cpp new file mode 100644 index 000000000..37273972a --- /dev/null +++ b/src/Nazara/Core/Process.cpp @@ -0,0 +1,23 @@ +// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Core module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include + +#if defined(NAZARA_PLATFORM_WINDOWS) +#include +#elif defined(NAZARA_PLATFORM_POSIX) +#include +#else +#error OS not handled +#endif + +#include + +namespace Nz +{ + Result Process::SpawnDetached(const std::filesystem::path& program, std::span arguments, const std::filesystem::path& workingDirectory) + { + return PlatformImpl::SpawnDetachedProcess(program, arguments, workingDirectory); + } +} diff --git a/src/Nazara/Core/StringExt.cpp b/src/Nazara/Core/StringExt.cpp index 063f60b15..55a41858b 100644 --- a/src/Nazara/Core/StringExt.cpp +++ b/src/Nazara/Core/StringExt.cpp @@ -56,7 +56,14 @@ namespace Nz template<> struct WideConverter<2> { + static constexpr std::size_t MaxCharacterPerCodepoint = 2; + // UTF-16 (Windows) + static std::size_t Append(char32_t codepoint, wchar_t* output) + { + return utf8::append16(codepoint, output) - output; + } + static std::string From(const wchar_t* wstr, std::size_t size) { return FromUtf16String(std::u16string_view(reinterpret_cast(wstr), size)); @@ -76,7 +83,15 @@ namespace Nz template<> struct WideConverter<4> { + static constexpr std::size_t MaxCharacterPerCodepoint = 1; + // UTF-32 (POSIX) + static std::size_t Append(char32_t codepoint, wchar_t* output) + { + *output = codepoint; + return 1; + } + static std::string From(const wchar_t* wstr, std::size_t size) { return FromUtf32String(std::u32string_view(reinterpret_cast(wstr), size)); @@ -91,6 +106,8 @@ namespace Nz } }; #endif + + using NativeWideConverter = WideConverter; } std::size_t ComputeCharacterCount(std::string_view str) @@ -183,7 +200,7 @@ namespace Nz { NAZARA_USE_ANONYMOUS_NAMESPACE - return WideConverter::From(wstr.data(), wstr.size()); + return NativeWideConverter::From(wstr.data(), wstr.size()); } std::size_t GetCharacterPosition(std::string_view str, std::size_t characterIndex) @@ -283,6 +300,31 @@ namespace Nz callback(std::u32string_view(&buffer[0], charCount)); } + void IterateOnWideChars(std::string_view str, FunctionRef callback) + { + std::array buffer; + std::size_t charCount = 0; + + utf8::unchecked::iterator it(str.data()); + utf8::unchecked::iterator end(str.data() + str.size()); + for (; it != end; ++it) + { + charCount += NativeWideConverter::Append(*it, &buffer[charCount]); + + // Leave enough space for a full character (using up to MaxCharacterPerCodepoint) + if (buffer.size() - charCount < NativeWideConverter::MaxCharacterPerCodepoint) + { + if (!callback(std::wstring_view(&buffer[0], charCount))) + return; + + charCount = 0; + } + } + + if (charCount != 0) + callback(std::wstring_view(&buffer[0], charCount)); + } + bool MatchPattern(std::string_view str, std::string_view pattern) { if (str.empty() || pattern.empty()) @@ -514,7 +556,7 @@ namespace Nz { NAZARA_USE_ANONYMOUS_NAMESPACE - return WideConverter::To(str); + return NativeWideConverter::To(str); } std::string_view TrimLeft(std::string_view str) diff --git a/src/Nazara/Core/Win32/ProcessImpl.cpp b/src/Nazara/Core/Win32/ProcessImpl.cpp new file mode 100644 index 000000000..7ceb9729d --- /dev/null +++ b/src/Nazara/Core/Win32/ProcessImpl.cpp @@ -0,0 +1,125 @@ +// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Core module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include +#include +#include +#include +#include + +namespace Nz::PlatformImpl +{ + std::wstring BuildCommandLine(const std::filesystem::path& program, std::span arguments) + { + std::wstring commandLine; + + auto AddProgramName = [&commandLine](const WidePathHolder& program) + { + if (!program.starts_with(L'"') && !program.ends_with(L'"') && program.find(L' ') != program.npos) + { + commandLine += L'"'; + commandLine += program; + commandLine += L'"'; + } + else + commandLine += program; + + ReplaceStr(commandLine, L'/', L'\\'); + commandLine += L' '; + }; + + // Use a lambda to keep WidePathHolder alive + AddProgramName(PathToWideTemp(program)); + + for (std::string_view arg : arguments) + { + commandLine += L' '; + + if (arg.empty()) + { + // Empty argument (ensures quotes) + commandLine += LR"("")"; + continue; + } + + // Characters requiring quotes from cmd /? + constexpr std::string_view specialChars = "\t &()[]{}^=;!'+,`~%|<>"; + + bool requiresQuote = arg.find_first_of(specialChars) != arg.npos; + + if (requiresQuote) + commandLine += L'"'; + + std::size_t backslashCount = 0; + IterateOnWideChars(arg, [&](std::wstring_view characters) + { + for (wchar_t character : characters) + { + if (character != L'\\') + { + // Escape quotes and double their preceding backslashes ('\\\"' => '\\\\\\\"') + if (character == L'"') + commandLine.append(backslashCount + 1, L'\\'); + + backslashCount = 0; + } + else + backslashCount++; + + commandLine.push_back(character); + } + + return true; + }); + + if (requiresQuote) + { + commandLine.append(backslashCount, L'\\'); + commandLine += L'"'; + } + } + + return commandLine; + } + + Result SpawnDetachedProcess(const std::filesystem::path& program, std::span arguments, const std::filesystem::path& workingDirectory) + { + DWORD creationFlags = CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS; + + std::wstring commandLine = BuildCommandLine(program, arguments); + + STARTUPINFOW startupInfo = { + .cb = sizeof(startupInfo), + .dwX = DWORD(CW_USEDEFAULT), + .dwY = DWORD(CW_USEDEFAULT), + .dwXSize = DWORD(CW_USEDEFAULT), + .dwYSize = DWORD(CW_USEDEFAULT) + }; + + PROCESS_INFORMATION processInfo; + BOOL success = CreateProcessW( + nullptr, // Application name + commandLine.data(), // Command line + nullptr, // Process attributes + nullptr, // Thread attributes + false, // Inherit handles + creationFlags, // Creation flags + nullptr, // Environment + (!workingDirectory.empty()) ? PathToWideTemp(workingDirectory).data() : nullptr, // Current directory + &startupInfo, // Startup info + &processInfo // Process information + ); + + if (!success) + return Err(Error::GetLastSystemError()); + + CloseHandle(processInfo.hThread); + CloseHandle(processInfo.hProcess); + + return Pid(processInfo.dwProcessId); + } +} + +#include diff --git a/src/Nazara/Core/Win32/ProcessImpl.hpp b/src/Nazara/Core/Win32/ProcessImpl.hpp new file mode 100644 index 000000000..b1471d602 --- /dev/null +++ b/src/Nazara/Core/Win32/ProcessImpl.hpp @@ -0,0 +1,18 @@ +// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Core module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_CORE_WIN32_PROCESSIMPL_HPP +#define NAZARA_CORE_WIN32_PROCESSIMPL_HPP + +#include +#include + +namespace Nz::PlatformImpl +{ + NAZARA_CORE_API Result SpawnDetachedProcess(const std::filesystem::path& program, std::span arguments = {}, const std::filesystem::path& workingDirectory = {}); +} + +#endif // NAZARA_CORE_WIN32_PROCESSIMPL_HPP