aboutsummaryrefslogtreecommitdiff
path: root/src/zenutil
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/zenutil
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/zenutil')
-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.h24
-rw-r--r--src/zenutil/service.cpp1120
-rw-r--r--src/zenutil/windows/service.cpp639
-rw-r--r--src/zenutil/xmake.lua8
-rw-r--r--src/zenutil/zenserverprocess.cpp17
8 files changed, 1867 insertions, 2 deletions
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/zenutil/include/zenutil/windows/service.h b/src/zenutil/include/zenutil/windows/service.h
new file mode 100644
index 000000000..ca0270a36
--- /dev/null
+++ b/src/zenutil/include/zenutil/windows/service.h
@@ -0,0 +1,24 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <zencore/windows.h>
+
+class WindowsService
+{
+public:
+ WindowsService();
+ ~WindowsService();
+
+ virtual int Run() = 0;
+
+ int ServiceMain();
+
+ static void Install();
+ static void Delete();
+
+ 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/zenutil/windows/service.cpp b/src/zenutil/windows/service.cpp
new file mode 100644
index 000000000..b76ed7a66
--- /dev/null
+++ b/src/zenutil/windows/service.cpp
@@ -0,0 +1,639 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zencore/zencore.h>
+
+#if ZEN_PLATFORM_WINDOWS
+# include <zenutil/windows/service.h>
+
+# include <zencore/except.h>
+# include <zencore/thread.h>
+
+# include <stdio.h>
+# include <tchar.h>
+# include <zencore/windows.h>
+
+# define SVCNAME L"Zen Store"
+
+SERVICE_STATUS gSvcStatus;
+SERVICE_STATUS_HANDLE gSvcStatusHandle;
+HANDLE ghSvcStopEvent = NULL;
+
+void SvcInstall(void);
+
+void SvcReportEvent(LPTSTR);
+
+WindowsService::WindowsService()
+{
+}
+
+WindowsService::~WindowsService()
+{
+}
+
+//
+// Purpose:
+// Installs a service in the SCM database
+//
+// Parameters:
+// None
+//
+// Return value:
+// None
+//
+VOID
+WindowsService::Install()
+{
+ SC_HANDLE schSCManager;
+ SC_HANDLE schService;
+ TCHAR szPath[MAX_PATH];
+
+ if (!GetModuleFileName(NULL, szPath, MAX_PATH))
+ {
+ printf("Cannot install service (%d)\n", GetLastError());
+ return;
+ }
+
+ // Get a handle to the SCM database.
+
+ schSCManager = OpenSCManager(NULL, // local computer
+ NULL, // ServicesActive database
+ SC_MANAGER_ALL_ACCESS); // full access rights
+
+ if (NULL == schSCManager)
+ {
+ printf("OpenSCManager failed (%d)\n", GetLastError());
+ return;
+ }
+
+ // Create the service
+
+ schService = CreateService(schSCManager, // SCM database
+ SVCNAME, // name of service
+ SVCNAME, // service name to display
+ SERVICE_ALL_ACCESS, // desired access
+ SERVICE_WIN32_OWN_PROCESS, // service type
+ SERVICE_DEMAND_START, // start type
+ SERVICE_ERROR_NORMAL, // error control type
+ szPath, // path to service's binary
+ NULL, // no load ordering group
+ NULL, // no tag identifier
+ NULL, // no dependencies
+ NULL, // LocalSystem account
+ NULL); // no password
+
+ if (schService == NULL)
+ {
+ printf("CreateService failed (%d)\n", GetLastError());
+ CloseServiceHandle(schSCManager);
+ return;
+ }
+ else
+ printf("Service installed successfully\n");
+
+ CloseServiceHandle(schService);
+ CloseServiceHandle(schSCManager);
+}
+
+void
+WindowsService::Delete()
+{
+ SC_HANDLE schSCManager;
+ SC_HANDLE schService;
+
+ // Get a handle to the SCM database.
+
+ schSCManager = OpenSCManager(NULL, // local computer
+ NULL, // ServicesActive database
+ SC_MANAGER_ALL_ACCESS); // full access rights
+
+ if (NULL == schSCManager)
+ {
+ printf("OpenSCManager failed (%d)\n", GetLastError());
+ return;
+ }
+
+ // Get a handle to the service.
+
+ schService = OpenService(schSCManager, // SCM database
+ SVCNAME, // name of service
+ DELETE); // need delete access
+
+ if (schService == NULL)
+ {
+ printf("OpenService failed (%d)\n", GetLastError());
+ CloseServiceHandle(schSCManager);
+ return;
+ }
+
+ // Delete the service.
+
+ if (!DeleteService(schService))
+ {
+ printf("DeleteService failed (%d)\n", GetLastError());
+ }
+ else
+ printf("Service deleted successfully\n");
+
+ CloseServiceHandle(schService);
+ CloseServiceHandle(schSCManager);
+}
+
+WindowsService* gSvc;
+
+void WINAPI
+CallMain(DWORD, LPSTR*)
+{
+ gSvc->SvcMain();
+}
+
+int
+WindowsService::ServiceMain()
+{
+ zen::SetCurrentThreadName("svc-main");
+
+ gSvc = this;
+
+ SERVICE_TABLE_ENTRY DispatchTable[] = {{(LPWSTR)SVCNAME, (LPSERVICE_MAIN_FUNCTION)&CallMain}, {NULL, NULL}};
+
+ // This call returns when the service has stopped.
+ // The process should simply terminate when the call returns.
+
+ if (!StartServiceCtrlDispatcher(DispatchTable))
+ {
+ const DWORD dwError = zen::GetLastError();
+
+ if (dwError == ERROR_FAILED_SERVICE_CONTROLLER_CONNECT)
+ {
+ // Not actually running as a service
+ gSvc = nullptr;
+
+ zen::SetIsInteractiveSession(true);
+
+ return Run();
+ }
+ else
+ {
+ zen::ThrowSystemError(dwError, "StartServiceCtrlDispatcher failed");
+ }
+ }
+
+ zen::SetIsInteractiveSession(false);
+
+ return zen::ApplicationExitCode();
+}
+
+int
+WindowsService::SvcMain()
+{
+ // Register the handler function for the service
+
+ gSvcStatusHandle = RegisterServiceCtrlHandler(SVCNAME, SvcCtrlHandler);
+
+ if (!gSvcStatusHandle)
+ {
+ SvcReportEvent((LPTSTR)TEXT("RegisterServiceCtrlHandler"));
+
+ return 1;
+ }
+
+ // These SERVICE_STATUS members remain as set here
+
+ gSvcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
+ gSvcStatus.dwServiceSpecificExitCode = 0;
+
+ // Report initial status to the SCM
+
+ ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000);
+
+ // Create an event. The control handler function, SvcCtrlHandler,
+ // signals this event when it receives the stop control code.
+
+ ghSvcStopEvent = CreateEvent(NULL, // default security attributes
+ TRUE, // manual reset event
+ FALSE, // not signaled
+ NULL); // no name
+
+ if (ghSvcStopEvent == NULL)
+ {
+ ReportSvcStatus(SERVICE_STOPPED, GetLastError(), 0);
+
+ return 1;
+ }
+
+ int ReturnCode = Run();
+ return ReturnCode;
+}
+
+//
+// Purpose:
+// Retrieves and displays the current service configuration.
+//
+// Parameters:
+// None
+//
+// Return value:
+// None
+//
+void
+DoQuerySvc()
+{
+ SC_HANDLE schSCManager{};
+ SC_HANDLE schService{};
+ LPQUERY_SERVICE_CONFIG lpsc{};
+ LPSERVICE_DESCRIPTION lpsd{};
+ DWORD dwBytesNeeded{}, cbBufSize{}, dwError{};
+
+ // Get a handle to the SCM database.
+
+ schSCManager = OpenSCManager(NULL, // local computer
+ NULL, // ServicesActive database
+ SC_MANAGER_ALL_ACCESS); // full access rights
+
+ if (NULL == schSCManager)
+ {
+ printf("OpenSCManager failed (%d)\n", GetLastError());
+ return;
+ }
+
+ // Get a handle to the service.
+
+ schService = OpenService(schSCManager, // SCM database
+ SVCNAME, // name of service
+ SERVICE_QUERY_CONFIG); // need query config access
+
+ if (schService == NULL)
+ {
+ printf("OpenService failed (%d)\n", GetLastError());
+ CloseServiceHandle(schSCManager);
+ return;
+ }
+
+ // Get the configuration information.
+
+ if (!QueryServiceConfig(schService, NULL, 0, &dwBytesNeeded))
+ {
+ dwError = GetLastError();
+ if (ERROR_INSUFFICIENT_BUFFER == dwError)
+ {
+ cbBufSize = dwBytesNeeded;
+ lpsc = (LPQUERY_SERVICE_CONFIG)LocalAlloc(LMEM_FIXED, cbBufSize);
+ }
+ else
+ {
+ printf("QueryServiceConfig failed (%d)", dwError);
+ goto cleanup;
+ }
+ }
+
+ if (!QueryServiceConfig(schService, lpsc, cbBufSize, &dwBytesNeeded))
+ {
+ printf("QueryServiceConfig failed (%d)", GetLastError());
+ goto cleanup;
+ }
+
+ if (!QueryServiceConfig2(schService, SERVICE_CONFIG_DESCRIPTION, NULL, 0, &dwBytesNeeded))
+ {
+ dwError = GetLastError();
+ if (ERROR_INSUFFICIENT_BUFFER == dwError)
+ {
+ cbBufSize = dwBytesNeeded;
+ lpsd = (LPSERVICE_DESCRIPTION)LocalAlloc(LMEM_FIXED, cbBufSize);
+ }
+ else
+ {
+ printf("QueryServiceConfig2 failed (%d)", dwError);
+ goto cleanup;
+ }
+ }
+
+ if (!QueryServiceConfig2(schService, SERVICE_CONFIG_DESCRIPTION, (LPBYTE)lpsd, cbBufSize, &dwBytesNeeded))
+ {
+ printf("QueryServiceConfig2 failed (%d)", GetLastError());
+ goto cleanup;
+ }
+
+ // Print the configuration information.
+
+ _tprintf(TEXT("%s configuration: \n"), SVCNAME);
+ _tprintf(TEXT(" Type: 0x%x\n"), lpsc->dwServiceType);
+ _tprintf(TEXT(" Start Type: 0x%x\n"), lpsc->dwStartType);
+ _tprintf(TEXT(" Error Control: 0x%x\n"), lpsc->dwErrorControl);
+ _tprintf(TEXT(" Binary path: %s\n"), lpsc->lpBinaryPathName);
+ _tprintf(TEXT(" Account: %s\n"), lpsc->lpServiceStartName);
+
+ if (lpsd->lpDescription != NULL && lstrcmp(lpsd->lpDescription, TEXT("")) != 0)
+ _tprintf(TEXT(" Description: %s\n"), lpsd->lpDescription);
+ if (lpsc->lpLoadOrderGroup != NULL && lstrcmp(lpsc->lpLoadOrderGroup, TEXT("")) != 0)
+ _tprintf(TEXT(" Load order group: %s\n"), lpsc->lpLoadOrderGroup);
+ if (lpsc->dwTagId != 0)
+ _tprintf(TEXT(" Tag ID: %d\n"), lpsc->dwTagId);
+ if (lpsc->lpDependencies != NULL && lstrcmp(lpsc->lpDependencies, TEXT("")) != 0)
+ _tprintf(TEXT(" Dependencies: %s\n"), lpsc->lpDependencies);
+
+ LocalFree(lpsc);
+ LocalFree(lpsd);
+
+cleanup:
+ CloseServiceHandle(schService);
+ CloseServiceHandle(schSCManager);
+}
+
+//
+// Purpose:
+// Disables the service.
+//
+// Parameters:
+// None
+//
+// Return value:
+// None
+//
+void
+DoDisableSvc()
+{
+ SC_HANDLE schSCManager;
+ SC_HANDLE schService;
+
+ // Get a handle to the SCM database.
+
+ schSCManager = OpenSCManager(NULL, // local computer
+ NULL, // ServicesActive database
+ SC_MANAGER_ALL_ACCESS); // full access rights
+
+ if (NULL == schSCManager)
+ {
+ printf("OpenSCManager failed (%d)\n", GetLastError());
+ return;
+ }
+
+ // Get a handle to the service.
+
+ schService = OpenService(schSCManager, // SCM database
+ SVCNAME, // name of service
+ SERVICE_CHANGE_CONFIG); // need change config access
+
+ if (schService == NULL)
+ {
+ printf("OpenService failed (%d)\n", GetLastError());
+ CloseServiceHandle(schSCManager);
+ return;
+ }
+
+ // Change the service start type.
+
+ if (!ChangeServiceConfig(schService, // handle of service
+ SERVICE_NO_CHANGE, // service type: no change
+ SERVICE_DISABLED, // service start type
+ SERVICE_NO_CHANGE, // error control: no change
+ NULL, // binary path: no change
+ NULL, // load order group: no change
+ NULL, // tag ID: no change
+ NULL, // dependencies: no change
+ NULL, // account name: no change
+ NULL, // password: no change
+ NULL)) // display name: no change
+ {
+ printf("ChangeServiceConfig failed (%d)\n", GetLastError());
+ }
+ else
+ printf("Service disabled successfully.\n");
+
+ CloseServiceHandle(schService);
+ CloseServiceHandle(schSCManager);
+}
+
+//
+// Purpose:
+// Enables the service.
+//
+// Parameters:
+// None
+//
+// Return value:
+// None
+//
+VOID __stdcall DoEnableSvc()
+{
+ SC_HANDLE schSCManager;
+ SC_HANDLE schService;
+
+ // Get a handle to the SCM database.
+
+ schSCManager = OpenSCManager(NULL, // local computer
+ NULL, // ServicesActive database
+ SC_MANAGER_ALL_ACCESS); // full access rights
+
+ if (NULL == schSCManager)
+ {
+ printf("OpenSCManager failed (%d)\n", GetLastError());
+ return;
+ }
+
+ // Get a handle to the service.
+
+ schService = OpenService(schSCManager, // SCM database
+ SVCNAME, // name of service
+ SERVICE_CHANGE_CONFIG); // need change config access
+
+ if (schService == NULL)
+ {
+ printf("OpenService failed (%d)\n", GetLastError());
+ CloseServiceHandle(schSCManager);
+ return;
+ }
+
+ // Change the service start type.
+
+ if (!ChangeServiceConfig(schService, // handle of service
+ SERVICE_NO_CHANGE, // service type: no change
+ SERVICE_DEMAND_START, // service start type
+ SERVICE_NO_CHANGE, // error control: no change
+ NULL, // binary path: no change
+ NULL, // load order group: no change
+ NULL, // tag ID: no change
+ NULL, // dependencies: no change
+ NULL, // account name: no change
+ NULL, // password: no change
+ NULL)) // display name: no change
+ {
+ printf("ChangeServiceConfig failed (%d)\n", GetLastError());
+ }
+ else
+ printf("Service enabled successfully.\n");
+
+ CloseServiceHandle(schService);
+ CloseServiceHandle(schSCManager);
+}
+//
+// Purpose:
+// Updates the service description to "This is a test description".
+//
+// Parameters:
+// None
+//
+// Return value:
+// None
+//
+void
+DoUpdateSvcDesc()
+{
+ SC_HANDLE schSCManager;
+ SC_HANDLE schService;
+ SERVICE_DESCRIPTION sd;
+ TCHAR szDesc[] = TEXT("This is a test description");
+
+ // Get a handle to the SCM database.
+
+ schSCManager = OpenSCManager(NULL, // local computer
+ NULL, // ServicesActive database
+ SC_MANAGER_ALL_ACCESS); // full access rights
+
+ if (NULL == schSCManager)
+ {
+ printf("OpenSCManager failed (%d)\n", GetLastError());
+ return;
+ }
+
+ // Get a handle to the service.
+
+ schService = OpenService(schSCManager, // SCM database
+ SVCNAME, // name of service
+ SERVICE_CHANGE_CONFIG); // need change config access
+
+ if (schService == NULL)
+ {
+ printf("OpenService failed (%d)\n", GetLastError());
+ CloseServiceHandle(schSCManager);
+ return;
+ }
+
+ // Change the service description.
+
+ sd.lpDescription = szDesc;
+
+ if (!ChangeServiceConfig2(schService, // handle to service
+ SERVICE_CONFIG_DESCRIPTION, // change: description
+ &sd)) // new description
+ {
+ printf("ChangeServiceConfig2 failed\n");
+ }
+ else
+ printf("Service description updated successfully.\n");
+
+ CloseServiceHandle(schService);
+ CloseServiceHandle(schSCManager);
+}
+
+//
+// Purpose:
+// Sets the current service status and reports it to the SCM.
+//
+// Parameters:
+// dwCurrentState - The current state (see SERVICE_STATUS)
+// dwWin32ExitCode - The system error code
+// dwWaitHint - Estimated time for pending operation,
+// in milliseconds
+//
+// Return value:
+// None
+//
+VOID
+ReportSvcStatus(DWORD dwCurrentState, DWORD dwWin32ExitCode, DWORD dwWaitHint)
+{
+ static DWORD dwCheckPoint = 1;
+
+ // Fill in the SERVICE_STATUS structure.
+
+ gSvcStatus.dwCurrentState = dwCurrentState;
+ gSvcStatus.dwWin32ExitCode = dwWin32ExitCode;
+ gSvcStatus.dwWaitHint = dwWaitHint;
+
+ if (dwCurrentState == SERVICE_START_PENDING)
+ gSvcStatus.dwControlsAccepted = 0;
+ else
+ gSvcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;
+
+ if ((dwCurrentState == SERVICE_RUNNING) || (dwCurrentState == SERVICE_STOPPED))
+ gSvcStatus.dwCheckPoint = 0;
+ else
+ gSvcStatus.dwCheckPoint = dwCheckPoint++;
+
+ // Report the status of the service to the SCM.
+ SetServiceStatus(gSvcStatusHandle, &gSvcStatus);
+}
+
+void
+WindowsService::SvcCtrlHandler(DWORD dwCtrl)
+{
+ // Handle the requested control code.
+ //
+ // Called by SCM whenever a control code is sent to the service
+ // using the ControlService function.
+
+ switch (dwCtrl)
+ {
+ case SERVICE_CONTROL_STOP:
+ ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 0);
+
+ // Signal the service to stop.
+
+ SetEvent(ghSvcStopEvent);
+ zen::RequestApplicationExit(0);
+
+ ReportSvcStatus(gSvcStatus.dwCurrentState, NO_ERROR, 0);
+ return;
+
+ case SERVICE_CONTROL_INTERROGATE:
+ break;
+
+ default:
+ break;
+ }
+}
+
+//
+// Purpose:
+// Logs messages to the event log
+//
+// Parameters:
+// szFunction - name of function that failed
+//
+// Return value:
+// None
+//
+// Remarks:
+// The service must have an entry in the Application event log.
+//
+VOID
+SvcReportEvent(LPTSTR szFunction)
+{
+ ZEN_UNUSED(szFunction);
+
+ // HANDLE hEventSource;
+ // LPCTSTR lpszStrings[2];
+ // TCHAR Buffer[80];
+
+ // hEventSource = RegisterEventSource(NULL, SVCNAME);
+
+ // if (NULL != hEventSource)
+ //{
+ // StringCchPrintf(Buffer, 80, TEXT("%s failed with %d"), szFunction, GetLastError());
+
+ // lpszStrings[0] = SVCNAME;
+ // lpszStrings[1] = Buffer;
+
+ // ReportEvent(hEventSource, // event log handle
+ // EVENTLOG_ERROR_TYPE, // event type
+ // 0, // event category
+ // SVC_ERROR, // event identifier
+ // NULL, // no security identifier
+ // 2, // size of lpszStrings array
+ // 0, // no binary data
+ // lpszStrings, // array of strings
+ // NULL); // no binary data
+
+ // DeregisterEventSource(hEventSource);
+ //}
+}
+
+#endif // ZEN_PLATFORM_WINDOWS
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);