// Copyright Epic Games, Inc. All Rights Reserved. #include "ui_cmd.h" #include "browser_launcher.h" #include "zenserviceclient.h" #include #include #include #include #include #include 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 += '/'; } } ExtendableStringBuilder<256> FullUrl; FullUrl << HostName << m_DashboardPath; LaunchBrowser(std::string_view(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); } ZenServiceClient Service({.HostSpec = m_HostName, .CommandName = Name}); if (IsUnixSocketSpec(Service.HostSpec())) { throw std::runtime_error("Cannot open browser for a Unix domain socket connection"); } OpenBrowser(Service.HostSpec()); } } // namespace zen