From 6593fa5d61c113b4f642bebaf8e4dffb1c37a624 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 24 Aug 2024 20:14:45 -0400 Subject: [PATCH 1/9] feat: add publisher metadata (#3080) --- .github/workflows/CI.yml | 11 +- .gitignore | 4 + cmake/compile_definitions/common.cmake | 5 + cmake/prep/options.cmake | 9 ++ docker/debian-bookworm.dockerfile | 6 +- docker/fedora-39.dockerfile | 6 +- docker/fedora-40.dockerfile | 6 +- docker/ubuntu-22.04.dockerfile | 6 +- docker/ubuntu-24.04.dockerfile | 6 +- packaging/linux/Arch/PKGBUILD | 5 +- .../flatpak/dev.lizardbyte.app.Sunshine.yml | 5 +- packaging/macos/Portfile | 5 +- packaging/sunshine.rb | 3 + scripts/linux_build.sh | 43 ++++-- src/crypto.h | 5 + src/entry_handler.cpp | 7 + src/entry_handler.h | 6 + src/main.cpp | 3 + tests/tests_log_checker.h | 135 ++++++++++++++++++ tests/unit/test_entry_handler.cpp | 18 +++ tests/unit/test_logging.cpp | 24 +--- 21 files changed, 279 insertions(+), 39 deletions(-) create mode 100644 tests/tests_log_checker.h create mode 100644 tests/unit/test_entry_handler.cpp diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4ac6c217..e91ce9f3 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -325,7 +325,13 @@ jobs: COMMIT: ${{ needs.setup_release.outputs.release_commit }} run: | chmod +x ./scripts/linux_build.sh - ./scripts/linux_build.sh --skip-cleanup --skip-package --ubuntu-test-repo ${{ matrix.EXTRA_ARGS }} + ./scripts/linux_build.sh \ + --publisher-name='${{ github.repository_owner }}' \ + --publisher-website='https://app.lizardbyte.dev' \ + --publisher-issue-url='https://app.lizardbyte.dev/support' \ + --skip-cleanup \ + --skip-package \ + --ubuntu-test-repo ${{ matrix.EXTRA_ARGS }} - name: Set AppImage Version if: | @@ -1101,6 +1107,9 @@ jobs: -DBUILD_WERROR=ON \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DSUNSHINE_ASSETS_DIR=assets \ + -DSUNSHINE_PUBLSIHER_NAME='${{ github.repository_owner }}' \ + -DSUNSHINE_PUBLISHER_WEBSITE='https://app.lizardbyte.dev' \ + -DSUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support' \ -DTESTS_SOFTWARE_ENCODER_UNAVAILABLE='skip' ninja -C build diff --git a/.gitignore b/.gitignore index b3c5bd85..30818d52 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,7 @@ package-lock.json # Dummy macOS files .DS_Store + +# Python +*.pyc +venv/ diff --git a/cmake/compile_definitions/common.cmake b/cmake/compile_definitions/common.cmake index a2260595..7b13168a 100644 --- a/cmake/compile_definitions/common.cmake +++ b/cmake/compile_definitions/common.cmake @@ -124,6 +124,11 @@ list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_ASSETS_DIR="${SUNSHINE_ASSETS_DIR_DEF} list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_TRAY=${SUNSHINE_TRAY}) +# Publisher metadata +list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_PUBLISHER_NAME="${SUNSHINE_PUBLISHER_NAME}") +list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_PUBLISHER_WEBSITE="${SUNSHINE_PUBLISHER_WEBSITE}") +list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_PUBLISHER_ISSUE_URL="${SUNSHINE_PUBLISHER_ISSUE_URL}") + include_directories("${CMAKE_SOURCE_DIR}") include_directories( diff --git a/cmake/prep/options.cmake b/cmake/prep/options.cmake index f358f727..f1b33f08 100644 --- a/cmake/prep/options.cmake +++ b/cmake/prep/options.cmake @@ -1,3 +1,12 @@ +# Publisher Metadata +set(SUNSHINE_PUBLISHER_NAME "Third Party Publisher" + CACHE STRING "The name of the publisher (not developer) of the application.") +set(SUNSHINE_PUBLISHER_WEBSITE "" + CACHE STRING "The URL of the publisher's website.") +set(SUNSHINE_PUBLISHER_ISSUE_URL "https://app.lizardbyte.dev/support" + CACHE STRING "The URL of the publisher's support site or issue tracker. + If you provide a modified version of Sunshine, we kindly request that you use your own url.") + option(BUILD_DOCS "Build documentation" ON) option(BUILD_TESTS "Build tests" ON) option(TESTS_ENABLE_PYTHON_TESTS "Enable Python tests" ON) diff --git a/docker/debian-bookworm.dockerfile b/docker/debian-bookworm.dockerfile index 8a6db148..b98bb96d 100644 --- a/docker/debian-bookworm.dockerfile +++ b/docker/debian-bookworm.dockerfile @@ -31,7 +31,11 @@ RUN <<_BUILD #!/bin/bash set -e chmod +x ./scripts/linux_build.sh -./scripts/linux_build.sh --sudo-off +./scripts/linux_build.sh \ + --publisher-name='LizardByte' \ + --publisher-website='https://app.lizardbyte.dev' \ + --publisher-issue-url='https://app.lizardbyte.dev/support' \ + --sudo-off apt-get clean rm -rf /var/lib/apt/lists/* _BUILD diff --git a/docker/fedora-39.dockerfile b/docker/fedora-39.dockerfile index e50fffdb..87d2eb29 100644 --- a/docker/fedora-39.dockerfile +++ b/docker/fedora-39.dockerfile @@ -29,7 +29,11 @@ RUN <<_BUILD #!/bin/bash set -e chmod +x ./scripts/linux_build.sh -./scripts/linux_build.sh --sudo-off +./scripts/linux_build.sh \ + --publisher-name='LizardByte' \ + --publisher-website='https://app.lizardbyte.dev' \ + --publisher-issue-url='https://app.lizardbyte.dev/support' \ + --sudo-off dnf clean all rm -rf /var/cache/yum _BUILD diff --git a/docker/fedora-40.dockerfile b/docker/fedora-40.dockerfile index 24735eef..61c6cced 100644 --- a/docker/fedora-40.dockerfile +++ b/docker/fedora-40.dockerfile @@ -29,7 +29,11 @@ RUN <<_BUILD #!/bin/bash set -e chmod +x ./scripts/linux_build.sh -./scripts/linux_build.sh --sudo-off +./scripts/linux_build.sh \ + --publisher-name='LizardByte' \ + --publisher-website='https://app.lizardbyte.dev' \ + --publisher-issue-url='https://app.lizardbyte.dev/support' \ + --sudo-off dnf clean all rm -rf /var/cache/yum _BUILD diff --git a/docker/ubuntu-22.04.dockerfile b/docker/ubuntu-22.04.dockerfile index bfb2c3d0..46952532 100644 --- a/docker/ubuntu-22.04.dockerfile +++ b/docker/ubuntu-22.04.dockerfile @@ -31,7 +31,11 @@ RUN <<_BUILD #!/bin/bash set -e chmod +x ./scripts/linux_build.sh -./scripts/linux_build.sh --sudo-off +./scripts/linux_build.sh \ + --publisher-name='LizardByte' \ + --publisher-website='https://app.lizardbyte.dev' \ + --publisher-issue-url='https://app.lizardbyte.dev/support' \ + --sudo-off apt-get clean rm -rf /var/lib/apt/lists/* _BUILD diff --git a/docker/ubuntu-24.04.dockerfile b/docker/ubuntu-24.04.dockerfile index cbadaa62..1decc62f 100644 --- a/docker/ubuntu-24.04.dockerfile +++ b/docker/ubuntu-24.04.dockerfile @@ -31,7 +31,11 @@ RUN <<_BUILD #!/bin/bash set -e chmod +x ./scripts/linux_build.sh -./scripts/linux_build.sh --sudo-off +./scripts/linux_build.sh \ + --publisher-name='LizardByte' \ + --publisher-website='https://app.lizardbyte.dev' \ + --publisher-issue-url='https://app.lizardbyte.dev/support' \ + --sudo-off apt-get clean rm -rf /var/lib/apt/lists/* _BUILD diff --git a/packaging/linux/Arch/PKGBUILD b/packaging/linux/Arch/PKGBUILD index 496ab699..e8eb8521 100644 --- a/packaging/linux/Arch/PKGBUILD +++ b/packaging/linux/Arch/PKGBUILD @@ -83,7 +83,10 @@ build() { -D BUILD_WERROR=ON \ -D CMAKE_INSTALL_PREFIX=/usr \ -D SUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine \ - -D SUNSHINE_ASSETS_DIR="share/sunshine" + -D SUNSHINE_ASSETS_DIR="share/sunshine" \ + -D SUNSHINE_PUBLSIHER_NAME='LizardByte' \ + -D SUNSHINE_PUBLISHER_WEBSITE='https://app.lizardbyte.dev' \ + -D SUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support' make -C build } diff --git a/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml b/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml index d2b71022..f5e90739 100644 --- a/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml +++ b/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml @@ -71,12 +71,15 @@ modules: - -DCMAKE_BUILD_TYPE=Release - -DCMAKE_CUDA_COMPILER=/app/cuda/bin/nvcc - -DSUNSHINE_ASSETS_DIR=share/sunshine + - -DSUNSHINE_BUILD_FLATPAK=ON - -DSUNSHINE_EXECUTABLE_PATH=/app/bin/sunshine - -DSUNSHINE_ENABLE_WAYLAND=ON - -DSUNSHINE_ENABLE_X11=ON - -DSUNSHINE_ENABLE_DRM=ON - -DSUNSHINE_ENABLE_CUDA=ON - - -DSUNSHINE_BUILD_FLATPAK=ON + - -DSUNSHINE_PUBLSIHER_NAME='LizardByte' + -DSUNSHINE_PUBLISHER_WEBSITE='https://app.lizardbyte.dev' + -DSUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support' sources: - type: git url: "@GITHUB_CLONE_URL@" diff --git a/packaging/macos/Portfile b/packaging/macos/Portfile index 1b9e1bda..a2a9eb5e 100644 --- a/packaging/macos/Portfile +++ b/packaging/macos/Portfile @@ -43,7 +43,10 @@ depends_lib port:curl \ configure.args -DBOOST_USE_STATIC=ON \ -DBUILD_WERROR=ON \ -DCMAKE_INSTALL_PREFIX=${prefix} \ - -DSUNSHINE_ASSETS_DIR=etc/sunshine/assets + -DSUNSHINE_ASSETS_DIR=etc/sunshine/assets \ + -DSUNSHINE_PUBLSIHER_NAME='LizardByte' \ + -DSUNSHINE_PUBLISHER_WEBSITE='https://app.lizardbyte.dev' \ + -DSUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support' configure.env-append BRANCH=@GITHUB_BRANCH@ configure.env-append BUILD_VERSION=@BUILD_VERSION@ diff --git a/packaging/sunshine.rb b/packaging/sunshine.rb index cf6886b4..a8d9a661 100644 --- a/packaging/sunshine.rb +++ b/packaging/sunshine.rb @@ -71,6 +71,9 @@ class @PROJECT_NAME@ < Formula -DSUNSHINE_ASSETS_DIR=sunshine/assets -DSUNSHINE_BUILD_HOMEBREW=ON -DSUNSHINE_ENABLE_TRAY=OFF + -DSUNSHINE_PUBLSIHER_NAME='LizardByte' + -DSUNSHINE_PUBLISHER_WEBSITE='https://app.lizardbyte.dev' + -DSUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support' ] if build.with? "docs-off" diff --git a/scripts/linux_build.sh b/scripts/linux_build.sh index 0ad3aaf6..f0cd3d7f 100644 --- a/scripts/linux_build.sh +++ b/scripts/linux_build.sh @@ -3,6 +3,9 @@ set -e # Default value for arguments appimage_build=0 +publisher_name="Third Party Publisher" +publisher_website="" +publisher_issue_url="https://app.lizardbyte.dev/support" skip_cleanup=0 skip_cuda=0 skip_libva=0 @@ -21,14 +24,18 @@ Usage: $0 [options] Options: - -h, --help Display this help message. - -s, --sudo-off Disable sudo command. - --appimage-build Compile for AppImage, this will not create the AppImage, just the executable. - --skip-cleanup Do not restore the original gcc alternatives, or the math-vector.h file. - --skip-cuda Skip CUDA installation. - --skip-libva Skip libva installation. This will automatically be enabled if passing --appimage-build. - --skip-package Skip creating DEB, or RPM package. - --ubuntu-test-repo Install ppa:ubuntu-toolchain-r/test repo on Ubuntu. + -h, --help Display this help message. + -s, --sudo-off Disable sudo command. + --appimage-build Compile for AppImage, this will not create the AppImage, just the executable. + --publisher-name The name of the publisher (not developer) of the application. + --publisher-website The URL of the publisher's website. + --publisher-issue-url The URL of the publisher's support site or issue tracker. + If you provide a modified version of Sunshine, we kindly request that you use your own url. + --skip-cleanup Do not restore the original gcc alternatives, or the math-vector.h file. + --skip-cuda Skip CUDA installation. + --skip-libva Skip libva installation. This will automatically be enabled if passing --appimage-build. + --skip-package Skip creating DEB, or RPM package. + --ubuntu-test-repo Install ppa:ubuntu-toolchain-r/test repo on Ubuntu. EOF exit "$exit_code" @@ -46,6 +53,15 @@ while getopts ":hs-:" opt; do appimage_build=1 skip_libva=1 ;; + publisher-name=*) + publisher_name="${OPTARG#*=}" + ;; + publisher-website=*) + publisher_website="${OPTARG#*=}" + ;; + publisher-issue-url=*) + publisher_issue_url="${OPTARG#*=}" + ;; skip-cleanup) skip_cleanup=1 ;; skip-cuda) skip_cuda=1 ;; skip-libva) skip_libva=1 ;; @@ -268,6 +284,17 @@ function run_install() { cmake_args+=("-DSUNSHINE_BUILD_APPIMAGE=ON") fi + # Publisher metadata + if [ -n "$publisher_name" ]; then + cmake_args+=("-DSUNSHINE_PUBLISHER_NAME='${publisher_name}'") + fi + if [ -n "$publisher_website" ]; then + cmake_args+=("-DSUNSHINE_PUBLISHER_WEBSITE='${publisher_website}'") + fi + if [ -n "$publisher_issue_url" ]; then + cmake_args+=("-DSUNSHINE_PUBLISHER_ISSUE_URL='${publisher_issue_url}'") + fi + # Update the package list $package_update_command diff --git a/src/crypto.h b/src/crypto.h index 20651ecb..859c6675 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -34,6 +34,11 @@ namespace crypto { using pkey_ctx_t = util::safe_ptr; using bignum_t = util::safe_ptr; + /** + * @brief Hashes the given plaintext using SHA-256. + * @param plaintext + * @return The SHA-256 hash of the plaintext. + */ sha256_t hash(const std::string_view &plaintext); diff --git a/src/entry_handler.cpp b/src/entry_handler.cpp index fb1ece2c..2f755e9a 100644 --- a/src/entry_handler.cpp +++ b/src/entry_handler.cpp @@ -109,6 +109,13 @@ namespace lifetime { } } // namespace lifetime +void +log_publisher_data() { + BOOST_LOG(info) << "Package Publisher: "sv << SUNSHINE_PUBLISHER_NAME; + BOOST_LOG(info) << "Publisher Website: "sv << SUNSHINE_PUBLISHER_WEBSITE; + BOOST_LOG(info) << "Get support: "sv << SUNSHINE_PUBLISHER_ISSUE_URL; +} + #ifdef _WIN32 bool is_gamestream_enabled() { diff --git a/src/entry_handler.h b/src/entry_handler.h index 7e680b43..a2c9735a 100644 --- a/src/entry_handler.h +++ b/src/entry_handler.h @@ -108,6 +108,12 @@ namespace lifetime { get_argv(); } // namespace lifetime +/** + * @brief Log the publisher metadata provided from CMake. + */ +void +log_publisher_data(); + #ifdef _WIN32 /** * @brief Check if NVIDIA's GameStream software is running. diff --git a/src/main.cpp b/src/main.cpp index 1a1d5ef2..c4ded3d5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -114,6 +114,9 @@ main(int argc, char *argv[]) { // the version should be printed to the log before anything else BOOST_LOG(info) << PROJECT_NAME << " version: " << PROJECT_VER; + // Log publisher metadata + log_publisher_data(); + if (!config::sunshine.cmd.name.empty()) { auto fn = cmd_to_func.find(config::sunshine.cmd.name); if (fn == std::end(cmd_to_func)) { diff --git a/tests/tests_log_checker.h b/tests/tests_log_checker.h new file mode 100644 index 00000000..656151f6 --- /dev/null +++ b/tests/tests_log_checker.h @@ -0,0 +1,135 @@ +/** + * @file tests/tests_log_checker.h + * @brief Utility functions to check log file contents. + */ +#pragma once + +#include +#include +#include +#include + +#include + +namespace log_checker { + + /** + * @brief Remove the timestamp prefix from a log line. + * @param line The log line. + * @return The log line without the timestamp prefix. + */ + inline std::string + remove_timestamp_prefix(const std::string &line) { + static const std::regex timestamp_regex(R"(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\]: )"); + return std::regex_replace(line, timestamp_regex, ""); + } + + /** + * @brief Check if a log file contains a line that starts with the given string. + * @param log_file Path to the log file. + * @param start_str The string that the line should start with. + * @return True if such a line is found, false otherwise. + */ + inline bool + line_starts_with(const std::string &log_file, const std::string_view &start_str) { + logging::log_flush(); + + std::ifstream input(log_file); + if (!input.is_open()) { + return false; + } + + for (std::string line; std::getline(input, line);) { + line = remove_timestamp_prefix(line); + if (line.rfind(start_str, 0) == 0) { + return true; + } + } + return false; + } + + /** + * @brief Check if a log file contains a line that ends with the given string. + * @param log_file Path to the log file. + * @param end_str The string that the line should end with. + * @return True if such a line is found, false otherwise. + */ + inline bool + line_ends_with(const std::string &log_file, const std::string_view &end_str) { + logging::log_flush(); + + std::ifstream input(log_file); + if (!input.is_open()) { + return false; + } + + for (std::string line; std::getline(input, line);) { + line = remove_timestamp_prefix(line); + if (line.size() >= end_str.size() && + line.compare(line.size() - end_str.size(), end_str.size(), end_str) == 0) { + return true; + } + } + return false; + } + + /** + * @brief Check if a log file contains a line that equals the given string. + * @param log_file Path to the log file. + * @param str The string that the line should equal. + * @return True if such a line is found, false otherwise. + */ + inline bool + line_equals(const std::string &log_file, const std::string_view &str) { + logging::log_flush(); + + std::ifstream input(log_file); + if (!input.is_open()) { + return false; + } + + for (std::string line; std::getline(input, line);) { + line = remove_timestamp_prefix(line); + if (line == str) { + return true; + } + } + return false; + } + + /** + * @brief Check if a log file contains a line that contains the given substring. + * @param log_file Path to the log file. + * @param substr The substring to search for. + * @param case_insensitive Whether the search should be case-insensitive. + * @return True if such a line is found, false otherwise. + */ + inline bool + line_contains(const std::string &log_file, const std::string_view &substr, bool case_insensitive = false) { + logging::log_flush(); + + std::ifstream input(log_file); + if (!input.is_open()) { + return false; + } + + std::string search_str(substr); + if (case_insensitive) { + // sonarcloud complains about this, but the solution doesn't work for macOS-12 + std::transform(search_str.begin(), search_str.end(), search_str.begin(), ::tolower); + } + + for (std::string line; std::getline(input, line);) { + line = remove_timestamp_prefix(line); + if (case_insensitive) { + // sonarcloud complains about this, but the solution doesn't work for macOS-12 + std::transform(line.begin(), line.end(), line.begin(), ::tolower); + } + if (line.find(search_str) != std::string::npos) { + return true; + } + } + return false; + } + +} // namespace log_checker diff --git a/tests/unit/test_entry_handler.cpp b/tests/unit/test_entry_handler.cpp new file mode 100644 index 00000000..d1e2b061 --- /dev/null +++ b/tests/unit/test_entry_handler.cpp @@ -0,0 +1,18 @@ +/** + * @file tests/unit/test_entry_handler.cpp + * @brief Test src/entry_handler.*. + */ +#include + +#include "../tests_common.h" +#include "../tests_log_checker.h" + +TEST(EntryHandlerTests, LogPublisherDataTest) { + // call log_publisher_data + log_publisher_data(); + + // check if specific log messages exist + ASSERT_TRUE(log_checker::line_starts_with("test_sunshine.log", "Info: Package Publisher: ")); + ASSERT_TRUE(log_checker::line_starts_with("test_sunshine.log", "Info: Publisher Website: ")); + ASSERT_TRUE(log_checker::line_starts_with("test_sunshine.log", "Info: Get support: ")); +} diff --git a/tests/unit/test_logging.cpp b/tests/unit/test_logging.cpp index 99b4b264..b53b493b 100644 --- a/tests/unit/test_logging.cpp +++ b/tests/unit/test_logging.cpp @@ -5,8 +5,8 @@ #include #include "../tests_common.h" +#include "../tests_log_checker.h" -#include #include namespace { @@ -40,25 +40,5 @@ TEST_P(LogLevelsTest, PutMessage) { auto test_message = std::to_string(rand_gen()) + std::to_string(rand_gen()); BOOST_LOG(logger) << test_message; - // Flush logger and search for the message in the log file - - logging::log_flush(); - - std::ifstream input(log_file); - ASSERT_TRUE(input.is_open()); - - bool found = false; - for (std::string line; std::getline(input, line);) { - if (line.find(test_message) != std::string::npos) { - // Assume that logger may change the case of log level label - std::transform(line.begin(), line.end(), line.begin(), - [](char c) { return std::tolower(c); }); - - if (line.find(label) != std::string::npos) { - found = true; - break; - } - } - } - ASSERT_TRUE(found); + ASSERT_TRUE(log_checker::line_contains(log_file, test_message)); } From ddd67ce01dec055e44a2809ba33fc4dceafc2d62 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 24 Aug 2024 22:39:48 -0400 Subject: [PATCH 2/9] build(docker): update dockerfiles (#3085) --- docker/archlinux.dockerfile | 37 +++++++++++++++++-------------- docker/clion-toolchain.dockerfile | 30 +++++++++++-------------- docker/debian-bookworm.dockerfile | 6 ++--- docker/fedora-39.dockerfile | 6 ++--- docker/fedora-40.dockerfile | 6 ++--- docker/ubuntu-22.04.dockerfile | 6 ++--- docker/ubuntu-24.04.dockerfile | 6 ++--- 7 files changed, 48 insertions(+), 49 deletions(-) diff --git a/docker/archlinux.dockerfile b/docker/archlinux.dockerfile index 5edb4416..f628ac07 100644 --- a/docker/archlinux.dockerfile +++ b/docker/archlinux.dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.4 +# syntax=docker/dockerfile:1 # artifacts: true # platforms: linux/amd64 # archlinux does not have an arm64 base image @@ -17,7 +17,7 @@ pacman -Syu --disable-download-timeout --noconfirm pacman -Scc --noconfirm _DEPS -FROM sunshine-base as sunshine-build +FROM sunshine-base AS sunshine-build ARG BRANCH ARG BUILD_VERSION @@ -31,18 +31,19 @@ ENV COMMIT=${COMMIT} SHELL ["/bin/bash", "-o", "pipefail", "-c"] -# Setup builder user, arch prevents running makepkg as root -RUN useradd -m builder && \ - echo 'builder ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers - -# patch the build flags # hadolint ignore=SC2016 -RUN sed -i 's,#MAKEFLAGS="-j2",MAKEFLAGS="-j$(nproc)",g' /etc/makepkg.conf - -# install dependencies -RUN <<_DEPS +RUN <<_SETUP #!/bin/bash set -e + +# Setup builder user, arch prevents running makepkg as root +useradd -m builder +echo 'builder ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers + +# patch the build flags +sed -i 's,#MAKEFLAGS="-j2",MAKEFLAGS="-j$(nproc)",g' /etc/makepkg.conf + +# install dependencies pacman -Syu --disable-download-timeout --needed --noconfirm \ base-devel \ cmake \ @@ -51,7 +52,7 @@ pacman -Syu --disable-download-timeout --needed --noconfirm \ namcap \ xorg-server-xvfb pacman -Scc --noconfirm -_DEPS +_SETUP # Setup builder user USER builder @@ -84,9 +85,11 @@ cmake \ _MAKE WORKDIR /build/sunshine/pkg -RUN mv /build/sunshine/build/PKGBUILD . -RUN mv /build/sunshine/build/sunshine.install . -RUN makepkg --printsrcinfo > .SRCINFO +RUN <<_PACKAGE +mv /build/sunshine/build/PKGBUILD . +mv /build/sunshine/build/sunshine.install . +makepkg --printsrcinfo > .SRCINFO +_PACKAGE # create a PKGBUILD archive USER root @@ -111,12 +114,12 @@ rm -f /build/sunshine/pkg/sunshine-debug*.pkg.tar.zst ls -a _PKGBUILD -FROM scratch as artifacts +FROM scratch AS artifacts COPY --link --from=sunshine-build /build/sunshine/pkg/sunshine*.pkg.tar.zst /sunshine.pkg.tar.zst COPY --link --from=sunshine-build /build/sunshine/sunshine.pkg.tar.gz /sunshine.pkg.tar.gz -FROM sunshine-base as sunshine +FROM sunshine-base AS sunshine COPY --link --from=artifacts /sunshine.pkg.tar.zst / diff --git a/docker/clion-toolchain.dockerfile b/docker/clion-toolchain.dockerfile index 647a4bed..6acd82ff 100644 --- a/docker/clion-toolchain.dockerfile +++ b/docker/clion-toolchain.dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.4 +# syntax=docker/dockerfile:1 # artifacts: false # platforms: linux/amd64 # platforms_pr: linux/amd64 @@ -9,7 +9,7 @@ FROM ${BASE}:${TAG} AS toolchain-base ENV DEBIAN_FRONTEND=noninteractive -FROM toolchain-base as toolchain +FROM toolchain-base AS toolchain ARG TARGETPLATFORM RUN echo "target_platform: ${TARGETPLATFORM}" @@ -17,7 +17,9 @@ RUN echo "target_platform: ${TARGETPLATFORM}" ENV DISPLAY=:0 SHELL ["/bin/bash", "-o", "pipefail", "-c"] + # install dependencies +# hadolint ignore=SC1091 RUN <<_DEPS #!/bin/bash set -e @@ -59,20 +61,14 @@ apt-get install -y --no-install-recommends \ xvfb apt-get clean rm -rf /var/lib/apt/lists/* -_DEPS -#Install Node -# hadolint ignore=SC1091 -RUN <<_INSTALL_NODE -#!/bin/bash -set -e -node_version="20.9.0" -wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash +# Install Node +wget --max-redirect=0 -qO- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash source "$HOME/.nvm/nvm.sh" -nvm install "$node_version" -nvm use "$node_version" -nvm alias default "$node_version" -_INSTALL_NODE +nvm install node +nvm use node +nvm alias default node +_DEPS # install cuda WORKDIR /build/cuda @@ -110,13 +106,13 @@ else exec "\$@" fi EOF -_ENTRYPOINT # Make the script executable -RUN chmod +x /entrypoint.sh +chmod +x /entrypoint.sh # Note about CLion -RUN echo "ATTENTION: CLion will override the entrypoint, you can disable this in the toolchain settings" +echo "ATTENTION: CLion will override the entrypoint, you can disable this in the toolchain settings" +_ENTRYPOINT # Use the shell script as the entrypoint ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/debian-bookworm.dockerfile b/docker/debian-bookworm.dockerfile index b98bb96d..84edc4c9 100644 --- a/docker/debian-bookworm.dockerfile +++ b/docker/debian-bookworm.dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.4 +# syntax=docker/dockerfile:1 # artifacts: true # platforms: linux/amd64,linux/arm64/v8 # platforms_pr: linux/amd64 @@ -9,7 +9,7 @@ FROM ${BASE}:${TAG} AS sunshine-base ENV DEBIAN_FRONTEND=noninteractive -FROM sunshine-base as sunshine-build +FROM sunshine-base AS sunshine-build ARG BRANCH ARG BUILD_VERSION @@ -57,7 +57,7 @@ ARG TAG ARG TARGETARCH COPY --link --from=sunshine-build /build/sunshine/build/cpack_artifacts/Sunshine.deb /sunshine-${BASE}-${TAG}-${TARGETARCH}.deb -FROM sunshine-base as sunshine +FROM sunshine-base AS sunshine # copy deb from builder COPY --link --from=artifacts /sunshine*.deb /sunshine.deb diff --git a/docker/fedora-39.dockerfile b/docker/fedora-39.dockerfile index 87d2eb29..579eec9e 100644 --- a/docker/fedora-39.dockerfile +++ b/docker/fedora-39.dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.4 +# syntax=docker/dockerfile:1 # artifacts: true # platforms: linux/amd64 # platforms_pr: linux/amd64 @@ -7,7 +7,7 @@ ARG BASE=fedora ARG TAG=39 FROM ${BASE}:${TAG} AS sunshine-base -FROM sunshine-base as sunshine-build +FROM sunshine-base AS sunshine-build ARG BRANCH ARG BUILD_VERSION @@ -55,7 +55,7 @@ ARG TAG ARG TARGETARCH COPY --link --from=sunshine-build /build/sunshine/build/cpack_artifacts/Sunshine.rpm /sunshine-${BASE}-${TAG}-${TARGETARCH}.rpm -FROM sunshine-base as sunshine +FROM sunshine-base AS sunshine # copy deb from builder COPY --link --from=artifacts /sunshine*.rpm /sunshine.rpm diff --git a/docker/fedora-40.dockerfile b/docker/fedora-40.dockerfile index 61c6cced..fc6362d4 100644 --- a/docker/fedora-40.dockerfile +++ b/docker/fedora-40.dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.4 +# syntax=docker/dockerfile:1 # artifacts: true # platforms: linux/amd64 # platforms_pr: linux/amd64 @@ -7,7 +7,7 @@ ARG BASE=fedora ARG TAG=40 FROM ${BASE}:${TAG} AS sunshine-base -FROM sunshine-base as sunshine-build +FROM sunshine-base AS sunshine-build ARG BRANCH ARG BUILD_VERSION @@ -55,7 +55,7 @@ ARG TAG ARG TARGETARCH COPY --link --from=sunshine-build /build/sunshine/build/cpack_artifacts/Sunshine.rpm /sunshine-${BASE}-${TAG}-${TARGETARCH}.rpm -FROM sunshine-base as sunshine +FROM sunshine-base AS sunshine # copy deb from builder COPY --link --from=artifacts /sunshine*.rpm /sunshine.rpm diff --git a/docker/ubuntu-22.04.dockerfile b/docker/ubuntu-22.04.dockerfile index 46952532..24ceda2b 100644 --- a/docker/ubuntu-22.04.dockerfile +++ b/docker/ubuntu-22.04.dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.4 +# syntax=docker/dockerfile:1 # artifacts: true # platforms: linux/amd64,linux/arm64/v8 # platforms_pr: linux/amd64 @@ -9,7 +9,7 @@ FROM ${BASE}:${TAG} AS sunshine-base ENV DEBIAN_FRONTEND=noninteractive -FROM sunshine-base as sunshine-build +FROM sunshine-base AS sunshine-build ARG BRANCH ARG BUILD_VERSION @@ -57,7 +57,7 @@ ARG TAG ARG TARGETARCH COPY --link --from=sunshine-build /build/sunshine/build/cpack_artifacts/Sunshine.deb /sunshine-${BASE}-${TAG}-${TARGETARCH}.deb -FROM sunshine-base as sunshine +FROM sunshine-base AS sunshine # copy deb from builder COPY --link --from=artifacts /sunshine*.deb /sunshine.deb diff --git a/docker/ubuntu-24.04.dockerfile b/docker/ubuntu-24.04.dockerfile index 1decc62f..1b7c0d6e 100644 --- a/docker/ubuntu-24.04.dockerfile +++ b/docker/ubuntu-24.04.dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.4 +# syntax=docker/dockerfile:1 # artifacts: true # platforms: linux/amd64,linux/arm64/v8 # platforms_pr: linux/amd64 @@ -9,7 +9,7 @@ FROM ${BASE}:${TAG} AS sunshine-base ENV DEBIAN_FRONTEND=noninteractive -FROM sunshine-base as sunshine-build +FROM sunshine-base AS sunshine-build ARG BRANCH ARG BUILD_VERSION @@ -57,7 +57,7 @@ ARG TAG ARG TARGETARCH COPY --link --from=sunshine-build /build/sunshine/build/cpack_artifacts/Sunshine.deb /sunshine-${BASE}-${TAG}-${TARGETARCH}.deb -FROM sunshine-base as sunshine +FROM sunshine-base AS sunshine # copy deb from builder COPY --link --from=artifacts /sunshine*.deb /sunshine.deb From bf92fda969367d9dbf427db6669ec012e86b7376 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sun, 25 Aug 2024 10:50:51 -0400 Subject: [PATCH 3/9] fix(linux): enable lowlatency mode for AMD (#3088) --- src/platform/common.h | 17 ++++++++++ src/platform/linux/misc.cpp | 14 +++++++++ src/platform/macos/misc.mm | 10 ++++++ src/platform/windows/misc.cpp | 10 ++++++ tests/unit/platform/test_common.cpp | 49 +++++++++++++++++++++++++++++ 5 files changed, 100 insertions(+) create mode 100644 tests/unit/platform/test_common.cpp diff --git a/src/platform/common.h b/src/platform/common.h index 5009c183..bd594ba1 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -612,6 +612,23 @@ namespace platf { void restart(); + /** + * @brief Set an environment variable. + * @param name The name of the environment variable. + * @param value The value to set the environment variable to. + * @return 0 on success, non-zero on failure. + */ + int + set_env(const std::string &name, const std::string &value); + + /** + * @brief Unset an environment variable. + * @param name The name of the environment variable. + * @return 0 on success, non-zero on failure. + */ + int + unset_env(const std::string &name); + struct buffer_descriptor_t { const char *buffer; size_t size; diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index 9997c405..b3e31ec6 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -326,6 +326,16 @@ namespace platf { lifetime::exit_sunshine(0, true); } + int + set_env(const std::string &name, const std::string &value) { + return setenv(name.c_str(), value.c_str(), 1); + } + + int + unset_env(const std::string &name) { + return unsetenv(name.c_str()); + } + bool request_process_group_exit(std::uintptr_t native_handle) { if (kill(-((pid_t) native_handle), SIGTERM) == 0 || errno == ESRCH) { @@ -913,6 +923,10 @@ namespace platf { std::unique_ptr init() { + // enable low latency mode for AMD + // https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/30039 + set_env("AMD_DEBUG", "lowlatency"); + // These are allowed to fail. gbm::init(); diff --git a/src/platform/macos/misc.mm b/src/platform/macos/misc.mm index f03dd3bf..6eca7d2e 100644 --- a/src/platform/macos/misc.mm +++ b/src/platform/macos/misc.mm @@ -253,6 +253,16 @@ namespace platf { lifetime::exit_sunshine(0, true); } + int + set_env(const std::string &name, const std::string &value) { + return setenv(name.c_str(), value.c_str(), 1); + } + + int + unset_env(const std::string &name) { + return unsetenv(name.c_str()); + } + bool request_process_group_exit(std::uintptr_t native_handle) { if (killpg((pid_t) native_handle, SIGTERM) == 0 || errno == ESRCH) { diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index a4190e01..657807fa 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -1313,6 +1313,16 @@ namespace platf { lifetime::exit_sunshine(0, true); } + int + set_env(const std::string &name, const std::string &value) { + return _putenv_s(name.c_str(), value.c_str()); + } + + int + unset_env(const std::string &name) { + return _putenv_s(name.c_str(), ""); + } + struct enum_wnd_context_t { std::set process_ids; bool requested_exit; diff --git a/tests/unit/platform/test_common.cpp b/tests/unit/platform/test_common.cpp new file mode 100644 index 00000000..6f1c9bec --- /dev/null +++ b/tests/unit/platform/test_common.cpp @@ -0,0 +1,49 @@ +/** + * @file tests/unit/platform/test_common.cpp + * @brief Test src/platform/common.*. + */ +#include + +#include "../../tests_common.h" + +struct SetEnvTest: ::testing::TestWithParam> { +protected: + void + TearDown() override { + // Clean up environment variable after each test + const auto &[name, value, expected] = GetParam(); + platf::unset_env(name); + } +}; + +TEST_P(SetEnvTest, SetEnvironmentVariableTests) { + const auto &[name, value, expected] = GetParam(); + platf::set_env(name, value); + + const char *env_value = std::getenv(name.c_str()); + if (expected == 0 && !value.empty()) { + ASSERT_NE(env_value, nullptr); + ASSERT_EQ(std::string(env_value), value); + } + else { + ASSERT_EQ(env_value, nullptr); + } +} + +TEST_P(SetEnvTest, UnsetEnvironmentVariableTests) { + const auto &[name, value, expected] = GetParam(); + platf::unset_env(name); + + const char *env_value = std::getenv(name.c_str()); + if (expected == 0) { + ASSERT_EQ(env_value, nullptr); + } +} + +INSTANTIATE_TEST_SUITE_P( + SetEnvTests, + SetEnvTest, + ::testing::Values( + std::make_tuple("SUNSHINE_UNIT_TEST_ENV_VAR", "test_value_0", 0), + std::make_tuple("SUNSHINE_UNIT_TEST_ENV_VAR", "test_value_1", 0), + std::make_tuple("", "test_value", -1))); From e15fd551b7e8ccdab2bbf46887268ea02767a980 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sun, 25 Aug 2024 10:58:23 -0400 Subject: [PATCH 4/9] docs(readme): update backage url (#3091) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a0f1b931..c99785ab 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![GitHub stars](https://img.shields.io/github/stars/lizardbyte/sunshine.svg?logo=github&style=for-the-badge)](https://github.com/LizardByte/Sunshine) [![GitHub Releases](https://img.shields.io/github/downloads/lizardbyte/sunshine/total.svg?style=for-the-badge&logo=github)](https://github.com/LizardByte/Sunshine/releases/latest) [![Docker](https://img.shields.io/docker/pulls/lizardbyte/sunshine.svg?style=for-the-badge&logo=docker)](https://hub.docker.com/r/lizardbyte/sunshine) -[![GHCR](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fipitio%2Fbackage%2Fmaster%2Findex%2FLizardByte%2FSunshine%2Fsunshine.json&query=%24.downloads&label=ghcr%20pulls&style=for-the-badge&logo=github)](https://github.com/LizardByte/Sunshine/pkgs/container/sunshine) +[![GHCR](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fipitio.github.io%2Fbackage%2FLizardByte%2FSunshine%2Fsunshine.json&query=%24.downloads&label=ghcr%20pulls&style=for-the-badge&logo=github)](https://github.com/LizardByte/Sunshine/pkgs/container/sunshine) [![Winget Version](https://img.shields.io/badge/dynamic/json.svg?color=orange&label=Winget&style=for-the-badge&prefix=v&query=$[-1:].name&url=https%3A%2F%2Fapi.github.com%2Frepos%2Fmicrosoft%2Fwinget-pkgs%2Fcontents%2Fmanifests%2Fl%2FLizardByte%2FSunshine&logo=microsoft)](https://github.com/microsoft/winget-pkgs/tree/master/manifests/l/LizardByte/Sunshine) [![GitHub Workflow Status (CI)](https://img.shields.io/github/actions/workflow/status/lizardbyte/sunshine/CI.yml.svg?branch=master&label=CI%20build&logo=github&style=for-the-badge)](https://github.com/LizardByte/Sunshine/actions/workflows/CI.yml?query=branch%3Amaster) [![GitHub Workflow Status (localize)](https://img.shields.io/github/actions/workflow/status/lizardbyte/sunshine/localize.yml.svg?branch=master&label=localize%20build&logo=github&style=for-the-badge)](https://github.com/LizardByte/Sunshine/actions/workflows/localize.yml?query=branch%3Amaster) From f048510ef701ca6e693fe3ea433aaf7bf7a058ae Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 25 Aug 2024 16:52:48 -0500 Subject: [PATCH 5/9] fix(nvhttp): wrap TLS socket to ensure graceful closure (#3077) --- src/nvhttp.cpp | 59 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 8ac56797..8b610a4c 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -44,19 +44,38 @@ namespace nvhttp { crypto::cert_chain_t cert_chain; - class SunshineHttpsServer: public SimpleWeb::Server { + class SunshineHTTPS: public SimpleWeb::HTTPS { public: - SunshineHttpsServer(const std::string &certification_file, const std::string &private_key_file): - SimpleWeb::Server::Server(certification_file, private_key_file) {} + SunshineHTTPS(boost::asio::io_service &io_service, boost::asio::ssl::context &ctx): + SimpleWeb::HTTPS(io_service, ctx) {} + + virtual ~SunshineHTTPS() { + // Gracefully shutdown the TLS connection + SimpleWeb::error_code ec; + shutdown(ec); + } + }; + + class SunshineHTTPSServer: public SimpleWeb::ServerBase { + public: + SunshineHTTPSServer(const std::string &certification_file, const std::string &private_key_file): + ServerBase::ServerBase(443), + context(boost::asio::ssl::context::tls_server) { + // Disabling TLS 1.0 and 1.1 (see RFC 8996) + context.set_options(boost::asio::ssl::context::no_tlsv1); + context.set_options(boost::asio::ssl::context::no_tlsv1_1); + context.use_certificate_chain_file(certification_file); + context.use_private_key_file(private_key_file, boost::asio::ssl::context::pem); + } std::function verify; std::function, std::shared_ptr)> on_verify_failed; protected: + boost::asio::ssl::context context; + void after_bind() override { - SimpleWeb::Server::after_bind(); - if (verify) { context.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once); context.set_verify_callback([](int verified, boost::asio::ssl::verify_context &ctx) { @@ -108,7 +127,7 @@ namespace nvhttp { } }; - using https_server_t = SunshineHttpsServer; + using https_server_t = SunshineHTTPSServer; using http_server_t = SimpleWeb::Server; struct conf_intern_t { @@ -142,7 +161,7 @@ namespace nvhttp { struct { util::Either< std::shared_ptr::Response>, - std::shared_ptr::Response>> + std::shared_ptr::Response>> response; std::string salt; } async_insert_pin; @@ -154,8 +173,8 @@ namespace nvhttp { std::atomic session_id_counter; using args_t = SimpleWeb::CaseInsensitiveMultimap; - using resp_https_t = std::shared_ptr::Response>; - using req_https_t = std::shared_ptr::Request>; + using resp_https_t = std::shared_ptr::Response>; + using req_https_t = std::shared_ptr::Request>; using resp_http_t = std::shared_ptr::Response>; using req_http_t = std::shared_ptr::Request>; @@ -483,7 +502,7 @@ namespace nvhttp { struct tunnel; template <> - struct tunnel { + struct tunnel { static auto constexpr to_string = "HTTPS"sv; }; @@ -671,7 +690,7 @@ namespace nvhttp { print_req(request); int pair_status = 0; - if constexpr (std::is_same_v) { + if constexpr (std::is_same_v) { auto args = request->parse_query_string(); auto clientID = args.find("uniqueid"s); @@ -696,7 +715,7 @@ namespace nvhttp { // Only include the MAC address for requests sent from paired clients over HTTPS. // For HTTP requests, use a placeholder MAC address that Moonlight knows to ignore. - if constexpr (std::is_same_v) { + if constexpr (std::is_same_v) { tree.put("root.mac", platf::get_mac_address(net::addr_to_normalized_string(local_endpoint.address()))); } else { @@ -777,7 +796,7 @@ namespace nvhttp { void applist(resp_https_t response, req_https_t request) { - print_req(request); + print_req(request); pt::ptree tree; @@ -806,7 +825,7 @@ namespace nvhttp { void launch(bool &host_audio, resp_https_t response, req_https_t request) { - print_req(request); + print_req(request); pt::ptree tree; auto g = util::fail_guard([&]() { @@ -899,7 +918,7 @@ namespace nvhttp { void resume(bool &host_audio, resp_https_t response, req_https_t request) { - print_req(request); + print_req(request); pt::ptree tree; auto g = util::fail_guard([&]() { @@ -985,7 +1004,7 @@ namespace nvhttp { void cancel(resp_https_t response, req_https_t request) { - print_req(request); + print_req(request); pt::ptree tree; auto g = util::fail_guard([&]() { @@ -1016,7 +1035,7 @@ namespace nvhttp { void appasset(resp_https_t response, req_https_t request) { - print_req(request); + print_req(request); auto args = request->parse_query_string(); auto app_image = proc::proc.get_app_image(util::from_view(get_arg(args, "appid"))); @@ -1109,9 +1128,9 @@ namespace nvhttp { tree.put("root..status_message"s, "The client is not authorized. Certificate verification failed."s); }; - https_server.default_resource["GET"] = not_found; - https_server.resource["^/serverinfo$"]["GET"] = serverinfo; - https_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair(add_cert, resp, req); }; + https_server.default_resource["GET"] = not_found; + https_server.resource["^/serverinfo$"]["GET"] = serverinfo; + https_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair(add_cert, resp, req); }; https_server.resource["^/applist$"]["GET"] = applist; https_server.resource["^/appasset$"]["GET"] = appasset; https_server.resource["^/launch$"]["GET"] = [&host_audio](auto resp, auto req) { launch(host_audio, resp, req); }; From 88ce5077b04e56a4b8717b67b685b1d47a0d0a00 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 25 Aug 2024 18:20:33 -0500 Subject: [PATCH 6/9] fix(mdns): don't hardcode mDNS instance name (#3084) --- src/network.cpp | 30 ++++++++++++++++++++++++++++++ src/network.h | 8 ++++++++ src/platform/linux/publish.cpp | 3 ++- src/platform/macos/publish.cpp | 3 ++- src/platform/windows/publish.cpp | 6 +++--- tests/unit/test_network.cpp | 26 ++++++++++++++++++++++++++ 6 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 tests/unit/test_network.cpp diff --git a/src/network.cpp b/src/network.cpp index b7b18de8..5d56866c 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -206,4 +206,34 @@ namespace net { return mapped_port; } + + /** + * @brief Returns a string for use as the instance name for mDNS. + * @param hostname The hostname to use for instance name generation. + * @return Hostname-based instance name or "Sunshine" if hostname is invalid. + */ + std::string + mdns_instance_name(const std::string_view &hostname) { + // Start with the unmodified hostname + std::string instancename { hostname.data(), hostname.size() }; + + // Truncate to 63 characters per RFC 6763 section 7.2. + if (instancename.size() > 63) { + instancename.resize(63); + } + + for (auto i = 0; i < instancename.size(); i++) { + // Replace any spaces with dashes + if (instancename[i] == ' ') { + instancename[i] = '-'; + } + else if (!std::isalnum(instancename[i]) && instancename[i] != '-') { + // Stop at the first invalid character + instancename.resize(i); + break; + } + } + + return !instancename.empty() ? instancename : "Sunshine"; + } } // namespace net diff --git a/src/network.h b/src/network.h index ffc0b2d2..dbae81b9 100644 --- a/src/network.h +++ b/src/network.h @@ -105,4 +105,12 @@ namespace net { */ int encryption_mode_for_address(boost::asio::ip::address address); + + /** + * @brief Returns a string for use as the instance name for mDNS. + * @param hostname The hostname to use for instance name generation. + * @return Hostname-based instance name or "Sunshine" if hostname is invalid. + */ + std::string + mdns_instance_name(const std::string_view &hostname); } // namespace net diff --git a/src/platform/linux/publish.cpp b/src/platform/linux/publish.cpp index 29641411..91d49248 100644 --- a/src/platform/linux/publish.cpp +++ b/src/platform/linux/publish.cpp @@ -426,7 +426,8 @@ namespace platf::publish { return nullptr; } - name.reset(avahi::strdup(SERVICE_NAME)); + auto instance_name = net::mdns_instance_name(boost::asio::ip::host_name()); + name.reset(avahi::strdup(instance_name.c_str())); client.reset( avahi::client_new(avahi::simple_poll_get(poll.get()), avahi::ClientFlags(0), client_callback, nullptr, &avhi_error)); diff --git a/src/platform/macos/publish.cpp b/src/platform/macos/publish.cpp index ec4f1f45..b8c977c0 100644 --- a/src/platform/macos/publish.cpp +++ b/src/platform/macos/publish.cpp @@ -105,7 +105,8 @@ namespace platf::publish { &serviceRef, 0, // flags 0, // interfaceIndex - SERVICE_NAME, SERVICE_TYPE, + nullptr, // name + SERVICE_TYPE, nullptr, // domain nullptr, // host htons(net::map_port(nvhttp::PORT_HTTP)), diff --git a/src/platform/windows/publish.cpp b/src/platform/windows/publish.cpp index fe3352e2..05208a9c 100644 --- a/src/platform/windows/publish.cpp +++ b/src/platform/windows/publish.cpp @@ -37,7 +37,6 @@ constexpr auto DNS_QUERY_RESULTS_VERSION1 = 0x1; #define SERVICE_DOMAIN "local" -constexpr auto SERVICE_INSTANCE_NAME = SV(SERVICE_NAME "." SERVICE_TYPE "." SERVICE_DOMAIN); constexpr auto SERVICE_TYPE_DOMAIN = SV(SERVICE_TYPE "." SERVICE_DOMAIN); #ifndef __MINGW32__ @@ -107,10 +106,11 @@ namespace platf::publish { service(bool enable, PDNS_SERVICE_INSTANCE &existing_instance) { auto alarm = safe::make_alarm(); - std::wstring name { SERVICE_INSTANCE_NAME.data(), SERVICE_INSTANCE_NAME.size() }; std::wstring domain { SERVICE_TYPE_DOMAIN.data(), SERVICE_TYPE_DOMAIN.size() }; - auto host = from_utf8(boost::asio::ip::host_name() + ".local"); + auto hostname = boost::asio::ip::host_name(); + auto name = from_utf8(net::mdns_instance_name(hostname) + '.') + domain; + auto host = from_utf8(hostname + ".local"); DNS_SERVICE_INSTANCE instance {}; instance.pszInstanceName = name.data(); diff --git a/tests/unit/test_network.cpp b/tests/unit/test_network.cpp new file mode 100644 index 00000000..fc8384ad --- /dev/null +++ b/tests/unit/test_network.cpp @@ -0,0 +1,26 @@ +/** + * @file tests/unit/test_network.cpp + * @brief Test src/network.* + */ +#include + +#include "../tests_common.h" + +struct MdnsInstanceNameTest: testing::TestWithParam> {}; + +TEST_P(MdnsInstanceNameTest, Run) { + auto [input, expected] = GetParam(); + ASSERT_EQ(net::mdns_instance_name(input), expected); +} + +INSTANTIATE_TEST_SUITE_P( + MdnsInstanceNameTests, + MdnsInstanceNameTest, + testing::Values( + std::make_tuple("shortname-123", "shortname-123"), + std::make_tuple("space 123", "space-123"), + std::make_tuple("hostname.domain.test", "hostname"), + std::make_tuple("&", "Sunshine"), + std::make_tuple("", "Sunshine"), + std::make_tuple("😁", "Sunshine"), + std::make_tuple(std::string(128, 'a'), std::string(63, 'a')))); From 45265fb10387d329dc813a0dd7778c5998b787cd Mon Sep 17 00:00:00 2001 From: Vithorio Polten Date: Mon, 26 Aug 2024 10:27:23 -0300 Subject: [PATCH 7/9] fix(macos/linux): import boost headers normally (#3096) --- src/platform/common.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/platform/common.h b/src/platform/common.h index bd594ba1..4104d4ea 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -11,6 +11,10 @@ #include #include +#ifndef _WIN32 + #include + #include +#endif #include "src/config.h" #include "src/logging.h" @@ -31,6 +35,7 @@ struct AVHWFramesContext; struct AVCodecContext; struct AVDictionary; +#ifdef _WIN32 // Forward declarations of boost classes to avoid having to include boost headers // here, which results in issues with Windows.h and WinSock2.h include order. namespace boost { @@ -50,6 +55,7 @@ namespace boost { typedef basic_environment environment; } // namespace process } // namespace boost +#endif namespace video { struct config_t; } // namespace video From 3976b63ee8ca036a14c4c67ece250d4bf9b099d0 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 26 Aug 2024 09:41:17 -0500 Subject: [PATCH 8/9] fix(win/input): fix false warnings about missing ViGEmBus (#3097) --- src/platform/common.h | 2 ++ src/platform/windows/input.cpp | 17 ++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/platform/common.h b/src/platform/common.h index 4104d4ea..5c319dce 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -860,6 +860,8 @@ namespace platf { /** * @brief Gets the supported gamepads for this platform backend. + * @details This may be called prior to `platf::input()`! + * @param input Pointer to the platform's `input_t` or `nullptr`. * @return Vector of gamepad options and status. */ std::vector & diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp index a252544b..5ff61689 100644 --- a/src/platform/windows/input.cpp +++ b/src/platform/windows/input.cpp @@ -1728,15 +1728,18 @@ namespace platf { std::vector & supported_gamepads(input_t *input) { - bool enabled; - if (input) { - auto vigem = ((input_raw_t *) input)->vigem; - enabled = vigem != nullptr; - } - else { - enabled = false; + if (!input) { + static std::vector gps { + supported_gamepad_t { "auto", true, "" }, + supported_gamepad_t { "x360", false, "" }, + supported_gamepad_t { "ds4", false, "" }, + }; + + return gps; } + auto vigem = ((input_raw_t *) input)->vigem; + auto enabled = vigem != nullptr; auto reason = enabled ? "" : "gamepads.vigem-not-available"; // ds4 == ps4 From 34054a20e1143a814f65cb698ed80d1f5b71c142 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Mon, 26 Aug 2024 20:42:34 -0400 Subject: [PATCH 9/9] chore(l10n): update translations (#3082) --- .../assets/web/public/assets/locale/es.json | 2 +- .../assets/web/public/assets/locale/ru.json | 292 +++++++++--------- 2 files changed, 147 insertions(+), 147 deletions(-) diff --git a/src_assets/common/assets/web/public/assets/locale/es.json b/src_assets/common/assets/web/public/assets/locale/es.json index 80e535b4..5cb2e10b 100644 --- a/src_assets/common/assets/web/public/assets/locale/es.json +++ b/src_assets/common/assets/web/public/assets/locale/es.json @@ -20,7 +20,7 @@ "see_more": "Ver más", "success": "¡Éxito!", "undo_cmd": "Deshacer comando", - "username": "Usuario", + "username": "Nombre de usuario", "warning": "¡Advertencia!" }, "apps": { diff --git a/src_assets/common/assets/web/public/assets/locale/ru.json b/src_assets/common/assets/web/public/assets/locale/ru.json index a9fd75e3..b23a3fa7 100644 --- a/src_assets/common/assets/web/public/assets/locale/ru.json +++ b/src_assets/common/assets/web/public/assets/locale/ru.json @@ -8,81 +8,81 @@ "disabled": "Отключено", "disabled_def": "Отключено (по умолчанию)", "dismiss": "Отклонить", - "do_cmd": "Выполнить команду", - "elevated": "Повышенный", + "do_cmd": "Команда запуска", + "elevated": "Требуются", "enabled": "Включено", "enabled_def": "Включено (по умолчанию)", "error": "Ошибка!", "note": "Примечание:", "password": "Пароль", - "run_as": "Запустить как Администратор", + "run_as": "Права администратора", "save": "Сохранить", "see_more": "Подробнее", "success": "Успех!", - "undo_cmd": "Отменить команду", + "undo_cmd": "Команда закрытия", "username": "Имя пользователя", "warning": "Предупреждение!" }, "apps": { "actions": "Действия", "add_cmds": "Добавить команды", - "add_new": "Добавить новый", + "add_new": "Добавить новое", "app_name": "Название приложения", - "app_name_desc": "Имя приложения, как показано в Moonlight", - "applications_desc": "Приложения обновляются только при перезапуске клиента", + "app_name_desc": "Имя приложения, для показа в Moonlight", + "applications_desc": "Приложения обновятся только после перезапуска клиента", "applications_title": "Приложения", "auto_detach": "Продолжить трансляцию, если приложение завершит работу быстро", - "auto_detach_desc": "Это попытается автоматически обнаружить приложения лаунчера, которые быстро закрываются после запуска другой программы или экземпляра самостоятельно. Когда приложение запускается тип, оно рассматривается как отдельное приложение.", + "auto_detach_desc": "Пытаться автоматически обнаружить приложения-лаунчеры, которые быстро закрываются после запуска другой программы или собственной копии. Когда такое приложение обнаружено, оно рассматривается как независимое.", "cmd": "Команда", "cmd_desc": "Основное приложение для запуска. Если пустое, то не будет запущено приложение.", "cmd_note": "Если путь к исполняемому файлу содержит пробелы, вы должны заключить его в кавычки.", - "cmd_prep_desc": "Список команд, которые должны быть выполнены до/после этого приложения. Если одна из команд не удается, запуск приложения прерван.", - "cmd_prep_name": "Подготовка команд", - "covers_found": "Найдено обложек", + "cmd_prep_desc": "Список команд, которые должны быть выполнены до/после этого приложения. Если одна из команд не выполнена, запуск приложения прерывается.", + "cmd_prep_name": "Команды подготовки", + "covers_found": "Найденные обложки", "delete": "Удалить", - "detached_cmds": "Отдельные команды", - "detached_cmds_add": "Добавить отдельную команду", - "detached_cmds_desc": "Список команд для работы в фоновом режиме.", - "detached_cmds_note": "Если путь к исполняемому файлу содержит пробелы, вы должны заключить его в кавычки.", - "edit": "Редактирование", + "detached_cmds": "Независимые команды", + "detached_cmds_add": "Добавить независимую команду", + "detached_cmds_desc": "Список команд, работающих в фоновом режиме.", + "detached_cmds_note": "Если путь к исполняемому файлу содержит пробелы, следует заключить его в кавычки.", + "edit": "Изменить", "env_app_id": "ID приложения", "env_app_name": "Название приложения", "env_client_audio_config": "Запрошенная клиентом конфигурация аудио (2.0/5.1/7.1)", - "env_client_enable_sops": "Клиент запросил опцию оптимизации игры для оптимального потокового вещания (true/false)", - "env_client_fps": "FPS запрошен клиентом (int)", - "env_client_gcmap": "Запрашиваемая маска геймпада, в bitset/bitfield формате (int)", - "env_client_hdr": "HDR включен клиентом (true/false)", - "env_client_height": "Высота, запрошенная клиентом (int)", - "env_client_host_audio": "Клиент запросил звук узла (true/false)", - "env_client_width": "Ширина запрошенная клиентом (int)", + "env_client_enable_sops": "Клиент запросил оптимизацию настроек игры для потокового вещания (истина/ложь)", + "env_client_fps": "Частота кадров, запрошенная клиентом (целое)", + "env_client_gcmap": "Запрашиваемая маска геймпада, в формате bitset/bitfield (целое)", + "env_client_hdr": "HDR включен клиентом (истина/ложь)", + "env_client_height": "Высота, запрошенная клиентом (целое)", + "env_client_host_audio": "Клиент запросил звук с сервера (истина/ложь)", + "env_client_width": "Ширина, запрошенная клиентом (целое)", "env_displayplacer_example": "Пример - displayplacer для автоматизации решения:", - "env_qres_example": "Пример - QRes для автоматизации разрешения:", + "env_qres_example": "Пример: автопереключение разрешения через QRes:", "env_qres_path": "путь qres", - "env_var_name": "Имя Вара", + "env_var_name": "Переменная окружения", "env_vars_about": "О переменных среды", - "env_vars_desc": "Все команды получают эти переменные окружения по умолчанию:", + "env_vars_desc": "Всем командам по умолчанию передаются эти переменные окружения:", "env_xrandr_example": "Пример - Xrandr для автоматизации решения:", - "exit_timeout": "Таймаут выхода", - "exit_timeout_desc": "Количество секунд, в течение которых все процессы приложения смогут легко выйти из приложения. Если не установлено, то приложение будет немедленно прекращено до 5 секунд. Если установлено значение нуля или отрицательное значение, приложение будет немедленно прекращено.", + "exit_timeout": "Ожидание завершения", + "exit_timeout_desc": "Сколько секунд ожидать корректного завершения всех процессов приложения при закрытии. Если не указано, то по умолчанию, ожидание длится 5 секунд. Если указан нуль или отрицательное значение, приложение будет прекращено незамедлительно.", "find_cover": "Найти обложку", "global_prep_desc": "Включить/отключить исполнение глобальных команд подготовки для этого приложения.", "global_prep_name": "Глобальные команды", "image": "Изображение", - "image_desc": "Значок приложения/путь изображения/изображения, который будет отправлен клиенту. Изображение должно быть PNG файлом. Если не установлено, Sunshine будет отправлять изображение по умолчанию.", + "image_desc": "Путь к иконке/обложке/изображению, который будет отправлен клиенту. Изображение должно быть PNG файлом. Если не указано, Sunshine пошлёт обложку по умолчанию.", "loading": "Загрузка...", - "name": "Наименование", + "name": "Название", "output_desc": "Файл, в котором сохраняется вывод команды, если он не указан, вывод игнорируется", "output_name": "Вывод", "run_as_desc": "Это может потребоваться для некоторых приложений, которым требуются права администратора для правильного запуска.", - "wait_all": "Продолжить вещание, пока не завершится все процессы приложения", - "wait_all_desc": "Это будет продолжаться, пока все процессы не будут запущены приложением. Если флажок не установлен, стриминг будет остановлен, когда начальный процесс завершится, даже если запущены другие процессы.", + "wait_all": "Продолжать вещание, пока не завершатся все процессы приложения", + "wait_all_desc": "Продолжать вещание, пока все процессы, запущенные приложением не будут завершены. Если не выбрано, вещание прекратится, по завершении начального процесса приложения, даже если запущены другие подпроцессы.", "working_dir": "Рабочая папка", - "working_dir_desc": "Рабочий каталог, передаваемый процессу. Например, некоторые приложения используют рабочий каталог для поиска конфигурационных файлов. Если не установлено, Sunshine будет по умолчанию родительский каталог команды" + "working_dir_desc": "Рабочий каталог, передаваемый процессу. К примеру, некоторые приложения используют рабочий каталог для поиска конфигурационных файлов. Если не указан, Sunshine по умолчанию использует вышестоящий каталог команды" }, "config": { "adapter_name": "Имя адаптера", "adapter_name_desc_linux_1": "Вручную укажите GPU для захвата.", - "adapter_name_desc_linux_2": "найти все устройства, способные к VAAPI", + "adapter_name_desc_linux_2": "найти все устройства, поддерживающие VAAPI", "adapter_name_desc_linux_3": "Замените ``renderD129`` устройством сверху, чтобы перечислить имя и возможности устройства. Чтобы быть поддержанным Sunshine, он должен иметь как минимум свое:", "adapter_name_desc_windows": "Укажите GPU для захвата. Если флажок установлен, GPU выбирается автоматически. Мы настоятельно рекомендуем оставить это поле пустым, чтобы использовать автоматический выбор GPU! Примечание: этот GPU должен иметь дисплей подключён и включен. Соответствующие значения могут быть найдены с помощью следующей команды:", "adapter_name_placeholder_windows": "Radeon RX 580 серия", @@ -91,8 +91,8 @@ "address_family_both": "IPv4 + IPv6", "address_family_desc": "Установить семейство адресов, используемое Sunshine", "address_family_ipv4": "Только IPv4", - "always_send_scancodes": "Всегда отправлять коды", - "always_send_scancodes_desc": "Отправка scancodes улучшает совместимость с играми и приложениями, но может привести к неправильному вводу клавиатуры от некоторых клиентов, которые не используют американскую клавиатуру. Включите, если ввод клавиатуры вообще не работает в некоторых приложениях. Отключено, если ключи на клиенте генерируют неправильный входной параметр на хосте.", + "always_send_scancodes": "Всегда посылать коды клавиш", + "always_send_scancodes_desc": "Передача кодов клавиш улучшает совместимость с играми и приложениями, но может привести к неправильному вводу с клавиатуры, если клиент используют раскладку, отличную от английской США. Включите, если в каких-то приложениях ввод с клавиатуры не работает вовсе. Отключите, если клавиши клиента передают на ввод не те клавиши сервер.", "amd_coder": "AMF Coder (H264)", "amd_coder_desc": "Позволяет выбрать энтропическую кодировку для приоритизации скорости или качества кодирования. H.264 только.", "amd_enforce_hrd": "AMF Hypothetical Reference Decoder (HRD) Enforcement", @@ -101,10 +101,10 @@ "amd_preanalysis_desc": "Это позволяет проводить предварительный анализ скорости управления, который может повысить качество за счет увеличения задержки кодирования.", "amd_quality": "Качество AMF", "amd_quality_balanced": "сбалансированный -- сбалансированный (по умолчанию)", - "amd_quality_desc": "Это определяет баланс между скоростью кодирования и качеством.", + "amd_quality_desc": "Задаёт соотношение между скоростью и качеством кодирования.", "amd_quality_group": "Настройки качества AMF", - "amd_quality_quality": "качество -- предпочитаемое качество", - "amd_quality_speed": "скорость - скорость отдачи", + "amd_quality_quality": "качество -- упор на качество", + "amd_quality_speed": "скорость -- упор на скорость", "amd_rc": "Контроль скорости AMF", "amd_rc_cbr": "cbr -- постоянный битрейт", "amd_rc_cqp": "cqp -- постоянный режим qp", @@ -123,7 +123,7 @@ "amd_vbaq_desc": "Как правило, визуальная система человека менее чувствительна к артефактам в особо текстурированных районах. В режиме VBAQ отклонение пикселей используется для обозначения сложности пространственной текстуры, что позволяет кодировщику выделять больше битов для более плавности зон. Включение этой функции приводит к улучшению субъективного качества зрения с некоторым содержимым.", "apply_note": "Нажмите 'Применить', чтобы перезапустить Sunshine и применить изменения. Все запущенные сессии будут завершены.", "audio_sink": "Снимок звука", - "audio_sink_desc_linux": "Название звуковой сигнала, используемой для аудиоциклов. Если эта переменная не указана, пульс будет выбирать стандартное устройство монитора. Вы можете найти название звуковой раковины, используя либо команду:", + "audio_sink_desc_linux": "Название звукового приёмника, используемого для обратной ретрансляции. Если эта переменная не указана, pulseaudio выберет устройство по умолчанию. Определить название звукового приёмника можно либо командой:", "audio_sink_desc_macos": "Название звуковой раковины, используемой для аудиоциклов. Sunshine может получить доступ только к микрофонам в macOS из-за ограничений системы. Для трансляции системного аудио с помощью Soundflower или BlackHole.", "audio_sink_desc_windows": "Укажите вручную определённое аудиоустройство для записи. Если устройство выключено, оно выбирается автоматически. Рекомендуем оставить это поле пустым, чтобы использовать автоматический выбор устройств! Если у вас несколько аудио устройств с одинаковыми именами, вы можете получить ID устройства, используя следующую команду:", "audio_sink_placeholder_macos": "BlackHole 2ch", @@ -139,7 +139,7 @@ "capture": "Принудительный метод захвата", "capture_desc": "В автоматическом режиме Sunshine будет использовать первый работающий драйвер NvFBC.", "cert": "Сертификат", - "cert_desc": "Сертификат, используемый для пары клиентов веб-интерфейса и Moonlight, для лучшей совместимости должен иметь публичный ключ RSA-2048.", + "cert_desc": "Сертификат, используемый для веб-интерфейса и привязки клиентов Moonlight. Для совместимости должен иметь открытый ключ RSA-2048.", "channels": "Максимальное число подключенных клиентов", "channels_desc_1": "Sunshine позволяет одновременное совместное использование одного сеанса потокового вещания.", "channels_desc_2": "Некоторые аппаратные кодировщики могут иметь ограничения, уменьшающие производительность с несколькими потоками.", @@ -153,7 +153,7 @@ "ds4_back_as_touchpad_click": "Назад/Выберете для нажатия сенсорной панели", "ds4_back_as_touchpad_click_desc": "При принудительной эмуляции DS4, нажмите на карточку Назад/Выделение для сенсорной панели", "encoder": "Принудительный кодировщик", - "encoder_desc": "Принудительно использовать специальный кодировщик, в противном случае Sunshine будет выбирать наилучший доступный вариант. Примечание: Если вы укажете аппаратный кодировщик в Windows, он должен соответствовать GPU, где установлен дисплей.", + "encoder_desc": "Принудительно использовать конкретный кодировщик, иначе Sunshine выберет наилучший доступный вариант. Примечание: Если указать аппаратный кодировщик в Windows, он должен соответствовать GPU, к которому подключён экран.", "encoder_software": "Программный", "external_ip": "Внешний IP", "external_ip_desc": "Если внешний IP адрес не указан, Sunshine будет автоматически определять внешний IP", @@ -162,10 +162,10 @@ "ffmpeg_auto": "auto -- пусть ffmpeg решить (по умолчанию)", "file_apps": "Файл приложений", "file_apps_desc": "Файл, в котором хранятся текущие приложения Sunshine.", - "file_state": "Файл штата", + "file_state": "Файл состояния", "file_state_desc": "Файл, в котором хранится текущее состояние Sunshine", - "gamepad": "Эмулированный тип геймпада", - "gamepad_auto": "Автоматические настройки выбора", + "gamepad": "Тип эмулируемого геймпада", + "gamepad_auto": "Настройка автоматического выбора", "gamepad_desc": "Выберите тип геймпада для эмулирования на хосте", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds5": "DS5 (PS5)", @@ -173,7 +173,7 @@ "gamepad_manual": "Ручные настройки DS4", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", - "global_prep_cmd": "Подготовка команд", + "global_prep_cmd": "Команды подготовки", "global_prep_cmd_desc": "Настроить список команд, которые будут выполнены до или после запуска любого приложения. Если какая-либо из указанных команд подготовки не удается, процесс запуска приложения будет прерван.", "hevc_mode": "Поддержка HEVC", "hevc_mode_0": "Sunshine будет рекламировать поддержку HEVC на основе возможностей кодировщика (рекомендуется)", @@ -184,31 +184,31 @@ "high_resolution_scrolling": "Поддержка прокрутки высокого разрешения", "high_resolution_scrolling_desc": "Когда включено, Sunshine будет прокручивать события с высоким разрешением от клиентов лунного света. Это может быть полезно для отключения для старых приложений, которые слишком быстро прокручивать при прокрутке событий высокого разрешения.", "install_steam_audio_drivers": "Установить Steam Audio Drivers", - "install_steam_audio_drivers_desc": "Если Steam установлен, он автоматически установит драйвер Steam Streaming Speakers для поддержки 5.1/7.1 объемного звука и отключения хоста.", - "key_repeat_delay": "Задержка повтора ключа", - "key_repeat_delay_desc": "Контролируйте как быстрые клавиши будут повторяться. Первоначальная задержка в миллисекундах перед повтором ключей.", - "key_repeat_frequency": "Частота повторения ключа", - "key_repeat_frequency_desc": "Как часто ключи повторяют каждую секунду. Эта настраиваемая опция поддерживает десятичные дроби.", - "key_rightalt_to_key_windows": "Карта клавиши Alt справа для клавиши Windows", - "key_rightalt_to_key_win_desc": "Возможно, вы не можете отправить ключ Windows непосредственно с лунного света. В этих случаях было бы полезно заставить Солнечный свет думать, что ключ правой Alt является клавишей Windows", - "keyboard": "Включить ввод клавиатуры", + "install_steam_audio_drivers_desc": "Если Steam установлен, он автоматически установит драйвер Steam Streaming Speakers для поддержки объёмного звука 5.1/7.1 и заглушения звука на сервере.", + "key_repeat_delay": "Задержка повтора нажатий", + "key_repeat_delay_desc": "Контролируйте как быстрые клавиши будут повторяться. Начальная задержка в миллисекундах до повторных нажатий.", + "key_repeat_frequency": "Частота повторения нажатий", + "key_repeat_frequency_desc": "Как часто нажатия повторяются за секунду. Эта настройка поддерживает десятичные дроби.", + "key_rightalt_to_key_windows": "Переопределить клавишу правый Alt как клавишу Windows", + "key_rightalt_to_key_win_desc": "Возможно, вы не можете послать нажатие кнопки Windows непосредственно из Moonlight. В таком случае, полезно чтобы Sunshine думал, что клавиша правый Alt является клавишей Windows", + "keyboard": "Включить ввод с клавиатуры", "keyboard_desc": "Позволяет гостям управлять системой хоста с помощью клавиатуры", "lan_encryption_mode": "Режим шифрования LAN", "lan_encryption_mode_1": "Включено для поддерживаемых клиентов", "lan_encryption_mode_2": "Требуется для всех клиентов", - "lan_encryption_mode_desc": "Определяет, когда шифрование будет использоваться при потоке по локальной сети. Шифрование может снизить производительность потокового вещания, особенно на менее мощных узлах и клиентах.", + "lan_encryption_mode_desc": "Определяет, когда шифрование будет использоваться при вещании в локальной сети. Шифрование может снизить качество вещания, особенно на более слабых серверах и клиентах.", "locale": "Язык", "locale_desc": "Локализация, используемая для пользовательского интерфейса Sunshine.", - "log_level": "Уровень журнала", - "log_level_0": "Verbose", - "log_level_1": "Debug", - "log_level_2": "Инфо", - "log_level_3": "Предупреждение", - "log_level_4": "Ошибка", - "log_level_5": "Fatal", + "log_level": "Уровень журналирования", + "log_level_0": "Подробные", + "log_level_1": "Отладочные", + "log_level_2": "Информация", + "log_level_3": "Предупреждения", + "log_level_4": "Ошибки", + "log_level_5": "Критические", "log_level_6": "Нет", - "log_level_desc": "Минимальный уровень журнала, напечатанный по стандарту", - "log_path": "Путь к лог-файлу", + "log_level_desc": "Минимальный уровень журнала, направляемый в стандартный вывод", + "log_path": "Путь к файлу журнала", "log_path_desc": "Файл, в котором хранятся текущие журналы Sunshine.", "min_fps_factor": "Минимальный коэффициент FPS", "min_fps_factor_desc": "Солнечный свет будет использовать этот фактор для расчета минимального времени между рамками. Увеличение этого значения может помочь при потоке в основном статического контента. Более высокие значения будут потреблять больше пропускной способности.", @@ -229,7 +229,7 @@ "nvenc_latency_over_power_desc": "Sunshine requests maximum GPU clock speed while streaming to reduce encoding latency. Отключение этого значения не рекомендуется, так как это может привести к значительному увеличению задержки кодирования.", "nvenc_opengl_vulkan_on_dxgi": "Настоящий OpenGL/Vulkan на вершине DXGI", "nvenc_opengl_vulkan_on_dxgi_desc": "Sunshine не может захватывать полноэкранные программы OpenGL и Vulkan с полной скоростью кадра, если они не присутствуют поверх DXGI. Это общесистемная настройка, которая возвращается при выходе из программы.", - "nvenc_preset": "Пресет производительности", + "nvenc_preset": "Преднастройка производительности", "nvenc_preset_1": "(быстрый, по умолчанию)", "nvenc_preset_7": "(медленно)", "nvenc_preset_desc": "Более высокие значения улучшают сжатие (качество на заданном битрейте) за счет увеличения задержки кодирования. Рекомендуется изменять только когда ограничено сетью или декодером, в противном случае подобный эффект может быть достигнут путем увеличения битрейта.", @@ -246,109 +246,109 @@ "nvenc_twopass_quarter_res": "Квартальное разрешение (по умолчанию)", "nvenc_vbv_increase": "Однокадровое увеличение VBV/HRD", "nvenc_vbv_increase_desc": "По умолчанию солнечный свет использует однокадровый VBV/HRD, что означает, что любой кодируемый размер видеокадра не должен превышать запрашиваемый битрейт, поделенный на заданную скорость кадра. Расслабляя это ограничение может быть полезным и выступать в качестве битрейта с низкой задержкой, но может также привести к потере пакетов, если в сети нет заголовка буфера для обработки битрейтов. Максимально допустимое значение - 400, что соответствует 5-кратному увеличенному пределу видеокадра в кодировке.", - "origin_web_ui_allowed": "Оригинальный Web UI разрешён", - "origin_web_ui_allowed_desc": "Источник адреса удаленной конечной точки, который не запрещает доступ к Web UI", - "origin_web_ui_allowed_lan": "Только в LAN могут получить доступ к Web UI", - "origin_web_ui_allowed_pc": "Только локальный хост может получить доступ к Web UI", - "origin_web_ui_allowed_wan": "Любой может получить доступ к веб-интерфейсу", - "output_name_desc_unix": "Во время запуска Sunshine вы увидите список обнаруженных дисплей. Примечание: используйте id значение внутри скобки.", - "output_name_desc_windows": "Вручную укажите дисплей для захвата. Если не установлено, то будет произведен захват основного экрана. Примечание: Если вы указали GPU выше, этот экран должен быть подключен к этому GPU. Соответствующие значения могут быть найдены с помощью следующей команды:", - "output_name_unix": "Отобразить номер", + "origin_web_ui_allowed": "Веб-интерфейс Origin разрешён", + "origin_web_ui_allowed_desc": "Источник адреса удаленной конечной точки, которой не запрещён доступ к веб-интерфейсу", + "origin_web_ui_allowed_lan": "Только из LAN могут получить доступ к веб-интерфейсу", + "origin_web_ui_allowed_pc": "Только локальный ПК имеет доступ к веб-интерфейсу", + "origin_web_ui_allowed_wan": "Любой имеет доступ к веб-интерфейсу", + "output_name_desc_unix": "Во время запуска Sunshine вы увидите список обнаруженных экранов. Примечание: используйте значение id в скобках. Пример ниже; нужный экран можно обнаружить на вкладке Устранение проблем.", + "output_name_desc_windows": "Вручную укажите экран для захвата. Если не указано, то будет произведён захват основного экрана. Примечание: Если вы ранее указали GPU, этот экран должен быть подключен к тому GPU. Подходящие значения определяются с помощью следующей команды:", + "output_name_unix": "Номер экрана", "output_name_windows": "Имя вывода", "ping_timeout": "Таймаут пинга", - "ping_timeout_desc": "Как долго ждать в миллисекундах данных с лунного света перед выключением потока", - "pkey": "Приватный ключ", - "pkey_desc": "Приватный ключ, используемый для пары клиентов Moonlight. Для лучшей совместимости это должен быть приватный ключ RSA-2048.", + "ping_timeout_desc": "Время ожидания данных от Moonlight до завершения вещания, в миллисекундах", + "pkey": "Закрытый ключ", + "pkey_desc": "Закрытый ключ, используемый для веб-интерфейса и привязки клиентов Moonlight. Для совместимости должен быть закрытым ключом RSA-2048.", "port": "Порт", - "port_alert_1": "Солнечный свет не может использовать порты ниже 1024!", + "port_alert_1": "Sunshine не может использовать порты ниже 1024!", "port_alert_2": "Порты выше 65535 недоступны!", "port_desc": "Установить семейство портов, используемых Sunshine", - "port_http_port_note": "Используйте этот порт для соединения с лунным светом.", + "port_http_port_note": "Используйте этот порт для соединения с Moonlight.", "port_note": "Примечание", "port_port": "Порт", - "port_protocol": "Protocol", + "port_protocol": "Протокол", "port_tcp": "TCP", "port_udp": "UDP", - "port_warning": "Размещение веб-интерфейса в Интернете является риском для безопасности! Продолжайте на свой страх и риск!", - "port_web_ui": "Web UI", - "qp": "Параметр количественного измерения", - "qp_desc": "Некоторые устройства могут не поддерживать постоянную ставку бит. Для этих устройств используется QP. Более высокое значение означает больше сжатия, но меньшее качество.", - "qsv_coder": "QuickSync Coder (H264)", - "qsv_preset": "QuickSync Preset", - "qsv_preset_fast": "быстрее (меньшее качество)", - "qsv_preset_faster": "самый быстрый (низкое качество)", - "qsv_preset_medium": "среднее (по умолчанию)", - "qsv_preset_slow": "медленно (хорошее качество)", - "qsv_preset_slower": "медленнее (лучшее качество)", - "qsv_preset_slowest": "самый медленный (лучшее качество)", + "port_warning": "Доступность веб-интерфейса из Интернета не безопасна! Продолжайте на свой страх и риск!", + "port_web_ui": "Веб-интерфейс", + "qp": "Параметр квантования", + "qp_desc": "Некоторые устройства могут не поддерживать постоянную ширину поток. Для таких устройств используется параметр квантования. Более высокое значение означает большее сжатие, но меньшее качество.", + "qsv_coder": "Кодировщик QuickSync (H264)", + "qsv_preset": "Предустановка QuickSync", + "qsv_preset_fast": "fast (низкое качество)", + "qsv_preset_faster": "faster (худшее качество)", + "qsv_preset_medium": "medium (по умолчанию)", + "qsv_preset_slow": "slow (хорошее качество)", + "qsv_preset_slower": "slower (отличное качество)", + "qsv_preset_slowest": "slowest (лучшее качество)", "qsv_preset_veryfast": "самый быстрый (низкое качество)", "qsv_slow_hevc": "Разрешить Slow HEVC кодирование", "qsv_slow_hevc_desc": "Это позволяет включить HEVC кодирование на старых процессорах Intel за счет более высокого использования GPU и более низкой производительности.", "restart_note": "Sunshine перезапускается, чтобы применить изменения.", - "sunshine_name": "Название сервиса Sunshine", - "sunshine_name_desc": "Имя хоста, отображаемое при Moonlight. Если не указано, используется имя хоста", - "sw_preset": "SW пресеты", + "sunshine_name": "Название сервера Sunshine", + "sunshine_name_desc": "Имя сервера, отображаемое в Moonlight. Если не указано, используется имя ПК", + "sw_preset": "Предустановки программного кодирования", "sw_preset_desc": "Оптимизировать компромисс между скоростью кодирования (кодированные кадры в секунду) и эффективностью сжатия (качество за бит в bitstream). По умолчанию супербыстро.", "sw_preset_fast": "быстро", "sw_preset_faster": "быстрее", "sw_preset_medium": "средняя", "sw_preset_slow": "медленно", "sw_preset_slower": "медленнее", - "sw_preset_superfast": "супербыстрый (по умолчанию)", - "sw_preset_ultrafast": "ультразвук", + "sw_preset_superfast": "сверхбыстро (по умолчанию)", + "sw_preset_ultrafast": "ультрабыстро", "sw_preset_veryfast": "veryfast", "sw_preset_veryslow": "veryslow", - "sw_tune": "SW Tune", - "sw_tune_animation": "анимация -- подходит для мультфильмов, использует более высокую блокировку и больше эталонных кадров", - "sw_tune_desc": "Настройка параметров, применяемых после пресета. По умолчанию нулевая настройка.", - "sw_tune_fastdecode": "fastdecode -- позволяет ускорить декодирование, отключив некоторые фильтры", - "sw_tune_film": "фильм -- используется для высококачественного содержания фильма; снижает блокировку", - "sw_tune_grain": "зерно - сохраняет структуру зерна в старых, зернистых пленках", - "sw_tune_stillimage": "stillimage -- подходит для содержимого как слайд-шоу", - "sw_tune_zerolatency": "zerolatency -- подходит для быстрой кодировки и потокового низкозадержки (по умолчанию)", - "touchpad_as_ds4": "Эмулируйте геймпад DS4, если клиент сообщает, что сенсорная панель присутствует", + "sw_tune": "Подстройки Программного кодирования", + "sw_tune_animation": "animation -- подходит для мультфильмов, использует агрессивное подавление блочности и больше опорных кадров", + "sw_tune_desc": "Подстроечные параметры, применяемые после предустановки. По умолчанию zerolatency.", + "sw_tune_fastdecode": "fastdecode -- позволяет ускорить декодирование отключением некоторых фильтров", + "sw_tune_film": "фильм -- используется для высококачественного кино; меньше подавляет блочность", + "sw_tune_grain": "grain -- предаёт зернистость старой фотоплёнки", + "sw_tune_stillimage": "stillimage -- хорош для малоподвижных изображений", + "sw_tune_zerolatency": "zerolatency -- хорош для быстрого кодирования и вещания с низкой задержкой (по умолчанию)", + "touchpad_as_ds4": "Эмулировать геймпад DS4, если клиент сообщает, о наличии сенсорной панели", "touchpad_as_ds4_desc": "Если отключено, присутствие сенсорной панели не будет учитываться при выборе типа геймпада.", "upnp": "UPnP", - "upnp_desc": "Автоматически настраивать переадресацию портов для трансляции через Интернет", - "virtual_sink": "Виртуальный снимок", - "virtual_sink_desc": "Вручную укажите виртуальное аудио устройство. Если установлено, устройство будет выбрано автоматически. Рекомендуем оставить это поле пустым, чтобы использовать автоматический выбор устройств!", + "upnp_desc": "Автоматически настраивать переадресацию портов для вещания через Интернет", + "virtual_sink": "Виртуальный приёмник", + "virtual_sink_desc": "Вручную укажите виртуальное аудио устройство. Если не указано, устройство будет выбрано автоматически. Рекомендуем оставить это поле пустым, для автоматического выбора устройств!", "virtual_sink_placeholder": "Динамики Steam", - "vt_coder": "VideoToolbox Coder", - "vt_realtime": "VideoToolbox в реальном времени кодирования", - "vt_software": "Программное кодирование VideoToolbox", + "vt_coder": "Кодировщик VideoToolbox", + "vt_realtime": "Кодирование в реальном времени через VideoToolbox", + "vt_software": "Программное кодирование через VideoToolbox", "vt_software_allowed": "Разрешено", "vt_software_forced": "Принудительно", "wan_encryption_mode": "Режим шифрования WAN", "wan_encryption_mode_1": "Включено для поддерживаемых клиентов (по умолчанию)", "wan_encryption_mode_2": "Требуется для всех клиентов", - "wan_encryption_mode_desc": "Определяет, когда шифрование будет использоваться при потоке через Интернет. Шифрование может снизить производительность потокового вещания, особенно на менее мощных хостах и клиентах." + "wan_encryption_mode_desc": "Определяет, когда будет использоваться шифрование при вещании через Интернет. Шифрование может снизить качество вещания, особенно на более слабых серверах и клиентах." }, "index": { - "description": "Sunshine - это хост для самостоятельной игры на Лунном свете.", + "description": "Sunshine - это ваш собственный сервер вещания игр для Moonlight.", "download": "Скачать", - "installed_version_not_stable": "Вы используете предварительную версию Sunshine. Вы можете столкнуться с ошибками или другими проблемами. Пожалуйста, сообщайте о проблемах, с которыми вы столкнулись. Благодарим вас за помощь в создании программного обеспечения для Sunshine!", - "loading_latest": "Загрузка последней версии...", + "installed_version_not_stable": "Вы используете предварительную версию Sunshine. Вы можете столкнуться с ошибками или другими проблемами. Пожалуйста, сообщайте о проблемах, с которыми вы столкнётесь. Благодарим за помощь по улучшению Sunshine!", + "loading_latest": "Загрузка свежей версии...", "new_pre_release": "Доступна новая предварительная версия!", "new_stable": "Доступна новая стабильная версия!", - "startup_errors": "Внимание! Sunshine обнаружил эти ошибки во время запуска. Мы НАСТОЯТЕЛЬНО РЕКОМЕНДУЕМ исправить их перед началом трансляции.", + "startup_errors": "Внимание! Sunshine обнаружил эти ошибки во время запуска. Мы НАСТОЯТЕЛЬНО РЕКОМЕНДУЕМ исправить их перед запуском вещания.", "version_dirty": "Спасибо за помощь в создании программного обеспечения Sunshine!", - "version_latest": "Вы используете последнюю версию Sunshine", + "version_latest": "Вы используете свежую версию Sunshine", "welcome": "Привет, Sunshine!" }, "navbar": { "applications": "Приложения", - "configuration": "Конфигурация", + "configuration": "Настройки", "home": "Главная", "password": "Изменить пароль", "pin": "Pin", - "theme_auto": "Авто", - "theme_dark": "Тёмная", - "theme_light": "Светлая", - "toggle_theme": "Тема", + "theme_auto": "Автоматически", + "theme_dark": "Тёмное", + "theme_light": "Светлое", + "toggle_theme": "Оформление", "troubleshoot": "Устранение проблем" }, "password": { - "confirm_password": "Подтверждение пароля", + "confirm_password": "Подтвердите пароль", "current_creds": "Текущие учетные данные", "new_creds": "Новые учетные данные", "new_username_desc": "Если не указано, имя пользователя не изменится", @@ -357,49 +357,49 @@ }, "pin": { "device_name": "Имя устройства", - "pair_failure": "Соединение не удалось: проверьте, правильно ли введен PIN-код", - "pair_success": "Успех! Пожалуйста, отметьте Лунный свет для продолжения", - "pin_pairing": "PIN Pairing", + "pair_failure": "Не удалось привязать: проверьте, правильность PIN-кода", + "pair_success": "Успешно! Перейдите в Moonlight для продолжения", + "pin_pairing": "PIN привязки", "send": "Отправить", - "warning_msg": "Убедитесь, что у вас есть доступ к клиенту, с которым вы сотрудничаете. Это программное обеспечение может дать полный контроль вашему компьютеру, так что будьте осторожны!" + "warning_msg": "Убедитесь, что клиент, который вы привязываете доступен. Данное ПО может передать полный контроль над вашим компьютером, так что будьте осторожны!" }, "resource_card": { "github_discussions": "GitHub Discussions", "legal": "Юридическая информация", - "legal_desc": "Продолжая использовать программное обеспечение, вы соглашаетесь с условиями в следующих документах.", + "legal_desc": "Используя данное ПО, вы соглашаетесь с условиями, изложенными в следующих документах.", "license": "Лицензия", "lizardbyte_website": "Сайт LizardByte", - "resources": "Ресурсы", - "resources_desc": "Ресурсы Sunshine!", - "third_party_notice": "Уведомление о третьих лицах" + "resources": "Полезные источники", + "resources_desc": "Полезные ресурсы, посвящённые Sunshine!", + "third_party_notice": "Уведомление о третьих сторонах" }, "troubleshooting": { "force_close": "Принудительное закрытие", - "force_close_desc": "Если Moonlight жалуется на запущенное приложение, принудительное закрытие приложения устранит проблему.", + "force_close_desc": "Если Moonlight жалуется на запущенное приложение, принудительное закрытие приложения должно помочь.", "force_close_error": "Ошибка при закрытии приложения", - "force_close_success": "Заявка успешно закрыта!", - "logs": "Логи", - "logs_desc": "Смотреть журналы, загруженные Sunshine", + "force_close_success": "Приложение закрыто успешно!", + "logs": "Журналы", + "logs_desc": "Смотреть журналы, выгруженные Sunshine", "logs_find": "Найти...", "restart_sunshine": "Перезапустить Sunshine", - "restart_sunshine_desc": "Если Sunshine работает некорректно, вы можете попробовать перезапустить его. Это прекратит работу всех запущенных сессий.", + "restart_sunshine_desc": "Если Sunshine работает некорректно, вы можете попробовать перезапустить его. Это прекратит работу всех запущенных сеансов.", "restart_sunshine_success": "Sunshine перезапускается", "troubleshooting": "Устранение проблем", - "unpair_all": "Отменить все", - "unpair_all_error": "Ошибка при отмене подключения", - "unpair_all_success": "Успешное восстановление!", - "unpair_desc": "Удалите ваши сопряженные устройства. Отдельные несвязанные устройства с активным сеансом останутся подключенными, но не могут начать или возобновить сессию.", - "unpair_single_no_devices": "Нет сопряженных устройств.", - "unpair_single_success": "Однако устройство (устройства) все еще может находиться в активном сеансе. Используйте кнопку «Закрыть» выше для завершения любых открытых сеансов.", + "unpair_all": "Отвязать все", + "unpair_all_error": "Ошибка при отвязывании", + "unpair_all_success": "Все устройства отвязаны.", + "unpair_desc": "Удалите свои привязанные устройства. Устройства с активным сеансом, отвязанные по одному, останутся подключенными, но не смогут начать или возобновить сеанс.", + "unpair_single_no_devices": "Нет привязанных устройств.", + "unpair_single_success": "Однако, устройство (устройства) может находиться в имеющемся сеансе. Воспользуйтесь кнопкой «Принудительное закрытие» выше для завершения всех сеансов.", "unpair_single_unknown": "Неизвестный клиент", - "unpair_title": "Непарные устройства" + "unpair_title": "Отвязать устройства" }, "welcome": { "confirm_password": "Подтвердите пароль", - "create_creds": "Перед началом работы нам нужно сделать новый логин и пароль для доступа к Web UI.", - "create_creds_alert": "Учетные данные, указанные ниже, необходимы для доступа к сетевому интерфейсу Sunshine. Храните их в безопасности, так как вы никогда их не увидите снова!", + "create_creds": "Перед началом работы нам нужно создать новые логин и пароль для доступа к веб-интерфейсу.", + "create_creds_alert": "Учетные данные, указанные ниже, необходимы для доступа к веб-интерфейсу Sunshine. Сохраните их в надёжном месте, так как больше вы их не увидите!", "greeting": "Добро пожаловать в Sunshine!", - "login": "Логин", + "login": "Вход", "welcome_success": "Эта страница скоро перезагрузится, ваш браузер запросит новые учетные данные" } }