Core: Add initial process support (Process::SpawnDetached)

This commit is contained in:
SirLynix 2024-01-22 23:17:12 +01:00 committed by Jérôme Leclercq
parent 278e59934b
commit ac1422c221
13 changed files with 575 additions and 6 deletions

View File

@ -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 <NazaraUtils/Prerequisites.hpp>
#include <Nazara/Core/Config.hpp>
#include <NazaraUtils/Result.hpp>
#include <filesystem>
#include <span>
#include <string>
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<Pid, std::string> SpawnDetached(const std::filesystem::path& program, std::span<const std::string> arguments = {}, const std::filesystem::path& workingDirectory = {}); };
}
#include <Nazara/Core/Process.inl>
#endif // NAZARA_CORE_PROCESS_HPP

View File

@ -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 <Nazara/Core/Debug.hpp>
namespace Nz
{
}
#include <Nazara/Core/DebugOff.hpp>

View File

@ -36,6 +36,7 @@ namespace Nz
inline bool IsNumber(std::string_view str);
NAZARA_CORE_API void IterateOnCodepoints(std::string_view str, FunctionRef<bool(std::u32string_view characters)> callback);
NAZARA_CORE_API void IterateOnWideChars(std::string_view str, FunctionRef<bool(std::wstring_view characters)> 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<typename T> std::basic_string<T>& ReplaceStr(std::basic_string<T>& str, T from, T to);
template<typename T> std::basic_string<T>& ReplaceStr(std::basic_string<T>& str, const T* from, const T* to);
template<typename T> std::basic_string<T>& ReplaceStr(std::basic_string<T>& str, std::basic_string_view<T> from, std::basic_string_view<T> 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);

View File

@ -77,11 +77,28 @@ namespace Nz
return str;
}
inline std::string& ReplaceStr(std::string& str, std::string_view from, std::string_view to)
template<typename T>
std::basic_string<T>& ReplaceStr(std::basic_string<T>& 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<typename T>
std::basic_string<T>& ReplaceStr(std::basic_string<T>& str, const T* from, const T* to)
{
return ReplaceStr(str, std::basic_string_view<T>(from), std::basic_string_view<T>(to));
}
template<typename T>
std::basic_string<T>& ReplaceStr(std::basic_string<T>& str, std::basic_string_view<T> from, std::basic_string_view<T> to)
{
std::size_t startPos = 0;
while ((startPos = str.find(from, startPos)) != std::string::npos)
{

View File

@ -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 <Nazara/Core/Posix/PosixUtils.hpp>
#include <cassert>
#include <cerrno>
#include <utility>
#include <fcntl.h>
#include <Nazara/Core/Debug.hpp>
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;
}
}

View File

@ -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 <NazaraUtils/Prerequisites.hpp>
#include <unistd.h>
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 <Nazara/Core/Posix/PosixUtils.inl>
#endif // NAZARA_CORE_POSIX_POSIXUTILS_HPP

View File

@ -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);
}
}

View File

@ -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 <Nazara/Core/Posix/ProcessImpl.hpp>
#include <Nazara/Core/Error.hpp>
#include <Nazara/Core/Posix/PosixUtils.hpp>
#include <NazaraUtils/Algorithm.hpp>
#include <NazaraUtils/StackArray.hpp>
#include <cerrno>
#include <cstdlib>
#include <unistd.h>
#include <sys/wait.h>
#include <Nazara/Core/Debug.hpp>
namespace Nz::PlatformImpl
{
Result<Pid, std::string> SpawnDetachedProcess(const std::filesystem::path& program, std::span<const std::string> 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<char*> 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<char*>(program.c_str());
for (std::size_t i = 0; i < arguments.size(); ++i)
argv[i + 1] = const_cast<char*>(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<Pid>(pidOrErr.pid);
}
}

View File

@ -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 <NazaraUtils/Prerequisites.hpp>
#include <Nazara/Core/Process.hpp>
namespace Nz::PlatformImpl
{
NAZARA_CORE_API Result<Pid, std::string> SpawnDetachedProcess(const std::filesystem::path& program, std::span<const std::string> arguments = {}, const std::filesystem::path& workingDirectory = {});
}
#endif // NAZARA_CORE_POSIX_PROCESSIMPL_HPP

View File

@ -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 <Nazara/Core/Process.hpp>
#if defined(NAZARA_PLATFORM_WINDOWS)
#include <Nazara/Core/Win32/ProcessImpl.hpp>
#elif defined(NAZARA_PLATFORM_POSIX)
#include <Nazara/Core/Posix/ProcessImpl.hpp>
#else
#error OS not handled
#endif
#include <Nazara/Core/Debug.hpp>
namespace Nz
{
Result<Pid, std::string> Process::SpawnDetached(const std::filesystem::path& program, std::span<const std::string> arguments, const std::filesystem::path& workingDirectory)
{
return PlatformImpl::SpawnDetachedProcess(program, arguments, workingDirectory);
}
}

View File

@ -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<const char16_t*>(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<const char32_t*>(wstr), size));
@ -91,6 +106,8 @@ namespace Nz
}
};
#endif
using NativeWideConverter = WideConverter<sizeof(wchar_t)>;
}
std::size_t ComputeCharacterCount(std::string_view str)
@ -183,7 +200,7 @@ namespace Nz
{
NAZARA_USE_ANONYMOUS_NAMESPACE
return WideConverter<sizeof(wchar_t)>::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<bool(std::wstring_view characters)> callback)
{
std::array<wchar_t, 128> buffer;
std::size_t charCount = 0;
utf8::unchecked::iterator<const char*> it(str.data());
utf8::unchecked::iterator<const char*> 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<sizeof(wchar_t)>::To(str);
return NativeWideConverter::To(str);
}
std::string_view TrimLeft(std::string_view str)

View File

@ -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 <Nazara/Core/Win32/ProcessImpl.hpp>
#include <Nazara/Core/Error.hpp>
#include <Nazara/Core/StringExt.hpp>
#include <Nazara/Core/Win32/Win32Utils.hpp>
#include <Windows.h>
#include <Nazara/Core/Debug.hpp>
namespace Nz::PlatformImpl
{
std::wstring BuildCommandLine(const std::filesystem::path& program, std::span<const std::string> 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<Pid, std::string> SpawnDetachedProcess(const std::filesystem::path& program, std::span<const std::string> 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 <Nazara/Core/AntiWindows.hpp>

View File

@ -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 <NazaraUtils/Prerequisites.hpp>
#include <Nazara/Core/Process.hpp>
namespace Nz::PlatformImpl
{
NAZARA_CORE_API Result<Pid, std::string> SpawnDetachedProcess(const std::filesystem::path& program, std::span<const std::string> arguments = {}, const std::filesystem::path& workingDirectory = {});
}
#endif // NAZARA_CORE_WIN32_PROCESSIMPL_HPP