// Copyright Epic Games, Inc. All Rights Reserved. #include #include #include #include #include #if ZEN_PLATFORM_WINDOWS # include #endif #if ZEN_PLATFORM_MAC # include # include # include # include #endif namespace zen { using namespace std::literals; 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.string(), 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 max_buffer = 256; char buffer[max_buffer]; std::string Command(Cmd); Command.append(" 2>&1"); FILE* stream = popen(Command.c_str(), "r"); if (stream) { while (!feof(stream)) { if (fgets(buffer, max_buffer, stream) != NULL) { data.append(buffer); } } int Res = -1; int st = pclose(stream); if (WIFEXITED(st)) Res = WEXITSTATUS(st); return {Res, data}; } return {errno, ""}; # if 0 int in[2], out[2], n, pid; char buf[255]; /* In a pipe, xx[0] is for reading, xx[1] is for writing */ if (pipe(in) < 0) { return {errno, ""}; } if (pipe(out) < 0) { close(in[0]); close(in[1]); return {errno, ""}; } if ((pid=fork()) == 0) { /* This is the child process */ /* Close stdin, stdout, stderr */ close(0); close(1); close(2); /* make our pipes, our new stdin,stdout and stderr */ dup2(in[0],0); dup2(out[1],1); dup2(out[1],2); /* Close the other ends of the pipes that the parent will use, because if * we leave these open in the child, the child/parent will not get an EOF * when the parent/child closes their end of the pipe. */ close(in[1]); close(out[0]); va_list args; va_start(args, Executable); va_end(args); /* Over-write the child process with the hexdump binary */ execl(Executable, Executable, args); } /* This is the parent process */ /* Close the pipe ends that the child uses to read from / write to so * the when we close the others, an EOF will be transmitted properly. */ close(in[0]); /* Because of the small amount of data, the child may block unless we * close it's input stream. This sends an EOF to the child on it's * stdin. */ close(in[1]); /* Read back any output */ n = read(out[0], buf, 250); if (n == 0) { n = read(out[1], buf, 250); } buf[n] = 0; close(out[0]); close(out[1]); std::string Output(buf); return {0, Output}; # endif // 0 } #endif // ZEN_PLATFORM_MAC || 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) { // 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(); } 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(); } } CloseServiceHandle(schService); 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) { 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) { ZEN_UNUSED(ServiceName, 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(std::string("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) { ZEN_UNUSED(ServiceName); const std::string DaemonName = GetDaemonName(ServiceName); const std::filesystem::path PListPath = GetPListPath(DaemonName); std::pair Res = ExecuteProgram(std::string("launchctl bootstrap system ") + PListPath.string()); if (Res.first != 0) { return MakeErrorCode(Res.first); } return {}; } std::error_code StopService(std::string_view ServiceName) { ZEN_UNUSED(ServiceName); const std::string DaemonName = GetDaemonName(ServiceName); const std::filesystem::path PListPath = GetPListPath(DaemonName); std::pair Res = ExecuteProgram(std::string("launchctl bootout system ") + PListPath.string()); if (Res.first != 0) { return MakeErrorCode(Res.first); } return {}; } #endif // ZEN_PLATFORM_MAC } // namespace zen