diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e77519d..7d618ea0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -764,6 +764,20 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h NoDelete: ") + # Adding an option for the start menu + set(CPACK_NSIS_MODIFY_PATH "OFF") + set(CPACK_NSIS_EXECUTABLES_DIRECTORY ".") + # This will be shown on the installed apps Windows settings + set(CPACK_NSIS_INSTALLED_ICON_NAME "${CMAKE_PROJECT_NAME}.exe") + set(CPACK_NSIS_CREATE_ICONS_EXTRA + "${CPACK_NSIS_CREATE_ICONS_EXTRA} + CreateShortCut '\$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${CMAKE_PROJECT_NAME}.lnk' \ + '\$INSTDIR\\\\${CMAKE_PROJECT_NAME}.exe' '--shortcut' + ") + set(CPACK_NSIS_DELETE_ICONS_EXTRA + "${CPACK_NSIS_DELETE_ICONS_EXTRA} + Delete '\$SMPROGRAMS\\\\$MUI_TEMP\\\\${CMAKE_PROJECT_NAME}.lnk' + ") # Checking for previous installed versions set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL "ON") diff --git a/src/config.cpp b/src/config.cpp index 60961cd6..820c32fc 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -16,6 +16,10 @@ #include "platform/common.h" +#ifdef _WIN32 + #include +#endif + namespace fs = std::filesystem; using namespace std::literals; @@ -1091,6 +1095,8 @@ namespace config { int parse(int argc, char *argv[]) { std::unordered_map cmd_vars; + bool shortcut_launch = false; + bool service_admin_launch = false; for (auto x = 1; x < argc; ++x) { auto line = argv[x]; @@ -1099,6 +1105,12 @@ namespace config { print_help(*argv); return 1; } + else if (line == "--shortcut"sv) { + shortcut_launch = true; + } + else if (line == "--shortcut-admin"sv) { + service_admin_launch = true; + } else if (*line == '-') { if (*(line + 1) == '-') { sunshine.cmd.name = line + 2; @@ -1156,6 +1168,51 @@ namespace config { apply_config(std::move(vars)); +#ifdef _WIN32 + // We have to wait until the config is loaded to handle these launches, + // because we need to have the correct base port loaded in our config. + if (service_admin_launch) { + // This is a relaunch as admin to start the service + service_ctrl::start_service(); + + // Always return 1 to ensure Sunshine doesn't start normally + return 1; + } + else if (shortcut_launch) { + if (!service_ctrl::is_service_running()) { + // If the service isn't running, relaunch ourselves as admin to start it + WCHAR executable[MAX_PATH]; + GetModuleFileNameW(NULL, executable, ARRAYSIZE(executable)); + + SHELLEXECUTEINFOW shell_exec_info {}; + shell_exec_info.cbSize = sizeof(shell_exec_info); + shell_exec_info.fMask = SEE_MASK_NOASYNC | SEE_MASK_NO_CONSOLE | SEE_MASK_NOCLOSEPROCESS; + shell_exec_info.lpVerb = L"runas"; + shell_exec_info.lpFile = executable; + shell_exec_info.lpParameters = L"--shortcut-admin"; + shell_exec_info.nShow = SW_NORMAL; + if (!ShellExecuteExW(&shell_exec_info)) { + auto winerr = GetLastError(); + std::cout << "Error: ShellExecuteEx() failed:"sv << winerr << std::endl; + return 1; + } + + // Wait for the elevated process to finish starting the service + WaitForSingleObject(shell_exec_info.hProcess, INFINITE); + CloseHandle(shell_exec_info.hProcess); + + // Wait for the UI to be ready for connections + service_ctrl::wait_for_ui_ready(); + } + + // Launch the web UI + launch_ui(); + + // Always return 1 to ensure Sunshine doesn't start normally + return 1; + } +#endif + return 0; } } // namespace config diff --git a/src/main.cpp b/src/main.cpp index e87dabc3..5a7348fb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -34,6 +34,10 @@ extern "C" { #include #include + +#ifdef _WIN32 + #include +#endif } safe::mail_t mail::man; @@ -141,6 +145,209 @@ namespace lifetime { } } // namespace lifetime +#ifdef _WIN32 +namespace service_ctrl { + class service_controller { + public: + /** + * @brief Constructor for service_controller class + * @param service_desired_access SERVICE_* desired access flags + */ + service_controller(DWORD service_desired_access) { + scm_handle = OpenSCManagerA(nullptr, nullptr, SC_MANAGER_CONNECT); + if (!scm_handle) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "OpenSCManager() failed: "sv << winerr; + return; + } + + service_handle = OpenServiceA(scm_handle, "SunshineSvc", service_desired_access); + if (!service_handle) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "OpenService() failed: "sv << winerr; + return; + } + } + + ~service_controller() { + if (service_handle) { + CloseServiceHandle(service_handle); + } + + if (scm_handle) { + CloseServiceHandle(scm_handle); + } + } + + /** + * @brief Asynchronously starts the Sunshine service + */ + bool + start_service() { + if (!service_handle) { + return false; + } + + if (!StartServiceA(service_handle, 0, nullptr)) { + auto winerr = GetLastError(); + if (winerr != ERROR_SERVICE_ALREADY_RUNNING) { + BOOST_LOG(error) << "StartService() failed: "sv << winerr; + return false; + } + } + + return true; + } + + /** + * @brief Query the service status + * @param status The SERVICE_STATUS struct to populate + */ + bool + query_service_status(SERVICE_STATUS &status) { + if (!service_handle) { + return false; + } + + if (!QueryServiceStatus(service_handle, &status)) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "QueryServiceStatus() failed: "sv << winerr; + return false; + } + + return true; + } + + private: + SC_HANDLE scm_handle = NULL; + SC_HANDLE service_handle = NULL; + }; + + /** + * @brief Check if the service is running + * + * EXAMPLES: + * ```cpp + * is_service_running(); + * ``` + */ + bool + is_service_running() { + service_controller sc { SERVICE_QUERY_STATUS }; + + SERVICE_STATUS status; + if (!sc.query_service_status(status)) { + return false; + } + + return status.dwCurrentState == SERVICE_RUNNING; + } + + /** + * @brief Start the service and wait for startup to complete + * + * EXAMPLES: + * ```cpp + * start_service(); + * ``` + */ + bool + start_service() { + service_controller sc { SERVICE_QUERY_STATUS | SERVICE_START }; + + std::cout << "Starting Sunshine..."sv; + + // This operation is asynchronous, so we must wait for it to complete + if (!sc.start_service()) { + return false; + } + + SERVICE_STATUS status; + do { + Sleep(1000); + std::cout << '.'; + } while (sc.query_service_status(status) && status.dwCurrentState == SERVICE_START_PENDING); + + if (status.dwCurrentState != SERVICE_RUNNING) { + BOOST_LOG(error) << SERVICE_NAME " failed to start: "sv << status.dwWin32ExitCode; + return false; + } + + std::cout << std::endl; + return true; + } + + /** + * @brief Wait for the UI to be ready after Sunshine startup + * + * EXAMPLES: + * ```cpp + * wait_for_ui_ready(); + * ``` + */ + bool + wait_for_ui_ready() { + std::cout << "Waiting for Web UI to be ready..."; + + // Wait up to 30 seconds for the web UI to start + for (int i = 0; i < 30; i++) { + PMIB_TCPTABLE tcp_table = nullptr; + ULONG table_size = 0; + ULONG err; + + auto fg = util::fail_guard([&tcp_table]() { + free(tcp_table); + }); + + do { + // Query all open TCP sockets to look for our web UI port + err = GetTcpTable(tcp_table, &table_size, false); + if (err == ERROR_INSUFFICIENT_BUFFER) { + free(tcp_table); + tcp_table = (PMIB_TCPTABLE) malloc(table_size); + } + } while (err == ERROR_INSUFFICIENT_BUFFER); + + if (err != NO_ERROR) { + BOOST_LOG(error) << "Failed to query TCP table: "sv << err; + return false; + } + + uint16_t port_nbo = htons(map_port(confighttp::PORT_HTTPS)); + for (DWORD i = 0; i < tcp_table->dwNumEntries; i++) { + auto &entry = tcp_table->table[i]; + + // Look for our port in the listening state + if (entry.dwLocalPort == port_nbo && entry.dwState == MIB_TCP_STATE_LISTEN) { + std::cout << std::endl; + return true; + } + } + + Sleep(1000); + std::cout << '.'; + } + + std::cout << "timed out"sv << std::endl; + return false; + } +} // namespace service_ctrl +#endif + +/** + * @brief Launch the Web UI + * + * EXAMPLES: + * ```cpp + * launch_ui(); + * ``` + */ +void +launch_ui() { + std::string url = "https://localhost:" + std::to_string(map_port(confighttp::PORT_HTTPS)); + platf::open_url(url); +} + /** * @brief Flush the log. * diff --git a/src/main.h b/src/main.h index 8e74c709..e046acb9 100644 --- a/src/main.h +++ b/src/main.h @@ -40,6 +40,8 @@ int write_file(const char *path, const std::string_view &contents); std::uint16_t map_port(int port); +void +launch_ui(); // namespaces namespace mail { @@ -73,4 +75,17 @@ namespace lifetime { get_argv(); } // namespace lifetime +#ifdef _WIN32 +namespace service_ctrl { + bool + is_service_running(); + + bool + start_service(); + + bool + wait_for_ui_ready(); +} // namespace service_ctrl +#endif + #endif // SUNSHINE_MAIN_H diff --git a/src/platform/common.h b/src/platform/common.h index 62c1d60f..5914017f 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -423,6 +423,13 @@ namespace platf { std::unique_ptr enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type); + /** + * @brief Open a url in the default web browser. + * @param url The url to open. + */ + void + open_url(const std::string &url); + input_t input(); void diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index 62fe3b4d..118912d1 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -178,6 +178,28 @@ namespace platf { } } + /** + * @brief Open a url in the default web browser. + * @param url The url to open. + */ + void + open_url(const std::string &url) { + // set working dir to user home directory + auto working_dir = boost::filesystem::path(std::getenv("HOME")); + std::string cmd = R"(xdg-open ")" + url + R"(")"; + + boost::process::environment _env = boost::this_process::environment(); + std::error_code ec; + auto child = run_command(false, cmd, working_dir, _env, nullptr, ec, nullptr); + if (ec) { + BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); + } + else { + BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; + child.detach(); + } + } + void adjust_thread_priority(thread_priority_e priority) { // Unimplemented diff --git a/src/platform/macos/misc.mm b/src/platform/macos/misc.mm index eff4bab1..46209dda 100644 --- a/src/platform/macos/misc.mm +++ b/src/platform/macos/misc.mm @@ -178,6 +178,27 @@ namespace platf { } } + /** + * @brief Open a url in the default web browser. + * @param url The url to open. + */ + void + open_url(const std::string &url) { + boost::filesystem::path working_dir; + std::string cmd = R"(open ")" + url + R"(")"; + + boost::process::environment _env = boost::this_process::environment(); + std::error_code ec; + auto child = run_command(false, cmd, working_dir, _env, nullptr, ec, nullptr); + if (ec) { + BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); + } + else { + BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; + child.detach(); + } + } + void adjust_thread_priority(thread_priority_e priority) { // Unimplemented diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index 6b529cd9..de851325 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -651,6 +651,31 @@ namespace platf { return create_boost_child_from_results(ret, cmd, ec, process_info, group); } + /** + * @brief Open a url in the default web browser. + * @param url The url to open. + */ + void + open_url(const std::string &url) { + // set working dir to Windows system directory + auto working_dir = boost::filesystem::path(std::getenv("SystemRoot")); + + // this isn't ideal as it briefly shows a command window + // but start a command built into cmd, not an executable + std::string cmd = R"(cmd /C "start )" + url + R"(")"; + + boost::process::environment _env = boost::this_process::environment(); + std::error_code ec; + auto child = run_command(false, cmd, working_dir, _env, nullptr, ec, nullptr); + if (ec) { + BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); + } + else { + BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; + child.detach(); + } + } + void adjust_thread_priority(thread_priority_e priority) { int win32_priority; diff --git a/src/system_tray.cpp b/src/system_tray.cpp index fde76c2e..c24c90aa 100644 --- a/src/system_tray.cpp +++ b/src/system_tray.cpp @@ -36,42 +36,6 @@ using namespace std::literals; // system_tray namespace namespace system_tray { - /** - * @brief Open a url in the default web browser. - * @param url The url to open. - */ - void - open_url(const std::string &url) { - boost::filesystem::path working_dir; - - // if windows - #if defined(_WIN32) - // set working dir to Windows system directory - working_dir = boost::filesystem::path(std::getenv("SystemRoot")); - - // this isn't ideal as it briefly shows a command window - // but start a command built into cmd, not an executable - std::string cmd = R"(cmd /C "start )" + url + R"(")"; - #elif defined(__linux__) || defined(linux) || defined(__linux) - // set working dir to user home directory - working_dir = boost::filesystem::path(std::getenv("HOME")); - std::string cmd = R"(xdg-open ")" + url + R"(")"; - #elif defined(__APPLE__) || defined(__MACH__) - std::string cmd = R"(open ")" + url + R"(")"; - #endif - - boost::process::environment _env = boost::this_process::environment(); - std::error_code ec; - auto child = platf::run_command(false, cmd, working_dir, _env, nullptr, ec, nullptr); - if (ec) { - BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); - } - else { - BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; - child.detach(); - } - } - /** * @brief Callback for opening the UI from the system tray. * @param item The tray menu item. @@ -79,12 +43,7 @@ namespace system_tray { void tray_open_ui_cb(struct tray_menu *item) { BOOST_LOG(info) << "Opening UI from system tray"sv; - - // create the url with the port - std::string url = "https://localhost:" + std::to_string(map_port(confighttp::PORT_HTTPS)); - - // open the url in the default web browser - open_url(url); + launch_ui(); } /** @@ -93,7 +52,7 @@ namespace system_tray { */ void tray_donate_github_cb(struct tray_menu *item) { - open_url("https://github.com/sponsors/LizardByte"); + platf::open_url("https://github.com/sponsors/LizardByte"); } /** @@ -102,7 +61,7 @@ namespace system_tray { */ void tray_donate_mee6_cb(struct tray_menu *item) { - open_url("https://mee6.xyz/m/804382334370578482"); + platf::open_url("https://mee6.xyz/m/804382334370578482"); } /** @@ -111,7 +70,7 @@ namespace system_tray { */ void tray_donate_patreon_cb(struct tray_menu *item) { - open_url("https://www.patreon.com/LizardByte"); + platf::open_url("https://www.patreon.com/LizardByte"); } /** @@ -120,7 +79,7 @@ namespace system_tray { */ void tray_donate_paypal_cb(struct tray_menu *item) { - open_url("https://www.paypal.com/paypalme/ReenigneArcher"); + platf::open_url("https://www.paypal.com/paypalme/ReenigneArcher"); } /**