aboutsummaryrefslogtreecommitdiff
path: root/src/zenutil/service.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/zenutil/service.cpp')
-rw-r--r--src/zenutil/service.cpp1120
1 files changed, 1120 insertions, 0 deletions
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