diff options
| author | Liam Mitchell <[email protected]> | 2025-08-22 16:12:38 -0700 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2025-08-22 16:12:38 -0700 |
| commit | 207c4a32612891711d9d69466b6dfc653428bb07 (patch) | |
| tree | d4b8de42a91ee3327b14fc0aa66c92bc3de46555 /src | |
| parent | 5.6.18-pre0 (diff) | |
| parent | Move windows service utilities to zenutil and fix clang-format errors (diff) | |
| download | zen-207c4a32612891711d9d69466b6dfc653428bb07.tar.xz zen-207c4a32612891711d9d69466b6dfc653428bb07.zip | |
Merge pull request #139 from ue-foundation/de/zen-service-command
zen service command
Diffstat (limited to 'src')
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("""); + break; + case '\'': + SB.Append("&apos"); + break; + case '<': + SB.Append("<"); + break; + case '>': + SB.Append(">"); + break; + case '&': + SB.Append("&"); + 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); |