aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLiam Mitchell <[email protected]>2025-08-22 16:12:38 -0700
committerGitHub Enterprise <[email protected]>2025-08-22 16:12:38 -0700
commit207c4a32612891711d9d69466b6dfc653428bb07 (patch)
treed4b8de42a91ee3327b14fc0aa66c92bc3de46555 /src
parent5.6.18-pre0 (diff)
parentMove windows service utilities to zenutil and fix clang-format errors (diff)
downloadzen-207c4a32612891711d9d69466b6dfc653428bb07.tar.xz
zen-207c4a32612891711d9d69466b6dfc653428bb07.zip
Merge pull request #139 from ue-foundation/de/zen-service-command
zen service command
Diffstat (limited to 'src')
-rw-r--r--src/zen/cmds/service_cmd.cpp609
-rw-r--r--src/zen/cmds/service_cmd.h55
-rw-r--r--src/zen/xmake.lua3
-rw-r--r--src/zen/zen.cpp3
-rw-r--r--src/zencore-test/zencore-test.cpp5
-rw-r--r--src/zencore/filesystem.cpp2
-rw-r--r--src/zencore/include/zencore/memory/fmalloc.h2
-rw-r--r--src/zencore/include/zencore/process.h5
-rw-r--r--src/zencore/memory/mallocmimalloc.cpp2
-rw-r--r--src/zencore/process.cpp27
-rw-r--r--src/zencore/thread.cpp2
-rw-r--r--src/zenhttp-test/zenhttp-test.cpp5
-rw-r--r--src/zennet-test/zennet-test.cpp5
-rw-r--r--src/zenserver-test/zenserver-test.cpp4
-rw-r--r--src/zenserver/main.cpp22
-rw-r--r--src/zenstore-test/zenstore-test.cpp5
-rw-r--r--src/zenutil-test/zenutil-test.cpp5
-rw-r--r--src/zenutil/chunking.cpp1
-rw-r--r--src/zenutil/chunkrequests.cpp2
-rw-r--r--src/zenutil/include/zenutil/service.h58
-rw-r--r--src/zenutil/include/zenutil/windows/service.h (renamed from src/zenserver/windows/service.h)4
-rw-r--r--src/zenutil/service.cpp1120
-rw-r--r--src/zenutil/windows/service.cpp (renamed from src/zenserver/windows/service.cpp)11
-rw-r--r--src/zenutil/xmake.lua8
-rw-r--r--src/zenutil/zenserverprocess.cpp17
25 files changed, 1962 insertions, 20 deletions
diff --git a/src/zen/cmds/service_cmd.cpp b/src/zen/cmds/service_cmd.cpp
new file mode 100644
index 000000000..3847c423d
--- /dev/null
+++ b/src/zen/cmds/service_cmd.cpp
@@ -0,0 +1,609 @@
+// 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>
+#include <filesystem>
+
+#if ZEN_PLATFORM_WINDOWS
+# include <zencore/windows.h>
+# include <shellapi.h>
+# include <Shlwapi.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::wstring CommandArguments(PathGetArgs(CommandLine.data()));
+
+ 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.");
+ ReturnCode = DWORD(-1);
+ }
+ 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>");
+
+ m_InstallOptions.add_option("",
+ "",
+ "full",
+ fmt::format("Uninstall a running service and update service binaries before installing"),
+ cxxopts::value(m_FullInstall),
+ "<full>");
+ m_InstallOptions.add_option("",
+ "",
+ "install-path",
+ fmt::format("Path in which to install service binaries"),
+ cxxopts::value(m_InstallPath),
+ "<install-path>");
+
+ m_InstallOptions.add_option("", "u", "user", "User to run service as, defaults to current user", cxxopts::value(m_UserName), "<user>");
+#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;
+}
+
+enum class ServiceStatusReturnCode
+{
+ Running = 0, // successful return indicates proper installation and everything running smoothly
+ NotInstalled,
+ NotRunning,
+ UnknownError
+};
+
+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 gsl::narrow<int>(ServiceStatusReturnCode::UnknownError);
+ }
+ if (Info.Status == ServiceStatus::NotInstalled)
+ {
+ ZEN_CONSOLE("Service '{}' is not installed", m_ServiceName);
+ return gsl::narrow<int>(ServiceStatusReturnCode::NotInstalled);
+ }
+ else if (Info.Status != ServiceStatus::Running)
+ {
+ ZEN_CONSOLE("Service '{}' is not running", m_ServiceName);
+ return gsl::narrow<int>(ServiceStatusReturnCode::NotRunning);
+ }
+ else
+ {
+ ZEN_CONSOLE("Service '{}':\n{}", m_ServiceName, FmtServiceInfo(Info, " "));
+ }
+ }
+
+ if (SubOption == &m_InstallOptions)
+ {
+ if (!IsElevated())
+ {
+ return RunElevated(m_AllowElevation);
+ }
+
+ ServiceInfo Info;
+ std::error_code Ec = QueryInstalledService(m_ServiceName, Info);
+ if (!Ec && Info.Status != ServiceStatus::NotInstalled)
+ {
+ if (m_FullInstall)
+ {
+ if (Info.Status == ServiceStatus::Running)
+ {
+ Ec = StopService(m_ServiceName);
+ if (Ec)
+ {
+ ZEN_CONSOLE("Failed to stop service '{}' using '{}'. Reason: '{}'",
+ m_ServiceName,
+ m_ServerExecutable,
+ Ec.message());
+
+ return 1;
+ }
+
+ int Timeout = 30000; // Wait up to 30 seconds for the service to fully stop before uninstalling
+ while (Timeout > 0)
+ {
+ Ec = QueryInstalledService(m_ServiceName, Info);
+ if (Ec)
+ {
+ ZEN_CONSOLE("Failed to wait for service to stop: '{}'", Ec.message());
+ return 1;
+ }
+
+ if (Info.Status == ServiceStatus::Stopped)
+ {
+ break;
+ }
+
+ Sleep(100);
+ Timeout -= 100;
+ }
+
+ if (Info.Status != ServiceStatus::Stopped)
+ {
+ ZEN_CONSOLE("Timed out waiting for service to stop");
+ return 1;
+ }
+ }
+
+ Ec = UninstallService(m_ServiceName);
+ if (Ec)
+ {
+ ZEN_CONSOLE("Failed to uninstall running service '{}' using '{}'. Reason: '{}'",
+ m_ServiceName,
+ m_ServerExecutable,
+ Ec.message());
+
+ return 1;
+ }
+ }
+ else
+ {
+ ZEN_CONSOLE("Service '{}' already installed:\n{}", m_ServiceName, FmtServiceInfo(Info, " "));
+ return 1;
+ }
+ }
+ 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);
+
+ if (m_FullInstall)
+ {
+ if (m_InstallPath.empty())
+ {
+ throw zen::OptionParseException("--full requires --install-path to be specified");
+ }
+
+ std::filesystem::path ExePath = zen::GetRunningExecutablePath();
+
+ std::filesystem::path FilesToCopy[] = {
+ ExePath,
+ m_ServerExecutable,
+#if ZEN_PLATFORM_WINDOWS
+ ExePath.replace_extension("pdb"),
+ m_ServerExecutable.replace_extension("pdb"),
+ ExePath
+ .replace_filename("crashpad_handler.exe")
+#endif
+#if ZEN_PLATFORM_MAC
+ ExePath.replace_filename("crashpad_handler")
+#endif
+ };
+
+ std::filesystem::path DestinationExePath = m_InstallPath / ExePath.filename();
+ std::filesystem::path DestinationServerPath = m_InstallPath / m_ServerExecutable.filename();
+
+ if (!std::filesystem::is_directory(m_InstallPath) && !CreateDirectories(m_InstallPath))
+ {
+ ZEN_CONSOLE("Unable to create install directory '{}'", m_InstallPath);
+ return 1;
+ }
+
+ for (const std::filesystem::path& File : FilesToCopy)
+ {
+ std::filesystem::path Destination = m_InstallPath / File.filename();
+ if (!CopyFile(File, Destination, {.EnableClone = false}))
+ {
+ ZEN_CONSOLE("Failed to copy '{}' to '{}'", File, Destination);
+ return 1;
+ }
+
+ ZEN_INFO("Copied '{}' to '{}'", File, Destination);
+
+ if (File.extension() != "pdb")
+ {
+ std::filesystem::permissions(Destination, std::filesystem::perms::owner_exec, std::filesystem::perm_options::add);
+ }
+ }
+
+ m_ServerExecutable = m_InstallPath / m_ServerExecutable.filename();
+ }
+
+ Ec = InstallService(
+ m_ServiceName,
+ ServiceSpec {
+ .ExecutablePath = m_ServerExecutable, .CommandLineOptions = GlobalOptions.PassthroughCommandLine, .UserName = m_UserName
+#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 (m_FullInstall)
+ {
+ Ec = StartService(m_ServiceName);
+ if (Ec)
+ {
+ ZEN_CONSOLE("Failed to start service '{}'. Reason: '{}'", m_ServiceName, Ec.message());
+ return 1;
+ }
+ }
+ }
+
+ 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..359e8e854
--- /dev/null
+++ b/src/zen/cmds/service_cmd.h
@@ -0,0 +1,55 @@
+// 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";
+ std::string m_UserName;
+
+ 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;
+ std::filesystem::path m_InstallPath;
+ bool m_FullInstall = false;
+#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/xmake.lua b/src/zen/xmake.lua
index bf595a21d..dadda32dd 100644
--- a/src/zen/xmake.lua
+++ b/src/zen/xmake.lua
@@ -17,8 +17,7 @@ target("zen")
add_files("zen.rc")
add_ldflags("/subsystem:console,5.02")
add_ldflags("/LTCG")
- add_links("crypt32", "wldap32", "Ws2_32")
-
+ add_links("crypt32", "wldap32", "Ws2_32", "Shlwapi")
add_links("dbghelp", "winhttp", "version") -- for Sentry
end
diff --git a/src/zen/zen.cpp b/src/zen/zen.cpp
index 6376bbad2..accd0d105 100644
--- a/src/zen/zen.cpp
+++ b/src/zen/zen.cpp
@@ -17,6 +17,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"
@@ -695,6 +696,7 @@ main(int argc, char** argv)
WipeCommand WipeCmd;
WorkspaceCommand WorkspaceCmd;
WorkspaceShareCommand WorkspaceShareCmd;
+ ServiceCommand ServiceCmd;
const struct CommandInfo
{
@@ -752,6 +754,7 @@ main(int argc, char** argv)
{WipeCommand::Name, &WipeCmd, WipeCommand::Description},
{WorkspaceCommand::Name, &WorkspaceCmd, WorkspaceCommand::Description},
{WorkspaceShareCommand::Name, &WorkspaceShareCmd, WorkspaceShareCommand::Description},
+ {ServiceCommand::Name, &ServiceCmd, ServiceCommand::Description},
// clang-format on
};
diff --git a/src/zencore-test/zencore-test.cpp b/src/zencore-test/zencore-test.cpp
index 928c4e5c2..0f64f4a5f 100644
--- a/src/zencore-test/zencore-test.cpp
+++ b/src/zencore-test/zencore-test.cpp
@@ -13,6 +13,7 @@
#if ZEN_WITH_TESTS
# define ZEN_TEST_WITH_RUNNER 1
# include <zencore/testing.h>
+# include <zencore/process.h>
#endif
int
@@ -21,6 +22,10 @@ main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
#if ZEN_WITH_TESTS
zen::zencore_forcelinktests();
+# if ZEN_PLATFORM_LINUX
+ zen::IgnoreChildSignals();
+# endif
+
zen::TraceInit("zencore-test");
zen::logging::InitializeLogging();
zen::MaximizeOpenFileCount();
diff --git a/src/zencore/filesystem.cpp b/src/zencore/filesystem.cpp
index 46337ffc8..8327838c9 100644
--- a/src/zencore/filesystem.cpp
+++ b/src/zencore/filesystem.cpp
@@ -900,6 +900,8 @@ CopyFile(const std::filesystem::path& FromPath, const std::filesystem::path& ToP
size_t FileSizeBytes = Stat.st_size;
+ fchown(ToFd, Stat.st_uid, Stat.st_gid);
+
// Copy impl
const size_t BufferSize = Min(FileSizeBytes, 64u << 10);
void* Buffer = malloc(BufferSize);
diff --git a/src/zencore/include/zencore/memory/fmalloc.h b/src/zencore/include/zencore/memory/fmalloc.h
index aeb05b651..5b476429e 100644
--- a/src/zencore/include/zencore/memory/fmalloc.h
+++ b/src/zencore/include/zencore/memory/fmalloc.h
@@ -2,6 +2,8 @@
#pragma once
+#include <cstddef>
+
#include <zenbase/zenbase.h>
namespace zen {
diff --git a/src/zencore/include/zencore/process.h b/src/zencore/include/zencore/process.h
index d3e1de703..98d352db6 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;
@@ -100,6 +101,10 @@ int GetProcessId(CreateProcResult ProcId);
std::filesystem::path GetProcessExecutablePath(int Pid, std::error_code& OutEc);
std::error_code FindProcess(const std::filesystem::path& ExecutableImage, ProcessHandle& OutHandle, bool IncludeSelf = true);
+#if ZEN_PLATFORM_LINUX
+void IgnoreChildSignals();
+#endif
+
void process_forcelink(); // internal
} // namespace zen
diff --git a/src/zencore/memory/mallocmimalloc.cpp b/src/zencore/memory/mallocmimalloc.cpp
index 1919af3bf..1f9aff404 100644
--- a/src/zencore/memory/mallocmimalloc.cpp
+++ b/src/zencore/memory/mallocmimalloc.cpp
@@ -1,5 +1,7 @@
// Copyright Epic Games, Inc. All Rights Reserved.
+#include <cstring>
+
#include <zencore/intmath.h>
#include <zencore/memory/align.h>
#include <zencore/memory/mallocmimalloc.h>
diff --git a/src/zencore/process.cpp b/src/zencore/process.cpp
index fcbe657cb..0b25d14f4 100644
--- a/src/zencore/process.cpp
+++ b/src/zencore/process.cpp
@@ -24,7 +24,6 @@
# include <sys/sem.h>
# include <sys/stat.h>
# include <sys/syscall.h>
-# include <sys/sysctl.h>
# include <sys/wait.h>
# include <time.h>
# include <unistd.h>
@@ -32,6 +31,8 @@
#if ZEN_PLATFORM_MAC
# include <libproc.h>
+# include <sys/types.h>
+# include <sys/sysctl.h>
#endif
ZEN_THIRD_PARTY_INCLUDES_START
@@ -41,7 +42,9 @@ ZEN_THIRD_PARTY_INCLUDES_END
namespace zen {
#if ZEN_PLATFORM_LINUX
-const bool bNoZombieChildren = []() {
+void
+IgnoreChildSignals()
+{
// When a child process exits it is put into a zombie state until the parent
// collects its result. This doesn't fit the Windows-like model that Zen uses
// where there is a less strict familial model and no zombification. Ignoring
@@ -52,8 +55,7 @@ const bool bNoZombieChildren = []() {
sigemptyset(&Action.sa_mask);
Action.sa_handler = SIG_IGN;
sigaction(SIGCHLD, &Action, nullptr);
- return true;
-}();
+}
static char
GetPidStatus(int Pid, std::error_code& OutEc)
@@ -443,6 +445,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 +594,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 +808,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 +855,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 0d5ad6091..b8ec85a4a 100644
--- a/src/zencore/thread.cpp
+++ b/src/zencore/thread.cpp
@@ -485,7 +485,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/zenhttp-test/zenhttp-test.cpp b/src/zenhttp-test/zenhttp-test.cpp
index a1327320e..116971b02 100644
--- a/src/zenhttp-test/zenhttp-test.cpp
+++ b/src/zenhttp-test/zenhttp-test.cpp
@@ -9,6 +9,7 @@
#if ZEN_WITH_TESTS
# define ZEN_TEST_WITH_RUNNER 1
# include <zencore/testing.h>
+# include <zencore/process.h>
#endif
int
@@ -17,6 +18,10 @@ main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
#if ZEN_WITH_TESTS
zen::zenhttp_forcelinktests();
+# if ZEN_PLATFORM_LINUX
+ zen::IgnoreChildSignals();
+# endif
+
zen::TraceInit("zenhttp-test");
zen::logging::InitializeLogging();
zen::MaximizeOpenFileCount();
diff --git a/src/zennet-test/zennet-test.cpp b/src/zennet-test/zennet-test.cpp
index bac0dec8f..a341bd344 100644
--- a/src/zennet-test/zennet-test.cpp
+++ b/src/zennet-test/zennet-test.cpp
@@ -10,6 +10,7 @@
#if ZEN_WITH_TESTS
# define ZEN_TEST_WITH_RUNNER 1
# include <zencore/testing.h>
+# include <zencore/process.h>
#endif
int
@@ -18,6 +19,10 @@ main([[maybe_unused]] int argc, [[maybe_unused]] char** argv)
#if ZEN_WITH_TESTS
zen::zennet_forcelinktests();
+# if ZEN_PLATFORM_LINUX
+ zen::IgnoreChildSignals();
+# endif
+
zen::TraceInit("zennet-test");
zen::logging::InitializeLogging();
zen::MaximizeOpenFileCount();
diff --git a/src/zenserver-test/zenserver-test.cpp b/src/zenserver-test/zenserver-test.cpp
index f913c108c..77ed87cb1 100644
--- a/src/zenserver-test/zenserver-test.cpp
+++ b/src/zenserver-test/zenserver-test.cpp
@@ -103,6 +103,10 @@ main(int argc, char** argv)
using namespace std::literals;
using namespace zen;
+# if ZEN_PLATFORM_LINUX
+ IgnoreChildSignals();
+# endif
+
zen::TraceInit("zenserver-test");
zen::logging::InitializeLogging();
diff --git a/src/zenserver/main.cpp b/src/zenserver/main.cpp
index 553562473..d8922f885 100644
--- a/src/zenserver/main.cpp
+++ b/src/zenserver/main.cpp
@@ -24,12 +24,14 @@
#include <zencore/memory/memorytrace.h>
#include <zencore/memory/newdelete.h>
+#include <zenutil/service.h>
+
#include "config.h"
#include "diag/logging.h"
#if ZEN_PLATFORM_WINDOWS
# include <zencore/windows.h>
-# include "windows/service.h"
+# include <zenutil/windows/service.h>
#endif
//////////////////////////////////////////////////////////////////////////
@@ -91,6 +93,7 @@ ZenEntryPoint::ZenEntryPoint(ZenServerOptions& ServerOptions) : m_ServerOptions(
int
ZenEntryPoint::Run()
{
+ ZEN_INFO("ZenEntryPoint::Run()");
zen::SetCurrentThreadName("main");
#if ZEN_USE_SENTRY
@@ -288,6 +291,8 @@ ZenEntryPoint::Run()
}});
auto CleanupShutdown = MakeGuard([&ShutdownEvent, &ShutdownThread] {
+ ReportServiceStatus(ServiceStatus::Stopping);
+
if (ShutdownEvent)
{
ShutdownEvent->Set();
@@ -303,6 +308,7 @@ ZenEntryPoint::Run()
Server.SetIsReadyFunc([&] {
m_LockFile.Update(MakeLockData(true), Ec);
+ ReportServiceStatus(ServiceStatus::Running);
NotifyReady();
});
@@ -313,6 +319,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());
@@ -321,6 +335,8 @@ ZenEntryPoint::Run()
ShutdownServerLogging();
+ ReportServiceStatus(ServiceStatus::Stopped);
+
return ApplicationExitCode();
}
@@ -390,6 +406,10 @@ main(int argc, char* argv[])
signal(SIGINT, utils::SignalCallbackHandler);
signal(SIGTERM, utils::SignalCallbackHandler);
+#if ZEN_PLATFORM_LINUX
+ IgnoreChildSignals();
+#endif
+
try
{
ZenServerOptions ServerOptions;
diff --git a/src/zenstore-test/zenstore-test.cpp b/src/zenstore-test/zenstore-test.cpp
index 04f5a8e10..6ae0b84b0 100644
--- a/src/zenstore-test/zenstore-test.cpp
+++ b/src/zenstore-test/zenstore-test.cpp
@@ -10,6 +10,7 @@
#if ZEN_WITH_TESTS
# define ZEN_TEST_WITH_RUNNER 1
# include <zencore/testing.h>
+# include <zencore/process.h>
#endif
int
@@ -18,6 +19,10 @@ main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
#if ZEN_WITH_TESTS
zen::zenstore_forcelinktests();
+# if ZEN_PLATFORM_LINUX
+ zen::IgnoreChildSignals();
+# endif
+
zen::TraceInit("zenstore-test");
zen::logging::InitializeLogging();
zen::MaximizeOpenFileCount();
diff --git a/src/zenutil-test/zenutil-test.cpp b/src/zenutil-test/zenutil-test.cpp
index ca8208314..eb4a17918 100644
--- a/src/zenutil-test/zenutil-test.cpp
+++ b/src/zenutil-test/zenutil-test.cpp
@@ -10,6 +10,7 @@
#if ZEN_WITH_TESTS
# define ZEN_TEST_WITH_RUNNER 1
# include <zencore/testing.h>
+# include <zencore/process.h>
#endif
int
@@ -18,6 +19,10 @@ main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
#if ZEN_WITH_TESTS
zen::zenutil_forcelinktests();
+# if ZEN_PLATFORM_LINUX
+ zen::IgnoreChildSignals();
+# endif
+
zen::TraceInit("zencore-test");
zen::logging::InitializeLogging();
zen::MaximizeOpenFileCount();
diff --git a/src/zenutil/chunking.cpp b/src/zenutil/chunking.cpp
index 30edd322a..71f0a06e4 100644
--- a/src/zenutil/chunking.cpp
+++ b/src/zenutil/chunking.cpp
@@ -5,6 +5,7 @@
#include <gsl/gsl-lite.hpp>
#include <cmath>
+#include <cstring>
namespace zen::detail {
diff --git a/src/zenutil/chunkrequests.cpp b/src/zenutil/chunkrequests.cpp
index 745363668..e28df02a8 100644
--- a/src/zenutil/chunkrequests.cpp
+++ b/src/zenutil/chunkrequests.cpp
@@ -1,5 +1,7 @@
// Copyright Epic Games, Inc. All Rights Reserved.
+#include <algorithm>
+
#include <zenutil/chunkrequests.h>
#include <zencore/blake3.h>
diff --git a/src/zenutil/include/zenutil/service.h b/src/zenutil/include/zenutil/service.h
new file mode 100644
index 000000000..a8bb868b3
--- /dev/null
+++ b/src/zenutil/include/zenutil/service.h
@@ -0,0 +1,58 @@
+// 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;
+ std::string UserName;
+#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);
+
+void ReportServiceStatus(ServiceStatus Status);
+
+} // namespace zen
diff --git a/src/zenserver/windows/service.h b/src/zenutil/include/zenutil/windows/service.h
index 7c9610983..ca0270a36 100644
--- a/src/zenserver/windows/service.h
+++ b/src/zenutil/include/zenutil/windows/service.h
@@ -2,6 +2,8 @@
#pragma once
+#include <zencore/windows.h>
+
class WindowsService
{
public:
@@ -18,3 +20,5 @@ public:
int SvcMain();
static void __stdcall SvcCtrlHandler(unsigned long);
};
+
+VOID ReportSvcStatus(DWORD dwCurrentState, DWORD dwWin32ExitCode, DWORD dwWaitHint);
diff --git a/src/zenutil/service.cpp b/src/zenutil/service.cpp
new file mode 100644
index 000000000..e4a9a951e
--- /dev/null
+++ b/src/zenutil/service.cpp
@@ -0,0 +1,1120 @@
+// 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>
+#include <string_view>
+
+#if ZEN_PLATFORM_WINDOWS
+# include <zencore/windows.h>
+# include <zenutil/windows/service.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>
+# include <regex>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+# include <systemd/sd-daemon.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+#endif
+
+namespace zen {
+using namespace std::literals;
+
+void
+ReportServiceStatus(ServiceStatus Status)
+{
+#if ZEN_PLATFORM_WINDOWS
+ switch (Status)
+ {
+ case ServiceStatus::Starting:
+ ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000);
+ break;
+ case ServiceStatus::Running:
+ ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);
+ break;
+ case ServiceStatus::Stopping:
+ ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 0);
+ break;
+ case ServiceStatus::Stopped:
+ ReportSvcStatus(SERVICE_STOPPED, (DWORD)ApplicationExitCode(), 0);
+ break;
+ default:
+ break;
+ }
+#elif ZEN_PLATFORM_LINUX
+ switch (Status)
+ {
+ case ServiceStatus::Running:
+ sd_notify(0, "READY=1");
+ break;
+ case ServiceStatus::Stopping:
+ sd_notify(0, "STOPPING=1");
+ break;
+ case ServiceStatus::Stopped:
+ sd_notifyf(0, "EXIT_STATUS=%d", ApplicationExitCode());
+ break;
+ }
+#endif
+ (void)Status;
+}
+
+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
+
+ 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)
+ {
+ ZEN_DEBUG("Command {} returned {}, errno {}", Command, Status, errno);
+ 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 UserName)
+ {
+ 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=notify\n"
+ "Restart=always\n"
+ "RestartSec=1\n"
+ "User={}\n"
+ "ExecStart={} {}\n"
+ "RuntimeDirectory={}\n"
+ "[Install]\n"
+ "WantedBy=multi-user.target",
+ ServiceName,
+ UserName,
+ ExecutablePath,
+ CommandLineOptions,
+ ExecutablePath.parent_path());
+ }
+
+#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();
+ }
+
+ auto __ = MakeGuard([schService]() { CloseServiceHandle(schService); });
+
+ 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();
+ }
+ }
+
+ // Actions defining what the service manager should do in the event of a zenserver crash.
+ // Attempt an immediate restart twice. If both restarts fail, stop trying.
+ // The attempt count will be reset based on the timeout specified in SERVICE_FAILURE_ACTIONS.
+ // If the service manages to survive for the length of that timeout, the reset attempt count
+ // will be reset and we will try restarting if we fail again.
+ SC_ACTION Actions[] = {{SC_ACTION_RESTART, 0}, {SC_ACTION_RESTART, 0}, {SC_ACTION_NONE, 0}};
+
+ SERVICE_FAILURE_ACTIONS FailureAction = {
+ 60, // if we haven't failed for one minute, assume the service is healthy and reset the failure count
+ NULL, // no reboot message - we don't want to reboot the whole system if zen dies
+ NULL, // no command to run on failure - just attempt restarting the service
+ ZEN_ARRAY_COUNT(Actions),
+ Actions};
+
+ if (!ChangeServiceConfig2(schService, SERVICE_CONFIG_FAILURE_ACTIONS, &FailureAction))
+ {
+ return MakeErrorCodeFromLastError();
+ }
+
+ 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)
+{
+ const std::string UnitName = GetUnitName(ServiceName);
+ const std::filesystem::path ServiceUnitPath = GetServiceUnitPath(UnitName);
+ std::string UserName = Spec.UserName;
+
+ if (UserName == "")
+ {
+ std::pair<int, std::string> UserResult = ExecuteProgram("echo $SUDO_USER");
+ if (UserResult.first != 0 || UserResult.second.empty())
+ {
+ ZEN_ERROR("Unable to determine current user");
+ return MakeErrorCode(UserResult.first);
+ }
+
+ UserName = UserResult.second;
+ }
+
+ std::string UnitFile = BuildUnitFile(ServiceName, Spec.ExecutablePath, Spec.CommandLineOptions, UserName);
+ 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;
+
+ std::pair<int, std::string> Res = ExecuteProgram(fmt::format("systemctl is-active --quiet {}", UnitName));
+ if (Res.first == 0)
+ {
+ OutInfo.Status = ServiceStatus::Running;
+
+ std::pair<int, std::string> ShowResult = ExecuteProgram(fmt::format("systemctl show -p ExecStart {}", UnitName));
+ if (ShowResult.first == 0)
+ {
+ std::regex Regex(R"~(ExecStart=\{ path=(.*?) ; argv\[\]=(.*?) ;)~");
+ std::smatch Match;
+
+ if (std::regex_search(ShowResult.second, Match, Regex))
+ {
+ std::string Executable = Match[1].str();
+ std::string CommandLine = Match[2].str();
+ OutInfo.Spec.ExecutablePath = Executable;
+ OutInfo.Spec.CommandLineOptions = CommandLine.substr(Executable.size(), CommandLine.size());
+ }
+ else
+ {
+ ZEN_WARN("Failed to parse output of systemctl show: {}", ShowResult.second);
+ }
+ }
+ else
+ {
+ ZEN_WARN("Failed to read start info from systemctl: error code {}", ShowResult.first);
+ }
+ }
+ 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/zenserver/windows/service.cpp b/src/zenutil/windows/service.cpp
index cb87df1f6..b76ed7a66 100644
--- a/src/zenserver/windows/service.cpp
+++ b/src/zenutil/windows/service.cpp
@@ -1,10 +1,9 @@
// Copyright Epic Games, Inc. All Rights Reserved.
-#include "service.h"
-
#include <zencore/zencore.h>
#if ZEN_PLATFORM_WINDOWS
+# include <zenutil/windows/service.h>
# include <zencore/except.h>
# include <zencore/thread.h>
@@ -21,7 +20,6 @@ HANDLE ghSvcStopEvent = NULL;
void SvcInstall(void);
-void ReportSvcStatus(DWORD, DWORD, DWORD);
void SvcReportEvent(LPTSTR);
WindowsService::WindowsService()
@@ -222,14 +220,7 @@ WindowsService::SvcMain()
return 1;
}
- // Report running status when initialization is complete.
-
- ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);
-
int ReturnCode = Run();
-
- ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
-
return ReturnCode;
}
diff --git a/src/zenutil/xmake.lua b/src/zenutil/xmake.lua
index 3d95651f2..6d87aefcc 100644
--- a/src/zenutil/xmake.lua
+++ b/src/zenutil/xmake.lua
@@ -8,3 +8,11 @@ target('zenutil')
add_includedirs("include", {public=true})
add_deps("zencore", "zenhttp")
add_packages("vcpkg::robin-map", "vcpkg::spdlog")
+
+ if is_plat("linux") then
+ add_includedirs("$(projectdir)/thirdparty/systemd/include")
+ add_linkdirs("$(projectdir)/thirdparty/systemd/lib")
+ add_links("systemd")
+ add_links("cap")
+ end
+
diff --git a/src/zenutil/zenserverprocess.cpp b/src/zenutil/zenserverprocess.cpp
index a5b342cb0..4fd1cef9a 100644
--- a/src/zenutil/zenserverprocess.cpp
+++ b/src/zenutil/zenserverprocess.cpp
@@ -167,10 +167,23 @@ 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);
+ ZEN_INFO("{}", geteuid());
+ 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");
+ // Work around a potential issue if the service user is changed in certain configurations.
+ // If the sysctl 'fs.protected_regular' is set to 1 or 2 (default on many distros),
+ // we will be unable to open an existing shared memory object created by another user using O_CREAT,
+ // even if we have the correct permissions, or are running as root. If we destroy the existing
+ // shared memory object and retry, we'll be able to get past shm_open() so long as we have
+ // the appropriate permissions to create the shared memory object.
+ shm_unlink("/UnrealEngineZen");
+ 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");
+ }
}
fchmod(Fd, 0666);
void* hMap = (void*)intptr_t(Fd);