aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/zen/cmds/service_cmd.cpp462
-rw-r--r--src/zen/cmds/service_cmd.h52
-rw-r--r--src/zen/zen.cpp3
-rw-r--r--src/zencore/include/zencore/process.h1
-rw-r--r--src/zencore/process.cpp17
-rw-r--r--src/zencore/thread.cpp2
-rw-r--r--src/zenserver/main.cpp16
-rw-r--r--src/zenutil/include/zenutil/service.h55
-rw-r--r--src/zenutil/service.cpp1025
-rw-r--r--src/zenutil/zenserverprocess.cpp3
10 files changed, 1634 insertions, 2 deletions
diff --git a/src/zen/cmds/service_cmd.cpp b/src/zen/cmds/service_cmd.cpp
new file mode 100644
index 000000000..372fce5cb
--- /dev/null
+++ b/src/zen/cmds/service_cmd.cpp
@@ -0,0 +1,462 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "service_cmd.h"
+
+#include <zencore/filesystem.h>
+#include <zencore/fmtutils.h>
+#include <zencore/logging.h>
+#include <zencore/zencore.h>
+#include <zenutil/service.h>
+
+#if ZEN_PLATFORM_WINDOWS
+# include <zencore/windows.h>
+# include <shellapi.h>
+#endif
+
+#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
+# include <unistd.h>
+#endif
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <gsl/gsl-lite.hpp>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+#include <memory>
+
+namespace zen {
+
+//////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+#if ZEN_PLATFORM_WINDOWS
+ BOOL IsElevated()
+ {
+ BOOL fRet = FALSE;
+ HANDLE hToken = NULL;
+ if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
+ {
+ TOKEN_ELEVATION Elevation;
+ DWORD cbSize = sizeof(TOKEN_ELEVATION);
+ if (GetTokenInformation(hToken, TokenElevation, &Elevation, sizeof(Elevation), &cbSize))
+ {
+ fRet = Elevation.TokenIsElevated;
+ }
+ }
+ if (hToken)
+ {
+ CloseHandle(hToken);
+ }
+ return fRet;
+ }
+
+ int WinRelaunchElevated()
+ {
+ TCHAR CurrentDir[4096];
+ GetCurrentDirectory(4096, CurrentDir);
+
+ ExtendableWideStringBuilder<256> Parameters;
+ std::filesystem::path ExecutablePath = GetRunningExecutablePath();
+ std::wstring CommandLine(GetCommandLine());
+ std::string::size_type ExtraLength = CommandLine[0] == '\"' ? 2 : 0;
+ std::wstring CommandArguments = CommandLine.substr(ExecutablePath.string().length() + ExtraLength + 1);
+
+ ZEN_CONSOLE("Attempting to run '{} {}' elevated...", ExecutablePath, WideToUtf8(CommandArguments));
+
+ SHELLEXECUTEINFO shExInfo = {0};
+ shExInfo.cbSize = sizeof(shExInfo);
+ shExInfo.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE;
+ shExInfo.hwnd = 0;
+ shExInfo.lpVerb = TEXT("runas"); // Operation to perform
+ shExInfo.lpFile = ExecutablePath.c_str(); // Application to start
+ shExInfo.lpParameters = CommandArguments.c_str(); // Additional parameters
+ shExInfo.lpDirectory = CurrentDir;
+ shExInfo.nShow = SW_SHOW;
+ shExInfo.hInstApp = 0;
+
+ DWORD ReturnCode = 1;
+ if (ShellExecuteEx(&shExInfo))
+ {
+ WaitForSingleObject(shExInfo.hProcess, INFINITE);
+ GetExitCodeProcess(shExInfo.hProcess, &ReturnCode);
+ CloseHandle(shExInfo.hProcess);
+ if (ReturnCode == 0)
+ {
+ ZEN_CONSOLE("Elevated execution completed successfully.");
+ }
+ else
+ {
+ ZEN_CONSOLE("Elevated execution completed unsuccessfully, return code: '{}'.", ReturnCode);
+ }
+ }
+ else
+ {
+ ZEN_CONSOLE("Failed to run elevated, operation did not complete.");
+ }
+ return (int)ReturnCode;
+ }
+
+#else // ZEN_PLATFORM_WINDOWS
+
+ bool IsElevated() { return geteuid() == 0; }
+
+#endif // ZEN_PLATFORM_WINDOWS
+
+ int RunElevated(bool AllowElevation)
+ {
+#if ZEN_PLATFORM_WINDOWS
+ if (AllowElevation)
+ {
+ return WinRelaunchElevated();
+ }
+ else
+ {
+ ZEN_CONSOLE(
+ "This command requires elevated priviliges. Run command with elevated priviliges or add '--allow-elevation' command line "
+ "option.");
+ return 1;
+ }
+#endif // ZEN_PLATFORM_WINDOWS
+#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
+ ZEN_UNUSED(AllowElevation);
+ ZEN_CONSOLE("This command requires elevated priviliges. Run the command with `sudo`");
+ return 1;
+#endif // ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
+ }
+
+} // namespace
+
+ServiceCommand::ServiceCommand()
+{
+ m_Options.add_option("",
+ "v",
+ "verb",
+ fmt::format("Verb for service - {}, {}, {}, {}, {}. Use '--' ",
+ m_StatusOptions.program(),
+ m_InstallOptions.program(),
+ m_UninstallOptions.program(),
+ m_StartOptions.program(),
+ m_StopOptions.program()),
+ cxxopts::value(m_Verb),
+ "<verb>");
+ m_Options.parse_positional({"verb"});
+ m_Options.positional_help("verb");
+
+ m_StatusOptions.add_options()("h,help", "Print help");
+ m_StatusOptions.add_option("",
+ "n",
+ "name",
+ fmt::format("Service name, defaults to \"{}\"", m_ServiceName),
+ cxxopts::value(m_ServiceName),
+ "<name>");
+ m_StatusOptions.parse_positional({"name"});
+ m_StatusOptions.positional_help("name");
+
+ m_InstallOptions.add_options()("h,help", "Print help");
+ m_InstallOptions.add_option("", "s", "executable", "Path to server executable", cxxopts::value(m_ServerExecutable), "<path>");
+ m_InstallOptions.add_option("",
+ "n",
+ "name",
+ fmt::format("Service name, defaults to \"{}\"", m_ServiceName),
+ cxxopts::value(m_ServiceName),
+ "<name>");
+#if ZEN_PLATFORM_WINDOWS
+ m_InstallOptions.add_option("",
+ "d",
+ "display-name",
+ fmt::format("Service display, defaults to \"{}\"", m_ServiceDisplayName),
+ cxxopts::value(m_ServiceDisplayName),
+ "<display-name>");
+ m_InstallOptions.add_option("",
+ "",
+ "description",
+ fmt::format("Service description", m_ServiceDescription),
+ cxxopts::value(m_ServiceDescription),
+ "<description>");
+ m_InstallOptions.add_option("",
+ "",
+ "allow-elevation",
+ fmt::format("Allow attempt to relauch command using an elevated"),
+ cxxopts::value(m_AllowElevation),
+ "<allow-elevation>");
+ m_InstallOptions.parse_positional({"executable", "name", "display-name"});
+#else
+ m_InstallOptions.parse_positional({"executable", "name"});
+#endif // ZEN_PLATFORM_WINDOWS
+ m_InstallOptions.positional_help("executable name display-name");
+
+ m_UninstallOptions.add_options()("h,help", "Print help");
+ m_UninstallOptions.add_option("",
+ "n",
+ "name",
+ fmt::format("Service name, defaults to \"{}\"", m_ServiceName),
+ cxxopts::value(m_ServiceName),
+ "<name>");
+ m_UninstallOptions.parse_positional({"name"});
+ m_UninstallOptions.positional_help("name");
+#if ZEN_PLATFORM_WINDOWS
+ m_UninstallOptions.add_option("",
+ "",
+ "allow-elevation",
+ fmt::format("Allow attempt to relauch command using an elevated"),
+ cxxopts::value(m_AllowElevation),
+ "<allow-elevation>");
+#endif // ZEN_PLATFORM_WINDOWS
+
+ m_StartOptions.add_options()("h,help", "Print help");
+ m_StartOptions.add_option("",
+ "n",
+ "name",
+ fmt::format("Service name, defaults to \"{}\"", m_ServiceName),
+ cxxopts::value(m_ServiceName),
+ "<name>");
+ m_StartOptions.parse_positional({"name"});
+ m_StartOptions.positional_help("name");
+#if ZEN_PLATFORM_WINDOWS
+ m_StartOptions.add_option("",
+ "",
+ "allow-elevation",
+ fmt::format("Allow attempt to relauch command using an elevated"),
+ cxxopts::value(m_AllowElevation),
+ "<allow-elevation>");
+#endif // ZEN_PLATFORM_WINDOWS
+
+ m_StopOptions.add_options()("h,help", "Print help");
+ m_StopOptions.add_option("",
+ "n",
+ "name",
+ fmt::format("Service name, defaults to \"{}\"", m_ServiceName),
+ cxxopts::value(m_ServiceName),
+ "<name>");
+ m_StopOptions.parse_positional({"name"});
+ m_StopOptions.positional_help("name");
+#if ZEN_PLATFORM_WINDOWS
+ m_StopOptions.add_option("",
+ "",
+ "allow-elevation",
+ fmt::format("Allow attempt to relauch command using an elevated"),
+ cxxopts::value(m_AllowElevation),
+ "<allow-elevation>");
+#endif // ZEN_PLATFORM_WINDOWS
+}
+
+ServiceCommand::~ServiceCommand() = default;
+
+std::string
+FmtServiceInfo(const ServiceInfo& Info, std::string_view Prefix)
+{
+ std::string Result = fmt::format(
+ "{}Status: {}\n"
+ "{}Executable: {}\n"
+ "{}CommandLineOptions: {}",
+ Prefix,
+ ToString(Info.Status),
+ Prefix,
+ Info.Spec.ExecutablePath,
+ Prefix,
+ Info.Spec.CommandLineOptions);
+
+#if ZEN_PLATFORM_WINDOWS
+ Result += fmt::format(
+ "\n"
+ "{}Display Name: {}\n"
+ "{}Description: {}",
+ Prefix,
+ Info.Spec.DisplayName,
+ Prefix,
+ Info.Spec.Description);
+#endif // ZEN_PLATFORM_WINDOWS
+
+ return Result;
+}
+
+int
+ServiceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
+{
+ ZEN_UNUSED(GlobalOptions);
+
+ using namespace std::literals;
+
+ std::vector<char*> SubCommandArguments;
+ cxxopts::Options* SubOption = nullptr;
+ int ParentCommandArgCount = GetSubCommand(m_Options, argc, argv, m_SubCommands, SubOption, SubCommandArguments);
+ if (!ParseOptions(ParentCommandArgCount, argv))
+ {
+ return 0;
+ }
+
+ if (SubOption == nullptr)
+ {
+ throw zen::OptionParseException("command verb is missing");
+ }
+
+ if (!ParseOptions(*SubOption, gsl::narrow<int>(SubCommandArguments.size()), SubCommandArguments.data()))
+ {
+ return 0;
+ }
+
+ if (SubOption == &m_StatusOptions)
+ {
+ ServiceInfo Info;
+ std::error_code Ec = QueryInstalledService(m_ServiceName, Info);
+ if (Ec)
+ {
+ ZEN_CONSOLE("Can't get information about service '{}'. Reason: '{}'", m_ServiceName, Ec.message());
+ return 1;
+ }
+ if (Info.Status == ServiceStatus::NotInstalled)
+ {
+ ZEN_CONSOLE("Service '{}' is not installed", m_ServiceName);
+ return 0;
+ }
+ else
+ {
+ ZEN_CONSOLE("Service '{}':\n{}", m_ServiceName, FmtServiceInfo(Info, " "));
+ }
+ }
+
+ if (SubOption == &m_InstallOptions)
+ {
+ ServiceInfo Info;
+ std::error_code Ec = QueryInstalledService(m_ServiceName, Info);
+ if (!Ec && Info.Status != ServiceStatus::NotInstalled)
+ {
+ ZEN_CONSOLE("Service '{}' already installed:\n{}", m_ServiceName, FmtServiceInfo(Info, " "));
+ return 1;
+ }
+
+ if (!IsElevated())
+ {
+ return RunElevated(m_AllowElevation);
+ }
+
+ if (m_ServerExecutable.empty())
+ {
+ std::filesystem::path ExePath = zen::GetRunningExecutablePath();
+ ExePath.replace_filename("zenserver" + ExePath.extension().string());
+ m_ServerExecutable = ExePath;
+ }
+ m_ServerExecutable = std::filesystem::absolute(m_ServerExecutable);
+ Ec = InstallService(
+ m_ServiceName,
+ ServiceSpec {
+ .ExecutablePath = m_ServerExecutable, .CommandLineOptions = GlobalOptions.PassthroughCommandLine
+#if ZEN_PLATFORM_WINDOWS
+ ,
+ .DisplayName = m_ServiceDisplayName, .Description = m_ServiceDescription
+#endif // ZEN_PLATFORM_WINDOWS
+ });
+ if (Ec)
+ {
+ ZEN_CONSOLE("Failed to install service '{}' using '{}' . Reason: '{}'", m_ServiceName, m_ServerExecutable, Ec.message());
+ return 1;
+ }
+ ZEN_CONSOLE("Installed service '{}' using '{}' successfully", m_ServiceName, m_ServerExecutable);
+ }
+
+ if (SubOption == &m_UninstallOptions)
+ {
+ ServiceInfo Info;
+ std::error_code Ec = QueryInstalledService(m_ServiceName, Info);
+ if (Ec)
+ {
+ ZEN_CONSOLE("Failed to inspect installed service '{}'. Reason: '{}'", m_ServiceName, Ec.message());
+ return 1;
+ }
+ if (Info.Status == ServiceStatus::NotInstalled)
+ {
+ ZEN_CONSOLE("Service '{}' is not installed", m_ServiceName);
+ return 0;
+ }
+ if (Info.Status != ServiceStatus::Stopped)
+ {
+ ZEN_CONSOLE("Service '{}' is running, stop before uninstalling", m_ServiceName);
+ return 0;
+ }
+
+ if (!IsElevated())
+ {
+ return RunElevated(m_AllowElevation);
+ }
+
+ Ec = UninstallService(m_ServiceName);
+ if (Ec)
+ {
+ ZEN_CONSOLE("Failed to uninstall service '{}'. Reason: '{}'", m_ServiceName, Ec.message());
+ return 1;
+ }
+ ZEN_CONSOLE("Uninstalled service {} successfully", m_ServiceName);
+ }
+
+ if (SubOption == &m_StartOptions)
+ {
+ ServiceInfo Info;
+ std::error_code Ec = QueryInstalledService(m_ServiceName, Info);
+ if (Ec)
+ {
+ ZEN_CONSOLE("Failed to inspect installed service '{}'. Reason: '{}'", m_ServiceName, Ec.message());
+ return 1;
+ }
+ if (Info.Status == ServiceStatus::NotInstalled)
+ {
+ ZEN_CONSOLE("Service '{}' is not installed", m_ServiceName);
+ return 1;
+ }
+ if (Info.Status != ServiceStatus::Stopped)
+ {
+ ZEN_CONSOLE("Service '{}' is already running", m_ServiceName);
+ return 1;
+ }
+
+ if (!IsElevated())
+ {
+ return RunElevated(m_AllowElevation);
+ }
+
+ Ec = StartService(m_ServiceName);
+ if (Ec)
+ {
+ ZEN_CONSOLE("Failed to start service '{}'. Reason: '{}'", m_ServiceName, Ec.message());
+ return 1;
+ }
+ ZEN_CONSOLE("Started service '{}' successfully", m_ServiceName);
+ }
+
+ if (SubOption == &m_StopOptions)
+ {
+ ServiceInfo Info;
+ std::error_code Ec = QueryInstalledService(m_ServiceName, Info);
+ if (Ec)
+ {
+ ZEN_CONSOLE("Failed to inspect installed service '{}'. Reason: '{}'", m_ServiceName, Ec.message());
+ return 1;
+ }
+ if (Info.Status == ServiceStatus::NotInstalled)
+ {
+ ZEN_CONSOLE("Service '{}' is not installed", m_ServiceName);
+ return 1;
+ }
+ if (Info.Status != ServiceStatus::Running)
+ {
+ ZEN_CONSOLE("Service '{}' is not running", m_ServiceName);
+ return 1;
+ }
+
+ if (!IsElevated())
+ {
+ return RunElevated(m_AllowElevation);
+ }
+
+ Ec = StopService(m_ServiceName);
+ if (Ec)
+ {
+ ZEN_CONSOLE("Failed to stop service '{}'. Reason: '{}'", m_ServiceName, Ec.message());
+ return 1;
+ }
+ ZEN_CONSOLE("Stopped service '{}' successfully", m_ServiceName);
+ }
+
+ return 0;
+}
+
+} // namespace zen
diff --git a/src/zen/cmds/service_cmd.h b/src/zen/cmds/service_cmd.h
new file mode 100644
index 000000000..4d370b29c
--- /dev/null
+++ b/src/zen/cmds/service_cmd.h
@@ -0,0 +1,52 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include "../zen.h"
+
+#include <filesystem>
+
+namespace zen {
+
+class ServiceCommand : public ZenCmdBase
+{
+public:
+ static constexpr char Name[] = "service";
+ static constexpr char Description[] = "Manage zenserver as a service - status, install, uninstall, start and stop";
+
+ ServiceCommand();
+ ~ServiceCommand();
+
+ virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override;
+ virtual cxxopts::Options& Options() override { return m_Options; }
+
+private:
+ cxxopts::Options m_Options{Name, Description};
+
+ std::string m_Verb; // create, info, remove
+
+ std::string m_ServiceName = "ZenServer";
+
+ bool m_AllowElevation = false;
+
+ cxxopts::Options m_StatusOptions{"status", "Show information about an installed zenserver service"};
+
+ cxxopts::Options m_InstallOptions{
+ "install",
+ "Install zenserver as a service. Arguments following \" -- \" will be added as parameters to the installed service."};
+ std::filesystem::path m_ServerExecutable;
+#if ZEN_PLATFORM_WINDOWS
+ std::string m_ServiceDisplayName = "Unreal Zen Storage Service";
+ std::string m_ServiceDescription;
+#endif // ZEN_PLATFORM_WINDOWS
+
+ cxxopts::Options m_UninstallOptions{"uninstall", "Uninstall zenserver as a service"};
+
+ cxxopts::Options m_StartOptions{"start", "Start an installed zenserver service"};
+
+ cxxopts::Options m_StopOptions{"stop", "Start an installed zenserver service"};
+
+ cxxopts::Options* m_SubCommands[5] = {&m_StatusOptions, &m_InstallOptions, &m_UninstallOptions, &m_StartOptions, &m_StopOptions};
+};
+
+} // namespace zen
diff --git a/src/zen/zen.cpp b/src/zen/zen.cpp
index fd58b024a..f64bc96a8 100644
--- a/src/zen/zen.cpp
+++ b/src/zen/zen.cpp
@@ -16,6 +16,7 @@
#include "cmds/rpcreplay_cmd.h"
#include "cmds/run_cmd.h"
#include "cmds/serve_cmd.h"
+#include "cmds/service_cmd.h"
#include "cmds/status_cmd.h"
#include "cmds/top_cmd.h"
#include "cmds/trace_cmd.h"
@@ -429,6 +430,7 @@ main(int argc, char** argv)
VfsCommand VfsCmd;
WorkspaceCommand WorkspaceCmd;
WorkspaceShareCommand WorkspaceShareCmd;
+ ServiceCommand ServiceCmd;
const struct CommandInfo
{
@@ -484,6 +486,7 @@ main(int argc, char** argv)
{"flush", &FlushCmd, "Flush storage"},
{WorkspaceCommand::Name, &WorkspaceCmd, WorkspaceCommand::Description},
{WorkspaceShareCommand::Name, &WorkspaceShareCmd, WorkspaceShareCommand::Description},
+ {ServiceCommand::Name, &ServiceCmd, ServiceCommand::Description},
// clang-format on
};
diff --git a/src/zencore/include/zencore/process.h b/src/zencore/include/zencore/process.h
index d1394cd9a..42b997c39 100644
--- a/src/zencore/include/zencore/process.h
+++ b/src/zencore/include/zencore/process.h
@@ -51,6 +51,7 @@ struct CreateProcOptions
Flag_NewConsole = 1 << 0,
Flag_Elevated = 1 << 1,
Flag_Unelevated = 1 << 2,
+ Flag_NoConsole = 1 << 3,
};
const std::filesystem::path* WorkingDirectory = nullptr;
diff --git a/src/zencore/process.cpp b/src/zencore/process.cpp
index c51e8f69d..004b36dca 100644
--- a/src/zencore/process.cpp
+++ b/src/zencore/process.cpp
@@ -443,6 +443,10 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
{
CreationFlags |= CREATE_NEW_CONSOLE;
}
+ if (Options.Flags & CreateProcOptions::Flag_NoConsole)
+ {
+ CreationFlags |= CREATE_NO_WINDOW;
+ }
const wchar_t* WorkingDir = nullptr;
if (Options.WorkingDirectory != nullptr)
@@ -588,6 +592,10 @@ CreateProcUnelevated(const std::filesystem::path& Executable, std::string_view C
{
CreateProcFlags |= CREATE_NEW_CONSOLE;
}
+ if (Options.Flags & CreateProcOptions::Flag_NoConsole)
+ {
+ CreateProcFlags |= CREATE_NO_WINDOW;
+ }
ExtendableWideStringBuilder<256> CommandLineZ;
CommandLineZ << CommandLine;
@@ -798,6 +806,11 @@ IsProcessRunning(int pid, std::error_code& OutEc)
{
return false;
}
+ if (Error == ERROR_ACCESS_DENIED)
+ {
+ // Process is running under other user probably, assume it is running
+ return true;
+ }
OutEc = MakeErrorCode(Error);
return false;
}
@@ -840,6 +853,10 @@ IsProcessRunning(int pid, std::error_code& OutEc)
{
return false;
}
+ else if (Error == EPERM)
+ {
+ return true; // Running under a user we don't have access to, assume it is live
+ }
else
{
OutEc = MakeErrorCode(Error);
diff --git a/src/zencore/thread.cpp b/src/zencore/thread.cpp
index ab7e6857a..fef5c28a4 100644
--- a/src/zencore/thread.cpp
+++ b/src/zencore/thread.cpp
@@ -483,7 +483,7 @@ NamedMutex::Create(std::string_view MutexName)
ExtendableStringBuilder<64> Name;
Name << "/tmp/" << MutexName;
- int Inner = open(Name.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0666);
+ int Inner = open(Name.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, geteuid() == 0 ? 0766 : 0666);
if (Inner < 0)
{
return false;
diff --git a/src/zenserver/main.cpp b/src/zenserver/main.cpp
index d5419d342..f35010866 100644
--- a/src/zenserver/main.cpp
+++ b/src/zenserver/main.cpp
@@ -91,6 +91,7 @@ ZenEntryPoint::ZenEntryPoint(ZenServerOptions& ServerOptions) : m_ServerOptions(
int
ZenEntryPoint::Run()
{
+ ZEN_INFO("ZenEntryPoint::Run()");
zen::SetCurrentThreadName("main");
#if ZEN_USE_SENTRY
@@ -107,8 +108,11 @@ ZenEntryPoint::Run()
try
{
// Mutual exclusion and synchronization
+ ZEN_INFO("ZenServerState ServerState");
ZenServerState ServerState;
+ ZEN_INFO("ServerState.Initialize()");
ServerState.Initialize();
+ ZEN_INFO("ServerState.Sweep()");
ServerState.Sweep();
uint32_t AttachSponsorProcessRetriesLeft = 3;
@@ -173,6 +177,8 @@ ZenEntryPoint::Run()
}
}
+ ZEN_INFO("Preparing lock file");
+
std::error_code Ec;
std::filesystem::path LockFilePath = m_ServerOptions.DataDir / ".lock";
@@ -186,6 +192,7 @@ ZenEntryPoint::Run()
.ExecutablePath = GetRunningExecutablePath()});
};
+ ZEN_INFO("m_LockFile.Create");
m_LockFile.Create(LockFilePath, MakeLockData(false), Ec);
if (Ec)
@@ -201,6 +208,7 @@ ZenEntryPoint::Run()
}
}
+ ZEN_INFO("InitializeServerLogging");
InitializeServerLogging(m_ServerOptions);
ZEN_INFO("Command line: {}", m_ServerOptions.CommandLine);
@@ -303,6 +311,14 @@ ZenEntryPoint::Run()
ZEN_CRITICAL("Caught assert exception in main for process {}: {}", zen::GetCurrentProcessId(), AssertEx.FullDescription());
RequestApplicationExit(1);
}
+ catch (const std::system_error& e)
+ {
+ ZEN_CRITICAL("Caught system error exception in main for process {}: {} ({})",
+ zen::GetCurrentProcessId(),
+ e.what(),
+ e.code().value());
+ RequestApplicationExit(1);
+ }
catch (const std::exception& e)
{
ZEN_CRITICAL("Caught exception in main for process {}: {}", zen::GetCurrentProcessId(), e.what());
diff --git a/src/zenutil/include/zenutil/service.h b/src/zenutil/include/zenutil/service.h
new file mode 100644
index 000000000..492e5c80a
--- /dev/null
+++ b/src/zenutil/include/zenutil/service.h
@@ -0,0 +1,55 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <zenbase/zenbase.h>
+
+#include <filesystem>
+#include <optional>
+
+namespace zen {
+
+enum class ServiceLevel
+{
+ CurrentUser,
+ AllUsers,
+ SystemService
+};
+
+struct ServiceSpec
+{
+ std::filesystem::path ExecutablePath;
+ std::string CommandLineOptions;
+#if ZEN_PLATFORM_WINDOWS
+ std::string DisplayName;
+ std::string Description;
+#endif // ZEN_PLATFORM_WINDOWS
+};
+
+enum class ServiceStatus
+{
+ NotInstalled,
+ Starting,
+ Running,
+ Stopping,
+ Stopped,
+ Pausing,
+ Paused,
+ Resuming
+};
+
+struct ServiceInfo
+{
+ ServiceStatus Status;
+ ServiceSpec Spec;
+};
+
+std::string_view ToString(ServiceStatus Status);
+
+std::error_code InstallService(std::string_view ServiceName, const ServiceSpec& Spec);
+std::error_code UninstallService(std::string_view ServiceName);
+std::error_code QueryInstalledService(std::string_view ServiceName, ServiceInfo& OutInfo);
+std::error_code StartService(std::string_view ServiceName);
+std::error_code StopService(std::string_view ServiceName);
+
+} // namespace zen
diff --git a/src/zenutil/service.cpp b/src/zenutil/service.cpp
new file mode 100644
index 000000000..45874d1b5
--- /dev/null
+++ b/src/zenutil/service.cpp
@@ -0,0 +1,1025 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zenutil/service.h>
+
+#include <zencore/except.h>
+#include <zencore/process.h>
+#include <zencore/scopeguard.h>
+#include <zencore/zencore.h>
+
+#if ZEN_PLATFORM_WINDOWS
+# include <zencore/windows.h>
+#endif
+#if ZEN_PLATFORM_MAC
+# include <zencore/filesystem.h>
+# include <zencore/fmtutils.h>
+
+# include <unistd.h>
+# include <sys/stat.h>
+#endif
+#if ZEN_PLATFORM_LINUX
+# include <zencore/filesystem.h>
+# include <zencore/fmtutils.h>
+
+# include <unistd.h>
+# include <sys/stat.h>
+#endif
+
+namespace zen {
+using namespace std::literals;
+
+namespace {
+#if ZEN_PLATFORM_WINDOWS
+
+ bool SplitExecutableAndArgs(const std::wstring& ExeAndArgs, std::filesystem::path& OutExecutablePath, std::string& OutArguments)
+ {
+ if (ExeAndArgs.size())
+ {
+ if (ExeAndArgs[0] == '"')
+ {
+ std::wstring::size_type ExecutableEnd = ExeAndArgs.find('"', 1);
+ if (ExecutableEnd == std::wstring::npos)
+ {
+ OutExecutablePath = ExeAndArgs;
+ return true;
+ }
+ else
+ {
+ OutExecutablePath = ExeAndArgs.substr(0, ExecutableEnd + 1);
+ OutArguments = WideToUtf8(ExeAndArgs.substr(ExecutableEnd + 1 + ExeAndArgs[ExecutableEnd + 1] == ' ' ? 1 : 0));
+ return true;
+ }
+ }
+ else
+ {
+ std::wstring::size_type ExecutableEnd = ExeAndArgs.find(' ', 1);
+ if (ExecutableEnd == std::wstring::npos)
+ {
+ OutExecutablePath = ExeAndArgs;
+ return true;
+ }
+ else
+ {
+ OutExecutablePath = ExeAndArgs.substr(0, ExecutableEnd);
+ OutArguments = WideToUtf8(ExeAndArgs.substr(ExecutableEnd + 1));
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+#endif // ZEN_PLATFORM_WINDOWS
+
+#if ZEN_PLATFORM_MAC
+ std::vector<std::string_view> SplitArguments(std::string_view Arguments)
+ {
+ bool IsQuote = false;
+ size_t Start = 0;
+ size_t Offset = 0;
+ std::vector<std::string_view> Result;
+ for (; Offset < Arguments.length(); Offset++)
+ {
+ switch (Arguments[Offset])
+ {
+ case ' ':
+ if (IsQuote)
+ {
+ continue;
+ }
+ else if (Offset > Start)
+ {
+ Result.push_back(Arguments.substr(Start, Offset - Start));
+ Start = Offset + 1;
+ }
+ break;
+ case '"':
+ if (IsQuote)
+ {
+ IsQuote = false;
+ if (Offset - Start > 1)
+ {
+ Result.push_back(Arguments.substr(Start + 1, Offset - (Start + 1)));
+ }
+ Start = Offset + 1;
+ }
+ else
+ {
+ IsQuote = true;
+ }
+ break;
+ case '=':
+ if (IsQuote)
+ {
+ continue;
+ }
+ else if (Offset > Start)
+ {
+ Result.push_back(Arguments.substr(Start, Offset - Start));
+ Start = Offset + 1;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ ZEN_ASSERT(!IsQuote);
+ if (Offset > Start)
+ {
+ Result.push_back(Arguments.substr(Start, Offset - Start));
+ }
+ return Result;
+ }
+
+ // Needs special character escaping
+ void AppendEscaped(std::string_view String, StringBuilderBase& SB)
+ {
+ size_t Offset = 0;
+ while (Offset < String.length())
+ {
+ size_t NextEscapeCharacter = String.find_first_of("\"'<>&", Offset);
+ if (NextEscapeCharacter == std::string_view::npos)
+ {
+ break;
+ }
+ if (NextEscapeCharacter > Offset)
+ {
+ SB.Append(String.substr(Offset, NextEscapeCharacter - Offset));
+ }
+ switch (String[NextEscapeCharacter])
+ {
+ case '"':
+ SB.Append("&quot");
+ break;
+ case '\'':
+ SB.Append("&apos");
+ break;
+ case '<':
+ SB.Append("&lt");
+ break;
+ case '>':
+ SB.Append("&gt");
+ break;
+ case '&':
+ SB.Append("&amp");
+ break;
+ default:
+ ZEN_ASSERT(false);
+ break;
+ }
+ Offset = NextEscapeCharacter + 1;
+ }
+ if (Offset == 0)
+ {
+ SB.Append(String);
+ }
+ else if (String.length() > Offset)
+ {
+ SB.Append(String.substr(Offset));
+ }
+ }
+
+ std::string GetDaemonName(std::string_view ServiceName) { return fmt::format("com.epicgames.unreal.{}", ServiceName); }
+
+ std::filesystem::path GetPListPath(const std::string& DaemonName)
+ {
+ const std::filesystem::path PListFolder = "/Library/LaunchDaemons";
+ return PListFolder / (DaemonName + ".plist");
+ }
+
+ std::string BuildPlist(std::string_view ServiceName,
+ const std::filesystem::path& ExecutablePath,
+ std::string_view CommandLineOptions,
+ std::string_view DaemonName,
+ bool Debug)
+ {
+ std::vector<std::string_view> Arguments = SplitArguments(CommandLineOptions);
+ ExtendableStringBuilder<256> ProgramArguments;
+ for (const std::string_view Argument : Arguments)
+ {
+ ProgramArguments.Append(" <string>");
+ AppendEscaped(Argument, ProgramArguments);
+ ProgramArguments.Append("</string>\n");
+ }
+
+ return fmt::format(
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
+ "<plist version=\"1.0\">\n"
+ "<dict>\n"
+ " <key>Label</key>\n"
+ " <string>{}</string>\n" // DaemonName
+ " \n"
+ " <key>ProgramArguments</key>\n"
+ " <array>\n"
+ " <string>{}</string>\n" // Program name
+ "{}" // "<string>arg</string>\n" * number of arguments
+ " </array>\n"
+ " \n"
+ " <key>RunAtLoad</key>\n"
+ " <true/>\n"
+ " \n"
+ // " <key>KeepAlive</key>\n"
+ // " <true/>\n"
+ // " \n"
+ " <key>StandardOutPath</key>\n"
+ " <string>/var/log/{}.log</string>\n"
+ " \n"
+ " <key>StandardErrorPath</key>\n"
+ " <string>/var/log/{}.err.log</string>\n"
+ " \n"
+ " <key>Debug</key>\n"
+ " <{}/>\n"
+ " \n"
+ "</dict>\n"
+ "</plist>\n",
+ DaemonName,
+ ExecutablePath,
+ ProgramArguments.ToView(),
+ ServiceName,
+ ServiceName,
+ Debug ? "true"sv : "false"sv);
+
+ // "<key>Sockets</key>"
+ // "<dict>"
+ // "<key>Listeners</key>"
+ // "<dict>"
+ // "<key>SockServiceName</key>"
+ // "<string>{}</string>" // Listen socket
+ // "<key>SockType</key>"
+ // "<string>tcp</string>"
+ // "<key>SockFamily</key>"
+ // "<string>IPv4</string>"
+ // "</dict>"
+ // "</dict>"
+ }
+
+#endif // ZEN_PLATFORM_MAC
+
+#if ZEN_PLATFORM_MAC || ZEN_PLATFORM_LINUX
+
+ // TODO: Is this good enough to capture all output/errors/return codes?
+ std::pair<int, std::string> ExecuteProgram(std::string_view Cmd)
+ {
+ std::string Data;
+ const int BufferSize = 256;
+ char Buffer[BufferSize];
+ std::string Command(Cmd);
+ Command.append(" 2>&1");
+
+ ZEN_DEBUG("Running: '{}'", Command);
+
+ FILE* Stream = popen(Command.c_str(), "r");
+ if (Stream)
+ {
+ while (!feof(Stream))
+ {
+ if (fgets(Buffer, BufferSize, Stream) != NULL)
+ {
+ Data.append(Buffer);
+ }
+ }
+
+ while (!Data.empty() && isspace(Data[Data.length() - 1]))
+ {
+ Data.pop_back();
+ }
+
+ int Res = -1;
+ int Status = pclose(Stream);
+ if (Status < 0)
+ {
+ return {Status, Data};
+ }
+ uint64_t WaitMS = 100;
+ if (!WIFEXITED(Status))
+ {
+ Res = WEXITSTATUS(Status);
+ }
+ if (Res != 0 && Res != (128 + 13))
+ {
+ return {Res, Data};
+ }
+ return {0, Data};
+ }
+ return {errno, ""};
+ }
+
+#endif // ZEN_PLATFORM_MAC || ZEN_PLATFORM_LINUX
+
+#if ZEN_PLATFORM_LINUX
+ std::string GetUnitName(std::string_view ServiceName) { return fmt::format("com.epicgames.unreal.{}", ServiceName); }
+
+ std::filesystem::path GetServiceUnitPath(const std::string& UnitName)
+ {
+ const std::filesystem::path SystemUnitFolder = "/etc/systemd/system/";
+ return SystemUnitFolder / (UnitName + ".service");
+ }
+
+ std::string BuildUnitFile(std::string_view ServiceName,
+ const std::filesystem::path& ExecutablePath,
+ std::string_view CommandLineOptions,
+ std::string_view AliasName)
+ {
+ // TODO: Revise to make sure the unit file is correct
+ // TODO: Do we need a separate config file or is that optional?
+ return fmt::format(
+ "[Unit]\n"
+ "Description={}\n"
+ "Documentation=https://github.com/epicgames/zen\n"
+ "\n"
+ "DefaultDependencies=no\n"
+ "After=network.target\n"
+ "StartLimitIntervalSec=0\n"
+ "\n"
+ "[Service]\n"
+ "Type=simple\n"
+ "Restart=always\n"
+ "RestartSec=1\n"
+ "User=serviceuser\n"
+ "ExecStart={} {}\n"
+ "Restart=always\n"
+ "RuntimeDirectory={}\n"
+ "[Install]\n"
+ "Alias={}\n"
+ "WantedBy=multi-user.target",
+ ServiceName,
+ ExecutablePath,
+ CommandLineOptions,
+ ExecutablePath.parent_path(),
+ AliasName);
+ }
+
+#endif // ZEN_PLATFORM_LINUX
+} // namespace
+
+std::string_view
+ToString(ServiceStatus Status)
+{
+ switch (Status)
+ {
+ case ServiceStatus::NotInstalled:
+ return "Not installed"sv;
+ case ServiceStatus::Starting:
+ return "Starting"sv;
+ case ServiceStatus::Running:
+ return "Running"sv;
+ case ServiceStatus::Stopping:
+ return "Stopping"sv;
+ case ServiceStatus::Stopped:
+ return "Stopped"sv;
+ case ServiceStatus::Pausing:
+ return "Pausing"sv;
+ case ServiceStatus::Paused:
+ return "Paused"sv;
+ case ServiceStatus::Resuming:
+ return "Resuming"sv;
+ default:
+ ZEN_ASSERT(false);
+ return ""sv;
+ }
+}
+
+#if ZEN_PLATFORM_WINDOWS
+
+std::error_code
+InstallService(std::string_view ServiceName, const ServiceSpec& Spec)
+{
+ // TODO: Is "LocalService account" the correct account?
+ // TODO: Is ther eother parameters/security settings that we need to set up properly?
+
+ // Get a handle to the SCM database.
+ SC_HANDLE schSCManager = OpenSCManager(NULL, // local computer
+ NULL, // ServicesActive database
+ SC_MANAGER_ALL_ACCESS); // full access rights
+
+ if (NULL == schSCManager)
+ {
+ return MakeErrorCodeFromLastError();
+ }
+
+ auto _ = MakeGuard([schSCManager]() { CloseServiceHandle(schSCManager); });
+
+ // Create the service
+
+ ExtendableWideStringBuilder<128> Name;
+ Utf8ToWide(ServiceName, Name);
+
+ ExtendableWideStringBuilder<128> DisplayName;
+ Utf8ToWide(Spec.DisplayName, DisplayName);
+
+ ExtendableWideStringBuilder<128> Path;
+ Path.Append(Spec.ExecutablePath.c_str());
+ if (!Spec.CommandLineOptions.empty())
+ {
+ Path.AppendAscii(" ");
+ Utf8ToWide(Spec.CommandLineOptions, Path);
+ }
+
+ SC_HANDLE schService = CreateService(schSCManager, // SCM database
+ Name.c_str(), // name of service
+ DisplayName.c_str(), // service name to display
+ SERVICE_ALL_ACCESS, // desired access
+ SERVICE_WIN32_OWN_PROCESS, // service type
+ SERVICE_AUTO_START, // start type
+ SERVICE_ERROR_NORMAL, // error control type
+ Path.c_str(), // path to service's binary
+ NULL, // no load ordering group
+ NULL, // no tag identifier
+ NULL, // no dependencies
+ TEXT("NT AUTHORITY\\LocalService"), // LocalService account
+ NULL); // no password
+
+ if (schService == NULL)
+ {
+ return MakeErrorCodeFromLastError();
+ }
+
+ if (!Spec.Description.empty())
+ {
+ ExtendableWideStringBuilder<128> DescriptionBuilder;
+ Utf8ToWide(Spec.Description, DescriptionBuilder);
+
+ SERVICE_DESCRIPTION Description;
+ Description.lpDescription = const_cast<wchar_t*>(DescriptionBuilder.c_str());
+ if (!ChangeServiceConfig2(schService, SERVICE_CONFIG_DESCRIPTION, &Description))
+ {
+ return MakeErrorCodeFromLastError();
+ }
+ }
+
+ CloseServiceHandle(schService);
+
+ return {};
+}
+
+std::error_code
+UninstallService(std::string_view ServiceName)
+{
+ // Get a handle to the SCM database.
+ SC_HANDLE schSCManager = OpenSCManager(NULL, // local computer
+ NULL, // ServicesActive database
+ SC_MANAGER_ALL_ACCESS); // full access rights
+
+ if (NULL == schSCManager)
+ {
+ return MakeErrorCodeFromLastError();
+ }
+
+ auto _ = MakeGuard([schSCManager]() { CloseServiceHandle(schSCManager); });
+
+ // Get a handle to the service.
+
+ ExtendableWideStringBuilder<128> Name;
+ Utf8ToWide(ServiceName, Name);
+
+ SC_HANDLE schService = OpenService(schSCManager, // SCM database
+ Name.c_str(), // name of service
+ DELETE); // need delete access
+
+ if (schService == NULL)
+ {
+ DWORD Error = ::GetLastError();
+ if (Error == ERROR_SERVICE_DOES_NOT_EXIST)
+ {
+ return {};
+ }
+ return MakeErrorCode(Error);
+ }
+ auto __ = MakeGuard([schService]() { CloseServiceHandle(schService); });
+
+ // Delete the service.
+
+ if (!DeleteService(schService))
+ {
+ return MakeErrorCodeFromLastError();
+ }
+
+ return {};
+}
+
+std::error_code
+QueryInstalledService(std::string_view ServiceName, ServiceInfo& OutInfo)
+{
+ // Get a handle to the SCM database.
+ SC_HANDLE schSCManager = OpenSCManager(NULL, // local computer
+ NULL, // ServicesActive database
+ SC_MANAGER_CONNECT); // standard access rights
+
+ if (NULL == schSCManager)
+ {
+ return MakeErrorCodeFromLastError();
+ }
+
+ auto _ = MakeGuard([schSCManager]() { CloseServiceHandle(schSCManager); });
+
+ // Get a handle to the service.
+
+ ExtendableWideStringBuilder<128> Name;
+ Utf8ToWide(ServiceName, Name);
+
+ SC_HANDLE schService = OpenService(schSCManager, // SCM database
+ Name.c_str(), // name of service
+ SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG); // need delete access
+
+ if (schService == NULL)
+ {
+ DWORD Error = ::GetLastError();
+ if (Error == ERROR_SERVICE_DOES_NOT_EXIST)
+ {
+ OutInfo.Status = ServiceStatus::NotInstalled;
+ return {};
+ }
+ return MakeErrorCode(Error);
+ }
+ auto __ = MakeGuard([schService]() { CloseServiceHandle(schService); });
+
+ std::vector<std::uint8_t> Buffer(8192);
+ QUERY_SERVICE_CONFIG* ServiceConfig = reinterpret_cast<QUERY_SERVICE_CONFIG*>(Buffer.data());
+ DWORD BytesNeeded = 0;
+ if (!QueryServiceConfig(schService, ServiceConfig, (DWORD)Buffer.size(), &BytesNeeded))
+ {
+ return MakeErrorCodeFromLastError();
+ }
+
+ std::wstring BinaryWithArguments(ServiceConfig->lpBinaryPathName);
+ (void)SplitExecutableAndArgs(BinaryWithArguments, OutInfo.Spec.ExecutablePath, OutInfo.Spec.CommandLineOptions);
+ OutInfo.Spec.DisplayName = WideToUtf8(ServiceConfig->lpDisplayName);
+
+ SERVICE_STATUS ServiceStatus;
+ if (!::QueryServiceStatus(schService, &ServiceStatus))
+ {
+ return MakeErrorCodeFromLastError();
+ }
+
+ switch (ServiceStatus.dwCurrentState)
+ {
+ case SERVICE_STOPPED:
+ OutInfo.Status = ServiceStatus::Stopped;
+ break;
+ case SERVICE_START_PENDING:
+ OutInfo.Status = ServiceStatus::Starting;
+ break;
+ case SERVICE_STOP_PENDING:
+ OutInfo.Status = ServiceStatus::Stopping;
+ break;
+ case SERVICE_RUNNING:
+ OutInfo.Status = ServiceStatus::Running;
+ break;
+ case SERVICE_CONTINUE_PENDING:
+ OutInfo.Status = ServiceStatus::Resuming;
+ break;
+ case SERVICE_PAUSE_PENDING:
+ OutInfo.Status = ServiceStatus::Pausing;
+ break;
+ case SERVICE_PAUSED:
+ OutInfo.Status = ServiceStatus::Paused;
+ break;
+ default:
+ throw std::runtime_error(fmt::format("Unknown service status for '{}': {}", ServiceName, ServiceStatus.dwCurrentState));
+ }
+
+ if (!QueryServiceConfig2(schService, SERVICE_CONFIG_DESCRIPTION, Buffer.data(), (DWORD)Buffer.size(), &BytesNeeded))
+ {
+ DWORD Error = ::GetLastError();
+ if (Error == ERROR_INSUFFICIENT_BUFFER)
+ {
+ Buffer.resize((size_t)BytesNeeded);
+ if (!QueryServiceConfig2(schService, SERVICE_CONFIG_DESCRIPTION, Buffer.data(), (DWORD)Buffer.size(), &BytesNeeded))
+ {
+ return MakeErrorCodeFromLastError();
+ }
+ }
+ else
+ {
+ return MakeErrorCode(Error);
+ }
+ }
+ SERVICE_DESCRIPTION* Description = (SERVICE_DESCRIPTION*)Buffer.data();
+ if (Description->lpDescription != NULL)
+ {
+ OutInfo.Spec.Description = WideToUtf8(std::wstring(Description->lpDescription));
+ }
+
+ return {};
+}
+
+std::error_code
+StartService(std::string_view ServiceName)
+{
+ // Get a handle to the SCM database.
+ SC_HANDLE schSCManager = OpenSCManager(NULL, // local computer
+ NULL, // ServicesActive database
+ SC_MANAGER_CONNECT); // default access rights
+
+ if (NULL == schSCManager)
+ {
+ return MakeErrorCodeFromLastError();
+ }
+
+ auto _ = MakeGuard([schSCManager]() { CloseServiceHandle(schSCManager); });
+
+ // Get a handle to the service.
+
+ ExtendableWideStringBuilder<128> Name;
+ Utf8ToWide(ServiceName, Name);
+
+ SC_HANDLE schService = OpenService(schSCManager, // SCM database
+ Name.c_str(), // name of service
+ SERVICE_START); // need start access
+
+ if (schService == NULL)
+ {
+ return MakeErrorCodeFromLastError();
+ }
+ auto __ = MakeGuard([schService]() { CloseServiceHandle(schService); });
+
+ // Start the service.
+
+ if (!::StartService(schService, 0, NULL))
+ {
+ return MakeErrorCodeFromLastError();
+ }
+
+ return {};
+}
+
+std::error_code
+StopService(std::string_view ServiceName)
+{
+ // Get a handle to the SCM database.
+ SC_HANDLE schSCManager = OpenSCManager(NULL, // local computer
+ NULL, // ServicesActive database
+ SC_MANAGER_CONNECT); // default access rights
+
+ if (NULL == schSCManager)
+ {
+ return MakeErrorCodeFromLastError();
+ }
+
+ auto _ = MakeGuard([schSCManager]() { CloseServiceHandle(schSCManager); });
+
+ // Get a handle to the service.
+
+ ExtendableWideStringBuilder<128> Name;
+ Utf8ToWide(ServiceName, Name);
+
+ SC_HANDLE schService = OpenService(schSCManager, // SCM database
+ Name.c_str(), // name of service
+ SERVICE_STOP); // need start access
+
+ if (schService == NULL)
+ {
+ return MakeErrorCodeFromLastError();
+ }
+ auto __ = MakeGuard([schService]() { CloseServiceHandle(schService); });
+
+ // Stop the service.
+ SERVICE_STATUS ServiceStatus;
+ if (!::ControlService(schService, SERVICE_CONTROL_STOP, &ServiceStatus))
+ {
+ return MakeErrorCodeFromLastError();
+ }
+
+ return {};
+}
+
+#endif
+
+#if ZEN_PLATFORM_MAC
+
+std::error_code
+InstallService(std::string_view ServiceName, const ServiceSpec& Spec)
+{
+ // TODO: Do we need to create a separate user for the service or is running as the default service user OK?
+ const std::string DaemonName = GetDaemonName(ServiceName);
+ std::string PList = BuildPlist(ServiceName, Spec.ExecutablePath, Spec.CommandLineOptions, DaemonName, true);
+
+ const std::filesystem::path PListPath = GetPListPath(DaemonName);
+ ZEN_INFO("Writing launchd plist to {}", PListPath.string());
+ try
+ {
+ zen::WriteFile(PListPath, IoBuffer(IoBuffer::Wrap, PList.data(), PList.size()));
+ }
+ catch (const std::system_error& Ex)
+ {
+ return MakeErrorCode(Ex.code().value());
+ }
+
+ ZEN_INFO("Changing permissions to 644 for {}", PListPath.string());
+ if (chmod(PListPath.string().c_str(), 0644) == -1)
+ {
+ return MakeErrorCodeFromLastError();
+ }
+ return {};
+}
+
+std::error_code
+UninstallService(std::string_view ServiceName)
+{
+ const std::string DaemonName = GetDaemonName(ServiceName);
+ const std::filesystem::path PListPath = GetPListPath(DaemonName);
+ ZEN_INFO("Attempting to remove launchd plist from {}", PListPath.string());
+ std::error_code Ec;
+ std::filesystem::remove(PListPath, Ec);
+ return Ec;
+}
+
+std::error_code
+QueryInstalledService(std::string_view ServiceName, ServiceInfo& OutInfo)
+{
+ OutInfo.Status = ServiceStatus::NotInstalled;
+ const std::string DaemonName = GetDaemonName(ServiceName);
+
+ const std::filesystem::path PListPath = GetPListPath(DaemonName);
+ if (std::filesystem::is_regular_file(PListPath))
+ {
+ OutInfo.Status = ServiceStatus::Stopped;
+
+ {
+ // Parse plist :(
+ IoBuffer Buffer = ReadFile(PListPath).Flatten();
+ MemoryView Data = Buffer.GetView();
+ std::string PList((const char*)Data.GetData(), Data.GetSize());
+
+ enum class ParseMode
+ {
+ None,
+ ExpectingProgramArgumentsArray,
+ ExpectingProgramExecutablePath,
+ ExpectingCommandLineOption
+ };
+
+ ParseMode Mode = ParseMode::None;
+
+ ForEachStrTok(PList, '\n', [&](std::string_view Line) {
+ switch (Mode)
+ {
+ case ParseMode::None:
+ {
+ if (Line.find("<key>ProgramArguments</key>") != std::string_view::npos)
+ {
+ Mode = ParseMode::ExpectingProgramArgumentsArray;
+ return true;
+ }
+ }
+ break;
+ case ParseMode::ExpectingProgramArgumentsArray:
+ {
+ if (Line.find("<array>") != std::string_view::npos)
+ {
+ Mode = ParseMode::ExpectingProgramExecutablePath;
+ return true;
+ }
+ Mode = ParseMode::None;
+ }
+ break;
+ case ParseMode::ExpectingProgramExecutablePath:
+ {
+ if (std::string_view::size_type ArgStart = Line.find("<string>"); ArgStart != std::string_view::npos)
+ {
+ ArgStart += 8;
+ if (std::string_view::size_type ArgEnd = Line.find("</string>", ArgStart); ArgEnd != std::string_view::npos)
+ {
+ std::string_view ProgramString = Line.substr(ArgStart, ArgEnd - ArgStart);
+ OutInfo.Spec.ExecutablePath = ProgramString;
+ Mode = ParseMode::ExpectingCommandLineOption;
+ return true;
+ }
+ }
+ Mode = ParseMode::None;
+ }
+ break;
+ case ParseMode::ExpectingCommandLineOption:
+ {
+ if (std::string_view::size_type ArgStart = Line.find("</array>"); ArgStart != std::string_view::npos)
+ {
+ Mode = ParseMode::None;
+ return true;
+ }
+ else if (std::string_view::size_type ArgStart = Line.find("<string>"); ArgStart != std::string_view::npos)
+ {
+ ArgStart += 8;
+ if (std::string_view::size_type ArgEnd = Line.find("</string>", ArgStart); ArgEnd != std::string_view::npos)
+ {
+ std::string_view ArgumentString = Line.substr(ArgStart, ArgEnd - ArgStart);
+ if (!OutInfo.Spec.CommandLineOptions.empty())
+ {
+ OutInfo.Spec.CommandLineOptions += " ";
+ }
+ OutInfo.Spec.CommandLineOptions += ArgumentString;
+ return true;
+ }
+ }
+ Mode = ParseMode::None;
+ }
+ break;
+ }
+ return true;
+ });
+ }
+ {
+ std::pair<int, std::string> Res = ExecuteProgram(fmt::format("launchctl list {}", DaemonName));
+ if (Res.first == 0 && !Res.second.empty())
+ {
+ ForEachStrTok(Res.second, '\n', [&](std::string_view Line) {
+ if (Line.find("\"PID\"") != std::string_view::npos)
+ {
+ std::string_view::size_type PidStart = Line.find('=');
+ std::string_view::size_type PidEnd = Line.find(';');
+ std::string_view PidString = Line.substr(PidStart + 2, PidEnd - (PidStart + 2));
+ if (ParseInt<int>(PidString).has_value())
+ {
+ OutInfo.Status = ServiceStatus::Running;
+ }
+ return false;
+ }
+ return true;
+ });
+ // Parse installed info
+ }
+ }
+ }
+
+ return {};
+}
+
+std::error_code
+StartService(std::string_view ServiceName)
+{
+ const std::string DaemonName = GetDaemonName(ServiceName);
+ const std::filesystem::path PListPath = GetPListPath(DaemonName);
+
+ std::pair<int, std::string> Res = ExecuteProgram(fmt::format("launchctl bootstrap system {}", PListPath));
+ if (Res.first != 0)
+ {
+ return MakeErrorCode(Res.first);
+ }
+
+ return {};
+}
+
+std::error_code
+StopService(std::string_view ServiceName)
+{
+ const std::string DaemonName = GetDaemonName(ServiceName);
+ const std::filesystem::path PListPath = GetPListPath(DaemonName);
+
+ std::pair<int, std::string> Res = ExecuteProgram(fmt::format("launchctl bootout system ", PListPath.));
+ if (Res.first != 0)
+ {
+ return MakeErrorCode(Res.first);
+ }
+
+ return {};
+}
+
+#endif // ZEN_PLATFORM_MAC
+
+#if ZEN_PLATFORM_LINUX
+
+std::error_code
+InstallService(std::string_view ServiceName, const ServiceSpec& Spec)
+{
+ // TODO: Do we need to create a separate user for the service or is running as root OK?
+
+ const std::string UnitName = GetUnitName(ServiceName);
+ const std::filesystem::path ServiceUnitPath = GetServiceUnitPath(UnitName);
+
+ std::string UnitFile = BuildUnitFile(ServiceName, Spec.ExecutablePath, Spec.CommandLineOptions, UnitName);
+ ZEN_DEBUG("Writing systemd unit file to {}", ServiceUnitPath.string());
+ try
+ {
+ zen::WriteFile(ServiceUnitPath, IoBuffer(IoBuffer::Wrap, UnitFile.data(), UnitFile.size()));
+ }
+ catch (const std::system_error& Ex)
+ {
+ return MakeErrorCode(Ex.code().value());
+ }
+
+ ZEN_DEBUG("Changing permissions to 644 for {}", ServiceUnitPath.string());
+ if (chmod(ServiceUnitPath.string().c_str(), 0644) == -1)
+ {
+ return MakeErrorCodeFromLastError();
+ }
+
+ std::pair<int, std::string> Res = ExecuteProgram("systemctl daemon-reload");
+ if (Res.first != 0 && Res.first != -1)
+ {
+ ZEN_ERROR("systemctl daemon-reload failed with {}: '{}'", Res.first, Res.second);
+ return MakeErrorCode(Res.first);
+ }
+
+ Res = ExecuteProgram(fmt::format("systemctl enable {}", UnitName));
+ if (Res.first != 0 && Res.first != -1)
+ {
+ ZEN_ERROR("systemctl enable failed with {}: '{}'", Res.first, Res.second);
+ return MakeErrorCode(Res.first);
+ }
+
+ return {};
+}
+
+std::error_code
+UninstallService(std::string_view ServiceName)
+{
+ const std::string UnitName = GetUnitName(ServiceName);
+ const std::filesystem::path ServiceUnitPath = GetServiceUnitPath(UnitName);
+
+ std::pair<int, std::string> Res = ExecuteProgram(fmt::format("systemctl disable {}", UnitName));
+ if (Res.first != 0 && Res.first != -1)
+ {
+ ZEN_ERROR("systemctl disable failed with {}: '{}'", Res.first, Res.second);
+ return MakeErrorCode(Res.first);
+ }
+
+ ZEN_DEBUG("Attempting to remove systemd unit file from {}", ServiceUnitPath.string());
+ std::error_code Ec;
+ std::filesystem::remove(ServiceUnitPath, Ec);
+ if (Ec)
+ {
+ ZEN_ERROR("failed to remove {}: '{}'", ServiceUnitPath, Ec.message());
+ return Ec;
+ }
+
+ Res = ExecuteProgram("systemctl daemon-reload");
+ if (Res.first != 0 && Res.first != -1)
+ {
+ ZEN_ERROR("systemctl daemon-reload failed with {}: '{}'", Res.first, Res.second);
+ return MakeErrorCode(Res.first);
+ }
+
+ return {};
+}
+
+std::error_code
+QueryInstalledService(std::string_view ServiceName, ServiceInfo& OutInfo)
+{
+ const std::string UnitName = GetUnitName(ServiceName);
+ const std::filesystem::path ServiceUnitPath = GetServiceUnitPath(UnitName);
+
+ OutInfo.Status = ServiceStatus::NotInstalled;
+
+ if (std::filesystem::is_regular_file(ServiceUnitPath))
+ {
+ OutInfo.Status = ServiceStatus::Stopped;
+ // TODO: Read and parse unit file ?
+
+ std::pair<int, std::string> Res = ExecuteProgram(fmt::format("systemctl status {}", UnitName));
+ if (Res.first == 0)
+ {
+ // TODO: What does status really return and what info can we use here to get the ServiceInfo complete?
+ OutInfo.Status = ServiceStatus::Running;
+ }
+ else
+ {
+ ZEN_DEBUG("systemctl status failed with '{}"({}), Res.second, Res.first);
+ }
+ }
+
+ return {};
+}
+
+std::error_code
+StartService(std::string_view ServiceName)
+{
+ // TODO: Starting the service returns -1 from ExecuteProgram, so the service won't start
+ // TODO: Running start from command line gives no output but the service does not start - not sure what is wrong.
+ // Starting the same command line for the service using `sudo` *will* start the service sucessfully so I
+ // assume that the Unit file or some config is wrong/missing.
+
+ const std::string UnitName = GetUnitName(ServiceName);
+ const std::filesystem::path ServiceUnitPath = GetServiceUnitPath(UnitName);
+
+ std::pair<int, std::string> Res = ExecuteProgram(fmt::format("systemctl start {}", UnitName));
+ if (Res.first != 0)
+ {
+ ZEN_ERROR("service start failed with {}: '{}'", Res.first, Res.second);
+ return MakeErrorCode(Res.first);
+ }
+
+ return {};
+}
+
+std::error_code
+StopService(std::string_view ServiceName)
+{
+ // TODO: Stopping the service returns -1 from ExecuteProgram, maybe this ie expected as I have yet to successfully start the service
+ // using systemctl start
+
+ const std::string UnitName = GetUnitName(ServiceName);
+ const std::filesystem::path ServiceUnitPath = GetServiceUnitPath(UnitName);
+
+ std::pair<int, std::string> Res = ExecuteProgram(fmt::format("systemctl stop {}", UnitName));
+ if (Res.first != 0)
+ {
+ ZEN_ERROR("service stop failed with {}: '{}'", Res.first, Res.second);
+ return MakeErrorCode(Res.first);
+ }
+
+ return {};
+}
+
+#endif // ZEN_PLATFORM_LINUX
+
+} // namespace zen
diff --git a/src/zenutil/zenserverprocess.cpp b/src/zenutil/zenserverprocess.cpp
index b36f11741..11fcce02f 100644
--- a/src/zenutil/zenserverprocess.cpp
+++ b/src/zenutil/zenserverprocess.cpp
@@ -167,7 +167,8 @@ ZenServerState::Initialize()
ThrowLastError("Could not map view of Zen server state");
}
#else
- int Fd = shm_open("/UnrealEngineZen", O_RDWR | O_CREAT | O_CLOEXEC, 0666);
+ ZEN_INFO("{}", S_IRUSR | S_IWUSR | S_IXUSR);
+ int Fd = shm_open("/UnrealEngineZen", O_RDWR | O_CREAT | O_CLOEXEC, geteuid() == 0 ? 0766 : 0666);
if (Fd < 0)
{
ThrowLastError("Could not open a shared memory object");