// Copyright Epic Games, Inc. All Rights Reserved. #include "ui_cmd.h" #include #include #include #include #include #include #if ZEN_PLATFORM_WINDOWS # include # include #endif namespace zen { namespace { struct RunningServerInfo { uint16_t Port; uint32_t Pid; std::string SessionId; std::string CmdLine; }; static std::vector CollectRunningServers() { std::vector Servers; ZenServerState State; if (!State.InitializeReadOnly()) return Servers; State.Snapshot([&](const ZenServerState::ZenServerEntry& Entry) { StringBuilder<25> SessionSB; Entry.GetSessionId().ToString(SessionSB); std::error_code CmdLineEc; std::string CmdLine = GetProcessCommandLine(static_cast(Entry.Pid.load()), CmdLineEc); Servers.push_back({Entry.EffectiveListenPort.load(), Entry.Pid.load(), std::string(SessionSB.c_str()), std::move(CmdLine)}); }); return Servers; } } // namespace UiCommand::UiCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_options()("a,all", "Open dashboard for all running instances", cxxopts::value(m_All)->default_value("false")); m_Options.add_option("", "u", "hosturl", kHostUrlHelp, cxxopts::value(m_HostName)->default_value(""), ""); m_Options.add_option("", "p", "path", "Dashboard path (default: /dashboard/)", cxxopts::value(m_DashboardPath)->default_value("/dashboard/"), ""); m_Options.parse_positional("path"); } UiCommand::~UiCommand() { } void UiCommand::OpenBrowser(std::string_view HostName) { // Allow shortcuts for specifying dashboard path, and ensure it is in a format we expect // (leading slash, trailing slash if no file extension) if (!m_DashboardPath.empty()) { if (m_DashboardPath[0] != '/') { m_DashboardPath = "/dashboard/" + m_DashboardPath; } if (m_DashboardPath.find_last_of('.') == std::string::npos && m_DashboardPath.back() != '/') { m_DashboardPath += '/'; } } bool Success = false; ExtendableStringBuilder<256> FullUrl; FullUrl << HostName << m_DashboardPath; #if ZEN_PLATFORM_WINDOWS HINSTANCE Result = ShellExecuteA(nullptr, "open", FullUrl.c_str(), nullptr, nullptr, SW_SHOWNORMAL); Success = reinterpret_cast(Result) > 32; #else // Validate URL doesn't contain shell metacharacters that could lead to command injection std::string_view FullUrlView = FullUrl; constexpr std::string_view DangerousChars = ";|&$`\\\"'<>(){}[]!#*?~\n\r"; if (FullUrlView.find_first_of(DangerousChars) != std::string_view::npos) { throw OptionParseException(fmt::format("URL contains invalid characters: '{}'", FullUrl), m_Options.help()); } # if ZEN_PLATFORM_MAC std::string Command = fmt::format("open \"{}\"", FullUrl); # elif ZEN_PLATFORM_LINUX std::string Command = fmt::format("xdg-open \"{}\"", FullUrl); # else ZEN_NOT_IMPLEMENTED("Browser launching not implemented on this platform"); # endif Success = system(Command.c_str()) == 0; #endif if (!Success) { throw zen::runtime_error("Failed to launch browser for '{}'", FullUrl); } ZEN_CONSOLE("Web browser launched for '{}' successfully", FullUrl); } void UiCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { using namespace std::literals; ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return; } // Resolve target server uint16_t ServerPort = 0; if (m_HostName.empty()) { // Auto-discover running instances. std::vector Servers = CollectRunningServers(); if (m_All) { if (Servers.empty()) { throw OptionParseException("No running Zen server instances found", m_Options.help()); } for (const auto& Server : Servers) { OpenBrowser(fmt::format("http://localhost:{}", Server.Port)); } return; } // If multiple are found and we have an interactive terminal, present a picker // instead of silently using the first one. if (Servers.size() > 1 && IsTuiAvailable()) { std::vector Labels; Labels.reserve(Servers.size() + 1); Labels.push_back(fmt::format("(all {} instances)", Servers.size())); const int32_t Cols = static_cast(TuiConsoleColumns()); constexpr int32_t kIndicator = 3; // " > " or " " prefix constexpr int32_t kSeparator = 2; // " " before cmdline constexpr int32_t kEllipsis = 3; // "..." for (const auto& Server : Servers) { std::string Label = fmt::format("port {:<5} pid {:<7} session {}", Server.Port, Server.Pid, Server.SessionId); if (!Server.CmdLine.empty()) { int32_t Available = Cols - kIndicator - kSeparator - static_cast(Label.size()); if (Available > kEllipsis) { Label += " "; if (static_cast(Server.CmdLine.size()) <= Available) { Label += Server.CmdLine; } else { Label.append(Server.CmdLine, 0, static_cast(Available - kEllipsis)); Label += "..."; } } } Labels.push_back(std::move(Label)); } int SelectedIdx = TuiPickOne("Multiple Zen server instances found. Select one to open:", Labels); if (SelectedIdx < 0) return; // User cancelled if (SelectedIdx == 0) { // "All" selected for (const auto& Server : Servers) { OpenBrowser(fmt::format("http://localhost:{}", Server.Port)); } return; } ServerPort = Servers[SelectedIdx - 1].Port; m_HostName = fmt::format("http://localhost:{}", ServerPort); } if (m_HostName.empty()) { // Single or zero instances, or not an interactive terminal: // fall back to default resolution (picks first instance or returns empty) m_HostName = ResolveTargetHostSpec("", ServerPort); } } else { if (m_All) { throw OptionParseException("--all cannot be used together with --hosturl", m_Options.help()); } m_HostName = ResolveTargetHostSpec(m_HostName, ServerPort); } if (m_HostName.empty()) { throw OptionParseException("Unable to resolve server specification", m_Options.help()); } if (IsUnixSocketSpec(m_HostName)) { throw std::runtime_error("Cannot open browser for a Unix domain socket connection"); } OpenBrowser(m_HostName); } } // namespace zen