aboutsummaryrefslogtreecommitdiff
path: root/src/zenutil/zenserverprocess.cpp
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2023-05-02 10:01:47 +0200
committerGitHub <[email protected]>2023-05-02 10:01:47 +0200
commit075d17f8ada47e990fe94606c3d21df409223465 (patch)
treee50549b766a2f3c354798a54ff73404217b4c9af /src/zenutil/zenserverprocess.cpp
parentfix: bundle shouldn't append content zip to zen (diff)
downloadzen-075d17f8ada47e990fe94606c3d21df409223465.tar.xz
zen-075d17f8ada47e990fe94606c3d21df409223465.zip
moved source directories into `/src` (#264)
* moved source directories into `/src` * updated bundle.lua for new `src` path * moved some docs, icon * removed old test trees
Diffstat (limited to 'src/zenutil/zenserverprocess.cpp')
-rw-r--r--src/zenutil/zenserverprocess.cpp677
1 files changed, 677 insertions, 0 deletions
diff --git a/src/zenutil/zenserverprocess.cpp b/src/zenutil/zenserverprocess.cpp
new file mode 100644
index 000000000..5ecde343b
--- /dev/null
+++ b/src/zenutil/zenserverprocess.cpp
@@ -0,0 +1,677 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "zenutil/zenserverprocess.h"
+
+#include <zencore/except.h>
+#include <zencore/filesystem.h>
+#include <zencore/fmtutils.h>
+#include <zencore/logging.h>
+#include <zencore/session.h>
+#include <zencore/string.h>
+#include <zencore/thread.h>
+
+#include <atomic>
+
+#if ZEN_PLATFORM_WINDOWS
+# include <atlbase.h>
+# include <zencore/windows.h>
+#else
+# include <sys/mman.h>
+#endif
+
+//////////////////////////////////////////////////////////////////////////
+
+namespace zen {
+
+namespace zenutil {
+#if ZEN_PLATFORM_WINDOWS
+ class SecurityAttributes
+ {
+ public:
+ inline SECURITY_ATTRIBUTES* Attributes() { return &m_Attributes; }
+
+ protected:
+ SECURITY_ATTRIBUTES m_Attributes{};
+ SECURITY_DESCRIPTOR m_Sd{};
+ };
+
+ // Security attributes which allows any user access
+
+ class AnyUserSecurityAttributes : public SecurityAttributes
+ {
+ public:
+ AnyUserSecurityAttributes()
+ {
+ m_Attributes.nLength = sizeof m_Attributes;
+ m_Attributes.bInheritHandle = false; // Disable inheritance
+
+ const BOOL Success = InitializeSecurityDescriptor(&m_Sd, SECURITY_DESCRIPTOR_REVISION);
+
+ if (Success)
+ {
+ if (!SetSecurityDescriptorDacl(&m_Sd, TRUE, (PACL)NULL, FALSE))
+ {
+ ThrowLastError("SetSecurityDescriptorDacl failed");
+ }
+
+ m_Attributes.lpSecurityDescriptor = &m_Sd;
+ }
+ }
+ };
+#endif // ZEN_PLATFORM_WINDOWS
+
+} // namespace zenutil
+
+//////////////////////////////////////////////////////////////////////////
+
+ZenServerState::ZenServerState()
+{
+}
+
+ZenServerState::~ZenServerState()
+{
+ if (m_OurEntry)
+ {
+ // Clean up our entry now that we're leaving
+
+ m_OurEntry->Reset();
+ m_OurEntry = nullptr;
+ }
+
+#if ZEN_PLATFORM_WINDOWS
+ if (m_Data)
+ {
+ UnmapViewOfFile(m_Data);
+ }
+
+ if (m_hMapFile)
+ {
+ CloseHandle(m_hMapFile);
+ }
+#else
+ if (m_Data != nullptr)
+ {
+ munmap(m_Data, m_MaxEntryCount * sizeof(ZenServerEntry));
+ }
+
+ int Fd = int(intptr_t(m_hMapFile));
+ close(Fd);
+#endif
+
+ m_Data = nullptr;
+}
+
+void
+ZenServerState::Initialize()
+{
+ size_t MapSize = m_MaxEntryCount * sizeof(ZenServerEntry);
+
+#if ZEN_PLATFORM_WINDOWS
+ // TODO: there's a small chance of a race here, this logic could be tightened up with a mutex to
+ // ensure only a single process at a time creates the mapping
+ // TODO: the fallback to Local instead of Global has a flaw where if you start a non-elevated instance
+ // first then start an elevated instance second you'll have the first instance with a local
+ // mapping and the second instance with a global mapping. This kind of elevated/non-elevated
+ // shouldn't be common, but handling for it should be improved in the future.
+
+ HANDLE hMap = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, L"Global\\ZenMap");
+ if (hMap == NULL)
+ {
+ hMap = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, L"Local\\ZenMap");
+ }
+
+ if (hMap == NULL)
+ {
+ // Security attributes to enable any user to access state
+ zenutil::AnyUserSecurityAttributes Attrs;
+
+ hMap = CreateFileMapping(INVALID_HANDLE_VALUE, // use paging file
+ Attrs.Attributes(), // allow anyone to access
+ PAGE_READWRITE, // read/write access
+ 0, // maximum object size (high-order DWORD)
+ DWORD(MapSize), // maximum object size (low-order DWORD)
+ L"Global\\ZenMap"); // name of mapping object
+
+ if (hMap == NULL)
+ {
+ hMap = CreateFileMapping(INVALID_HANDLE_VALUE, // use paging file
+ Attrs.Attributes(), // allow anyone to access
+ PAGE_READWRITE, // read/write access
+ 0, // maximum object size (high-order DWORD)
+ m_MaxEntryCount * sizeof(ZenServerEntry), // maximum object size (low-order DWORD)
+ L"Local\\ZenMap"); // name of mapping object
+ }
+
+ if (hMap == NULL)
+ {
+ ThrowLastError("Could not open or create file mapping object for Zen server state");
+ }
+ }
+
+ void* pBuf = MapViewOfFile(hMap, // handle to map object
+ FILE_MAP_ALL_ACCESS, // read/write permission
+ 0, // offset high
+ 0, // offset low
+ DWORD(MapSize));
+
+ if (pBuf == NULL)
+ {
+ ThrowLastError("Could not map view of Zen server state");
+ }
+#else
+ int Fd = shm_open("/UnrealEngineZen", O_RDWR | O_CREAT | O_CLOEXEC, 0666);
+ if (Fd < 0)
+ {
+ ThrowLastError("Could not open a shared memory object");
+ }
+ fchmod(Fd, 0666);
+ void* hMap = (void*)intptr_t(Fd);
+
+ int Result = ftruncate(Fd, MapSize);
+ ZEN_UNUSED(Result);
+
+ void* pBuf = mmap(nullptr, MapSize, PROT_READ | PROT_WRITE, MAP_SHARED, Fd, 0);
+ if (pBuf == MAP_FAILED)
+ {
+ ThrowLastError("Could not map view of Zen server state");
+ }
+#endif
+
+ m_hMapFile = hMap;
+ m_Data = reinterpret_cast<ZenServerEntry*>(pBuf);
+ m_IsReadOnly = false;
+}
+
+bool
+ZenServerState::InitializeReadOnly()
+{
+ size_t MapSize = m_MaxEntryCount * sizeof(ZenServerEntry);
+
+#if ZEN_PLATFORM_WINDOWS
+ HANDLE hMap = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, L"Global\\ZenMap");
+ if (hMap == NULL)
+ {
+ hMap = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, L"Local\\ZenMap");
+ }
+
+ if (hMap == NULL)
+ {
+ return false;
+ }
+
+ void* pBuf = MapViewOfFile(hMap, // handle to map object
+ FILE_MAP_READ, // read permission
+ 0, // offset high
+ 0, // offset low
+ MapSize);
+
+ if (pBuf == NULL)
+ {
+ ThrowLastError("Could not map view of Zen server state");
+ }
+#else
+ int Fd = shm_open("/UnrealEngineZen", O_RDONLY | O_CLOEXEC, 0666);
+ if (Fd < 0)
+ {
+ return false;
+ }
+ void* hMap = (void*)intptr_t(Fd);
+
+ void* pBuf = mmap(nullptr, MapSize, PROT_READ, MAP_PRIVATE, Fd, 0);
+ if (pBuf == MAP_FAILED)
+ {
+ ThrowLastError("Could not map read-only view of Zen server state");
+ }
+#endif
+
+ m_hMapFile = hMap;
+ m_Data = reinterpret_cast<ZenServerEntry*>(pBuf);
+
+ return true;
+}
+
+ZenServerState::ZenServerEntry*
+ZenServerState::Lookup(int DesiredListenPort)
+{
+ for (int i = 0; i < m_MaxEntryCount; ++i)
+ {
+ if (m_Data[i].DesiredListenPort == DesiredListenPort)
+ {
+ return &m_Data[i];
+ }
+ }
+
+ return nullptr;
+}
+
+ZenServerState::ZenServerEntry*
+ZenServerState::Register(int DesiredListenPort)
+{
+ if (m_Data == nullptr)
+ {
+ return nullptr;
+ }
+
+ // Allocate an entry
+
+ int Pid = GetCurrentProcessId();
+
+ for (int i = 0; i < m_MaxEntryCount; ++i)
+ {
+ ZenServerEntry& Entry = m_Data[i];
+
+ if (Entry.DesiredListenPort.load(std::memory_order_relaxed) == 0)
+ {
+ uint16_t Expected = 0;
+ if (Entry.DesiredListenPort.compare_exchange_strong(Expected, uint16_t(DesiredListenPort)))
+ {
+ // Successfully allocated entry
+
+ m_OurEntry = &Entry;
+
+ Entry.Pid = Pid;
+ Entry.EffectiveListenPort = 0;
+ Entry.Flags = 0;
+
+ const Oid SesId = GetSessionId();
+ memcpy(Entry.SessionId, &SesId, sizeof SesId);
+
+ return &Entry;
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+void
+ZenServerState::Sweep()
+{
+ if (m_Data == nullptr)
+ {
+ return;
+ }
+
+ ZEN_ASSERT(m_IsReadOnly == false);
+
+ for (int i = 0; i < m_MaxEntryCount; ++i)
+ {
+ ZenServerEntry& Entry = m_Data[i];
+
+ if (Entry.DesiredListenPort)
+ {
+ if (IsProcessRunning(Entry.Pid) == false)
+ {
+ ZEN_DEBUG("Sweep - pid {} not running, reclaiming entry (port {})", Entry.Pid, Entry.DesiredListenPort);
+
+ Entry.Reset();
+ }
+ }
+ }
+}
+
+void
+ZenServerState::Snapshot(std::function<void(const ZenServerEntry&)>&& Callback)
+{
+ if (m_Data == nullptr)
+ {
+ return;
+ }
+
+ for (int i = 0; i < m_MaxEntryCount; ++i)
+ {
+ ZenServerEntry& Entry = m_Data[i];
+
+ if (Entry.DesiredListenPort)
+ {
+ Callback(Entry);
+ }
+ }
+}
+
+void
+ZenServerState::ZenServerEntry::Reset()
+{
+ Pid = 0;
+ DesiredListenPort = 0;
+ Flags = 0;
+ EffectiveListenPort = 0;
+}
+
+void
+ZenServerState::ZenServerEntry::SignalShutdownRequest()
+{
+ Flags |= uint16_t(FlagsEnum::kShutdownPlease);
+}
+
+void
+ZenServerState::ZenServerEntry::SignalReady()
+{
+ Flags |= uint16_t(FlagsEnum::kIsReady);
+}
+
+bool
+ZenServerState::ZenServerEntry::AddSponsorProcess(uint32_t PidToAdd)
+{
+ for (std::atomic<uint32_t>& PidEntry : SponsorPids)
+ {
+ if (PidEntry.load(std::memory_order_relaxed) == 0)
+ {
+ uint32_t Expected = 0;
+ if (PidEntry.compare_exchange_strong(Expected, PidToAdd))
+ {
+ // Success!
+ return true;
+ }
+ }
+ else if (PidEntry.load(std::memory_order_relaxed) == PidToAdd)
+ {
+ // Success, the because pid is already in the list
+ return true;
+ }
+ }
+
+ return false;
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+std::atomic<int> ZenServerTestCounter{0};
+
+ZenServerEnvironment::ZenServerEnvironment()
+{
+}
+
+ZenServerEnvironment::~ZenServerEnvironment()
+{
+}
+
+void
+ZenServerEnvironment::Initialize(std::filesystem::path ProgramBaseDir)
+{
+ m_ProgramBaseDir = ProgramBaseDir;
+
+ ZEN_DEBUG("Program base dir is '{}'", ProgramBaseDir);
+
+ m_IsInitialized = true;
+}
+
+void
+ZenServerEnvironment::InitializeForTest(std::filesystem::path ProgramBaseDir,
+ std::filesystem::path TestBaseDir,
+ std::string_view ServerClass)
+{
+ using namespace std::literals;
+
+ m_ProgramBaseDir = ProgramBaseDir;
+ m_TestBaseDir = TestBaseDir;
+
+ ZEN_INFO("Program base dir is '{}'", ProgramBaseDir);
+ ZEN_INFO("Cleaning test base dir '{}'", TestBaseDir);
+ DeleteDirectories(TestBaseDir.c_str());
+
+ m_IsTestInstance = true;
+ m_IsInitialized = true;
+
+ if (ServerClass.empty())
+ {
+#if ZEN_WITH_HTTPSYS
+ m_ServerClass = "httpsys"sv;
+#else
+ m_ServerClass = "asio"sv;
+#endif
+ }
+ else
+ {
+ m_ServerClass = ServerClass;
+ }
+}
+
+std::filesystem::path
+ZenServerEnvironment::CreateNewTestDir()
+{
+ using namespace std::literals;
+
+ ExtendableWideStringBuilder<256> TestDir;
+ TestDir << "test"sv << int64_t(++ZenServerTestCounter);
+
+ std::filesystem::path TestPath = m_TestBaseDir / TestDir.c_str();
+
+ ZEN_INFO("Creating new test dir @ '{}'", TestPath);
+
+ CreateDirectories(TestPath.c_str());
+
+ return TestPath;
+}
+
+std::filesystem::path
+ZenServerEnvironment::GetTestRootDir(std::string_view Path)
+{
+ std::filesystem::path Root = m_ProgramBaseDir.parent_path().parent_path();
+
+ std::filesystem::path Relative{Path};
+
+ return Root / Relative;
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+std::atomic<int> ChildIdCounter{0};
+
+ZenServerInstance::ZenServerInstance(ZenServerEnvironment& TestEnvironment) : m_Env(TestEnvironment)
+{
+ ZEN_ASSERT(TestEnvironment.IsInitialized());
+}
+
+ZenServerInstance::~ZenServerInstance()
+{
+ Shutdown();
+}
+
+void
+ZenServerInstance::SignalShutdown()
+{
+ m_ShutdownEvent.Set();
+}
+
+void
+ZenServerInstance::Shutdown()
+{
+ if (m_Process.IsValid())
+ {
+ if (m_Terminate)
+ {
+ ZEN_INFO("Terminating zenserver process");
+ m_Process.Terminate(111);
+ m_Process.Reset();
+ }
+ else
+ {
+ SignalShutdown();
+ m_Process.Wait();
+ m_Process.Reset();
+ }
+ }
+}
+
+void
+ZenServerInstance::SpawnServer(int BasePort, std::string_view AdditionalServerArgs)
+{
+ ZEN_ASSERT(!m_Process.IsValid()); // Only spawn once
+
+ const int MyPid = zen::GetCurrentProcessId();
+ const int ChildId = ++ChildIdCounter;
+
+ ExtendableStringBuilder<32> ChildEventName;
+ ChildEventName << "Zen_Child_" << ChildId;
+ NamedEvent ChildEvent{ChildEventName};
+
+ CreateShutdownEvent(BasePort);
+
+ ExtendableStringBuilder<32> LogId;
+ LogId << "Zen" << ChildId;
+
+ ExtendableStringBuilder<512> CommandLine;
+ CommandLine << "zenserver" ZEN_EXE_SUFFIX_LITERAL; // see CreateProc() call for actual binary path
+
+ const bool IsTest = m_Env.IsTestEnvironment();
+
+ if (IsTest)
+ {
+ if (!m_OwnerPid.has_value())
+ {
+ m_OwnerPid = MyPid;
+ }
+
+ CommandLine << " --test --log-id " << LogId;
+ }
+
+ if (m_OwnerPid.has_value())
+ {
+ CommandLine << " --owner-pid " << m_OwnerPid.value();
+ }
+
+ CommandLine << " --child-id " << ChildEventName;
+
+ if (std::string_view ServerClass = m_Env.GetServerClass(); ServerClass.empty() == false)
+ {
+ CommandLine << " --http " << ServerClass;
+ }
+
+ if (BasePort)
+ {
+ CommandLine << " --port " << BasePort;
+ m_BasePort = BasePort;
+ }
+
+ if (!m_TestDir.empty())
+ {
+ CommandLine << " --data-dir ";
+ PathToUtf8(m_TestDir.c_str(), CommandLine);
+ }
+
+ if (!AdditionalServerArgs.empty())
+ {
+ CommandLine << " " << AdditionalServerArgs;
+ }
+
+ std::filesystem::path CurrentDirectory = std::filesystem::current_path();
+
+ ZEN_DEBUG("Spawning server '{}'", LogId);
+
+ uint32_t CreationFlags = 0;
+ if (!IsTest)
+ {
+ CreationFlags |= CreateProcOptions::Flag_NewConsole;
+ }
+
+ const std::filesystem::path BaseDir = m_Env.ProgramBaseDir();
+ const std::filesystem::path Executable = BaseDir / "zenserver" ZEN_EXE_SUFFIX_LITERAL;
+ CreateProcOptions CreateOptions = {
+ .WorkingDirectory = &CurrentDirectory,
+ .Flags = CreationFlags,
+ };
+ CreateProcResult ChildPid = CreateProc(Executable, CommandLine.ToView(), CreateOptions);
+#if ZEN_PLATFORM_WINDOWS
+ if (!ChildPid && ::GetLastError() == ERROR_ELEVATION_REQUIRED)
+ {
+ ZEN_DEBUG("Regular spawn failed - spawning elevated server");
+ CreateOptions.Flags |= CreateProcOptions::Flag_Elevated;
+ ChildPid = CreateProc(Executable, CommandLine.ToView(), CreateOptions);
+ }
+#endif
+
+ if (!ChildPid)
+ {
+ ThrowLastError("Server spawn failed");
+ }
+
+ ZEN_DEBUG("Server '{}' spawned OK", LogId);
+
+ if (IsTest)
+ {
+ m_Process.Initialize(ChildPid);
+ }
+
+ m_ReadyEvent = std::move(ChildEvent);
+}
+
+void
+ZenServerInstance::CreateShutdownEvent(int BasePort)
+{
+ ExtendableStringBuilder<32> ChildShutdownEventName;
+ ChildShutdownEventName << "Zen_" << BasePort;
+ ChildShutdownEventName << "_Shutdown";
+ NamedEvent ChildShutdownEvent{ChildShutdownEventName};
+ m_ShutdownEvent = std::move(ChildShutdownEvent);
+}
+
+void
+ZenServerInstance::AttachToRunningServer(int BasePort)
+{
+ ZenServerState State;
+ if (!State.InitializeReadOnly())
+ {
+ // TODO: return success/error code instead?
+ throw std::runtime_error("No zen state found");
+ }
+
+ const ZenServerState::ZenServerEntry* Entry = nullptr;
+
+ if (BasePort)
+ {
+ Entry = State.Lookup(BasePort);
+ }
+ else
+ {
+ State.Snapshot([&](const ZenServerState::ZenServerEntry& InEntry) { Entry = &InEntry; });
+ }
+
+ if (!Entry)
+ {
+ // TODO: return success/error code instead?
+ throw std::runtime_error("No server found");
+ }
+
+ m_Process.Initialize(Entry->Pid);
+ CreateShutdownEvent(Entry->EffectiveListenPort);
+}
+
+void
+ZenServerInstance::Detach()
+{
+ if (m_Process.IsValid())
+ {
+ m_Process.Reset();
+ m_ShutdownEvent.Close();
+ }
+}
+
+void
+ZenServerInstance::WaitUntilReady()
+{
+ while (m_ReadyEvent.Wait(100) == false)
+ {
+ if (!m_Process.IsRunning() || !m_Process.IsValid())
+ {
+ ZEN_INFO("Wait abandoned by invalid process (running={})", m_Process.IsRunning());
+ return;
+ }
+ }
+}
+
+bool
+ZenServerInstance::WaitUntilReady(int Timeout)
+{
+ return m_ReadyEvent.Wait(Timeout);
+}
+
+std::string
+ZenServerInstance::GetBaseUri() const
+{
+ ZEN_ASSERT(m_BasePort);
+
+ return fmt::format("http://localhost:{}", m_BasePort);
+}
+
+} // namespace zen