// Copyright Epic Games, Inc. All Rights Reserved. #include #include #include #include #include #include #if ZEN_PLATFORM_WINDOWS # include # include #endif #if ZEN_PLATFORM_MAC # include # include # include # include #endif #if ZEN_PLATFORM_LINUX # include # include # include # include # include ZEN_THIRD_PARTY_INCLUDES_START # include 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 SplitArguments(std::string_view Arguments) { bool IsQuote = false; size_t Start = 0; size_t Offset = 0; std::vector 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 Arguments = SplitArguments(CommandLineOptions); ExtendableStringBuilder<256> ProgramArguments; for (const std::string_view Argument : Arguments) { ProgramArguments.Append(" "); AppendEscaped(Argument, ProgramArguments); ProgramArguments.Append("\n"); } return fmt::format( "\n" "\n" "\n" "\n" " Label\n" " {}\n" // DaemonName " \n" " ProgramArguments\n" " \n" " {}\n" // Program name "{}" // "arg\n" * number of arguments " \n" " \n" " RunAtLoad\n" " \n" " \n" // " KeepAlive\n" // " \n" // " \n" " StandardOutPath\n" " /var/log/{}.log\n" " \n" " StandardErrorPath\n" " /var/log/{}.err.log\n" " \n" " Debug\n" " <{}/>\n" " \n" "\n" "\n", DaemonName, ExecutablePath, ProgramArguments.ToView(), ServiceName, ServiceName, Debug ? "true"sv : "false"sv); // "Sockets" // "" // "Listeners" // "" // "SockServiceName" // "{}" // Listen socket // "SockType" // "tcp" // "SockFamily" // "IPv4" // "" // "" } #endif // ZEN_PLATFORM_MAC #if ZEN_PLATFORM_MAC || ZEN_PLATFORM_LINUX std::pair 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(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 Buffer(8192); QUERY_SERVICE_CONFIG* ServiceConfig = reinterpret_cast(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("ProgramArguments") != std::string_view::npos) { Mode = ParseMode::ExpectingProgramArgumentsArray; return true; } } break; case ParseMode::ExpectingProgramArgumentsArray: { if (Line.find("") != 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(""); ArgStart != std::string_view::npos) { ArgStart += 8; if (std::string_view::size_type ArgEnd = Line.find("", 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(""); ArgStart != std::string_view::npos) { Mode = ParseMode::None; return true; } else if (std::string_view::size_type ArgStart = Line.find(""); ArgStart != std::string_view::npos) { ArgStart += 8; if (std::string_view::size_type ArgEnd = Line.find("", 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 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(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 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 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 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 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 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 Res = ExecuteProgram(fmt::format("systemctl is-active --quiet {}", UnitName)); if (Res.first == 0) { OutInfo.Status = ServiceStatus::Running; std::pair 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 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 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