├── .clang-format ├── .clang-tidy ├── .github ├── assets │ ├── audio.png │ ├── autostart-option.png │ ├── running-buddy.png │ ├── settings.png │ └── sunshine.png └── workflows │ ├── build.yaml │ ├── filename.yaml │ ├── formatting.yaml │ └── publish.yaml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── resources ├── icons │ ├── moondeckbuddy-128.png │ ├── moondeckbuddy-16.png │ ├── moondeckbuddy-256.png │ ├── moondeckbuddy-32.png │ ├── moondeckbuddy-64.png │ └── moondeckbuddy.ico ├── linux │ ├── AppRun │ └── MoonDeckBuddy.desktop ├── resources.qrc └── windows │ ├── innocode.iss │ ├── innoextra.iss │ ├── resources.rc │ └── version.rc.in └── src ├── CMakeLists.txt ├── buddy ├── CMakeLists.txt ├── main.cpp ├── routing.cpp └── routing.h ├── lib ├── CMakeLists.txt ├── os │ ├── CMakeLists.txt │ ├── autostarthandler.cpp │ ├── include │ │ └── os │ │ │ ├── autostarthandler.h │ │ │ ├── networkinfo.h │ │ │ ├── pccontrol.h │ │ │ ├── pcstatehandler.h │ │ │ ├── sleepinhibitor.h │ │ │ ├── steamhandler.h │ │ │ ├── streamstatehandler.h │ │ │ ├── streamstatehandlerinterface.h │ │ │ ├── sunshineapps.h │ │ │ └── systemtray.h │ ├── linux │ │ ├── CMakeLists.txt │ │ ├── include │ │ │ └── os │ │ │ │ └── linux │ │ │ │ ├── nativeautostarthandler.h │ │ │ │ ├── nativepcstatehandler.h │ │ │ │ ├── nativeprocesshandler.h │ │ │ │ └── nativesleepinhibitor.h │ │ ├── nativeautostarthandler.cpp │ │ ├── nativepcstatehandler.cpp │ │ ├── nativeprocesshandler.cpp │ │ └── nativesleepinhibitor.cpp │ ├── networkinfo.cpp │ ├── pccontrol.cpp │ ├── pcstatehandler.cpp │ ├── shared │ │ ├── CMakeLists.txt │ │ └── include │ │ │ └── os │ │ │ └── shared │ │ │ ├── nativeautostarthandlerinterface.h │ │ │ ├── nativepcstatehandlerinterface.h │ │ │ ├── nativeprocesshandlerinterface.h │ │ │ └── nativesleepinhibitorinterface.h │ ├── sleepinhibitor.cpp │ ├── steam │ │ ├── CMakeLists.txt │ │ ├── include │ │ │ └── os │ │ │ │ └── steam │ │ │ │ ├── steamappwatcher.h │ │ │ │ ├── steamcontentlogtracker.h │ │ │ │ ├── steamgameprocesslogtracker.h │ │ │ │ ├── steamlogtracker.h │ │ │ │ ├── steamprocesstracker.h │ │ │ │ ├── steamshaderlogtracker.h │ │ │ │ └── steamwebhelperlogtracker.h │ │ ├── steamappwatcher.cpp │ │ ├── steamcontentlogtracker.cpp │ │ ├── steamgameprocesslogtracker.cpp │ │ ├── steamlogtracker.cpp │ │ ├── steamprocesstracker.cpp │ │ ├── steamshaderlogtracker.cpp │ │ └── steamwebhelperlogtracker.cpp │ ├── steamhandler.cpp │ ├── streamstatehandler.cpp │ ├── sunshineapps.cpp │ ├── systemtray.cpp │ └── win │ │ ├── CMakeLists.txt │ │ ├── include │ │ └── os │ │ │ └── win │ │ │ ├── nativeautostarthandler.h │ │ │ ├── nativepcstatehandler.h │ │ │ ├── nativeprocesshandler.h │ │ │ └── nativesleepinhibitor.h │ │ ├── nativeautostarthandler.cpp │ │ ├── nativepcstatehandler.cpp │ │ ├── nativeprocesshandler.cpp │ │ └── nativesleepinhibitor.cpp ├── server │ ├── CMakeLists.txt │ ├── clientids.cpp │ ├── httpserver.cpp │ ├── include │ │ └── server │ │ │ ├── clientids.h │ │ │ ├── httpserver.h │ │ │ └── pairingmanager.h │ └── pairingmanager.cpp ├── shared │ ├── CMakeLists.txt │ ├── appmetadata.cpp │ ├── include │ │ └── shared │ │ │ ├── appmetadata.h │ │ │ ├── enums.h │ │ │ └── loggingcategories.h │ └── loggingcategories.cpp └── utils │ ├── CMakeLists.txt │ ├── appsettings.cpp │ ├── heartbeat.cpp │ ├── include │ └── utils │ │ ├── appsettings.h │ │ ├── heartbeat.h │ │ ├── jsonvalueconverter.h │ │ ├── logsettings.h │ │ ├── pairinginput.h │ │ ├── singleinstanceguard.h │ │ └── unixsignalhandler.h │ ├── logsettings.cpp │ ├── pairinginput.cpp │ ├── singleinstanceguard.cpp │ └── unixsignalhandler.cpp └── stream ├── CMakeLists.txt └── main.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | AccessModifierOffset: -4 3 | AlignAfterOpenBracket: Align 4 | AlignConsecutiveMacros: true 5 | AlignConsecutiveAssignments: true 6 | AlignConsecutiveDeclarations: true 7 | AlignEscapedNewlines: Right 8 | AlignOperands: true 9 | AlignTrailingComments: true 10 | AllowAllArgumentsOnNextLine: true 11 | AllowAllConstructorInitializersOnNextLine: false 12 | AllowAllParametersOfDeclarationOnNextLine: false 13 | AllowShortBlocksOnASingleLine: 'Never' 14 | AllowShortCaseLabelsOnASingleLine: false 15 | AllowShortFunctionsOnASingleLine: None 16 | AllowShortIfStatementsOnASingleLine: Never 17 | AllowShortLambdasOnASingleLine: All 18 | AllowShortLoopsOnASingleLine: false 19 | AlwaysBreakAfterReturnType: None 20 | AlwaysBreakBeforeMultilineStrings: false 21 | AlwaysBreakTemplateDeclarations: 'Yes' 22 | BinPackArguments: true 23 | BinPackParameters: true 24 | BreakBeforeBinaryOperators: NonAssignment 25 | BreakBeforeBraces: Allman 26 | BreakBeforeTernaryOperators: true 27 | BreakConstructorInitializers: BeforeComma 28 | BreakInheritanceList: BeforeComma 29 | BreakStringLiterals: true 30 | ColumnLimit: 120 31 | CompactNamespaces: false 32 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 33 | ConstructorInitializerIndentWidth: 4 34 | ContinuationIndentWidth: 4 35 | Cpp11BracedListStyle: true 36 | DerivePointerAlignment: false 37 | FixNamespaceComments: true 38 | IncludeBlocks: Preserve 39 | IndentCaseLabels: true 40 | IndentPPDirectives: BeforeHash 41 | #IndentRequiresClause: false 42 | IndentWidth: 4 43 | IndentWrappedFunctionNames: true 44 | KeepEmptyLinesAtTheStartOfBlocks: false 45 | Language: Cpp 46 | MaxEmptyLinesToKeep: 1 47 | NamespaceIndentation: None 48 | PointerAlignment: Left 49 | ReflowComments: true 50 | #RequiresClausePosition: WithPreceding 51 | SortIncludes: 'CaseSensitive' 52 | SortUsingDeclarations: true 53 | SpaceAfterCStyleCast: false 54 | SpaceAfterLogicalNot: false 55 | SpaceAfterTemplateKeyword: false 56 | SpaceBeforeAssignmentOperators: true 57 | SpaceBeforeCpp11BracedList: false 58 | SpaceBeforeCtorInitializerColon: true 59 | SpaceBeforeInheritanceColon: true 60 | SpaceBeforeParens: ControlStatements 61 | SpaceBeforeRangeBasedForLoopColon: true 62 | SpaceInEmptyParentheses: false 63 | SpacesBeforeTrailingComments: 2 64 | SpacesInAngles: false 65 | SpacesInCStyleCastParentheses: false 66 | SpacesInContainerLiterals: false 67 | SpacesInParentheses: false 68 | SpacesInSquareBrackets: false 69 | Standard: c++20 70 | TabWidth: 4 71 | UseTab: Never 72 | 73 | ... 74 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | Checks: '-*,bugprone-*,modernize-*,-modernize-use-trailing-return-type,-modernize-use-nodiscard,-bugprone-suspicious-include,cert-dcl21-cpp,cert-dcl50-cpp,cert-env33-c,cert-err34-c,cert-err52-cpp,cert-err60-cpp,cert-flp30-c,cert-msc50-cpp,cert-msc51-cpp,cppcoreguidelines-*,-cppcoreguidelines-macro-usage,google-build-using-namespace,google-explicit-constructor,google-global-names-in-headers,google-readability-casting,google-runtime-int,google-runtime-operator,hicpp-*,misc-*,performance-*,readability-*' 2 | WarningsAsErrors: '*' 3 | CheckOptions: 4 | - key: bugprone-argument-comment.StrictMode 5 | value: 1 6 | - key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic 7 | value: 1 8 | - key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor 9 | value: 1 10 | - key: hicpp-special-member-functions.AllowSoleDefaultDtor 11 | value: 1 12 | - key: cppcoreguidelines-special-member-functions.AllowMissingMoveFunctionsWhenCopyIsDeleted 13 | value: 1 14 | - key: hicpp-special-member-functions.AllowMissingMoveFunctionsWhenCopyIsDeleted 15 | value: 1 16 | FormatStyle: 'file' 17 | HeaderFilterRegex: 'src/.*\.(?:h|c|hpp|cpp)$' -------------------------------------------------------------------------------- /.github/assets/audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FrogTheFrog/moondeck-buddy/376352b17e4f879b6ba19df57afddbdadb45c506/.github/assets/audio.png -------------------------------------------------------------------------------- /.github/assets/autostart-option.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FrogTheFrog/moondeck-buddy/376352b17e4f879b6ba19df57afddbdadb45c506/.github/assets/autostart-option.png -------------------------------------------------------------------------------- /.github/assets/running-buddy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FrogTheFrog/moondeck-buddy/376352b17e4f879b6ba19df57afddbdadb45c506/.github/assets/running-buddy.png -------------------------------------------------------------------------------- /.github/assets/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FrogTheFrog/moondeck-buddy/376352b17e4f879b6ba19df57afddbdadb45c506/.github/assets/settings.png -------------------------------------------------------------------------------- /.github/assets/sunshine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FrogTheFrog/moondeck-buddy/376352b17e4f879b6ba19df57afddbdadb45c506/.github/assets/sunshine.png -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [pull_request,workflow_call] 3 | jobs: 4 | Formatting: 5 | uses: ./.github/workflows/formatting.yaml 6 | 7 | Filename: 8 | uses: ./.github/workflows/filename.yaml 9 | with: 10 | original_event_name: "${{ github.event_name }}" 11 | 12 | Windows: 13 | runs-on: windows-latest 14 | needs: [Formatting, Filename] 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | submodules: recursive 20 | 21 | - name: Install MSCV 22 | uses: ilammy/msvc-dev-cmd@v1 23 | 24 | - name: Install Qt 25 | uses: jurplel/install-qt-action@v4 26 | with: 27 | version: '6.8.*' 28 | host: 'windows' 29 | target: 'desktop' 30 | arch: 'win64_msvc2022_64' 31 | modules: 'qthttpserver qtwebsockets' 32 | 33 | - name: Configure 34 | run: cmake -DENABLE_CLANG_TIDY=True -DCMAKE_CXX_COMPILER=cl -DCMAKE_BUILD_TYPE:STRING=Release -B build -G Ninja 35 | 36 | - name: Build 37 | run: cmake --build build 38 | 39 | - name: Bundle 40 | run: cd build && cpack -C CPackConfig.cmake 41 | 42 | - name: Rename 7z 43 | working-directory: ./dist 44 | run: mv MoonDeckBuddy*.7z ${{ needs.Filename.outputs.filename }}-win64.7z 45 | 46 | - name: Rename Exe 47 | working-directory: ./dist 48 | run: mv MoonDeckBuddy*.exe ${{ needs.Filename.outputs.filename }}-win64.exe 49 | 50 | - name: Upload artifacts 51 | uses: actions/upload-artifact@v4 52 | with: 53 | name: moondeckbuddy-windows 54 | path: dist/* 55 | if-no-files-found: error 56 | 57 | Linux: 58 | runs-on: ubuntu-22.04 # Stay on 22.04 for now as the next release increments GLIBC 59 | needs: [Formatting, Filename] 60 | steps: 61 | - name: Install packages 62 | run: sudo apt-get install ninja-build libxrandr-dev libfuse2 libxcb-cursor-dev 63 | 64 | - name: Build libproc2 65 | run: | 66 | sudo apt-get install autopoint autoconf automake libtool-bin gettext libncursesw5-dev dejagnu libnuma-dev libsystemd-dev 67 | wget https://github.com/warmchang/procps/archive/refs/tags/v4.0.5.zip -O procps.zip 68 | unzip procps.zip 69 | cd procps-4.0.5 70 | ./autogen.sh 71 | ./configure 72 | make 73 | sudo make install 74 | 75 | - name: Checkout 76 | uses: actions/checkout@v4 77 | with: 78 | submodules: recursive 79 | 80 | - name: Install Qt 81 | uses: jurplel/install-qt-action@v4 82 | with: 83 | version: '6.8.*' 84 | host: 'linux' 85 | target: 'desktop' 86 | arch: 'linux_gcc_64' 87 | modules: 'qthttpserver qtwebsockets' 88 | 89 | - name: Configure 90 | run: mkdir build && cd build && cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE:STRING=Release -G Ninja 91 | 92 | - name: Build 93 | working-directory: ./build 94 | run: ninja 95 | 96 | - name: Install 97 | working-directory: ./build 98 | run: DESTDIR=AppDir ninja install 99 | 100 | - name: Get linuxdeploy's AppImage 101 | working-directory: ./build 102 | run: wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage && chmod +x ./linuxdeploy-x86_64.AppImage 103 | 104 | - name: Get linuxdeploy's QT AppImage 105 | working-directory: ./build 106 | run: wget https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage && chmod +x ./linuxdeploy-plugin-qt-x86_64.AppImage 107 | 108 | - name: Rename 109 | run: echo "OUTPUT=${{ needs.Filename.outputs.filename }}-x86_64.AppImage" >> $GITHUB_ENV 110 | 111 | - name: Package AppImage 112 | working-directory: ./build 113 | run: ./linuxdeploy-x86_64.AppImage --appdir ./AppDir --output appimage --plugin qt --custom-apprun="../resources/linux/AppRun" --library="/usr/lib/x86_64-linux-gnu/libssl.so.3" --library="/usr/lib/x86_64-linux-gnu/libcrypto.so.3" 114 | 115 | - name: Upload artifacts 116 | uses: actions/upload-artifact@v4 117 | with: 118 | name: moondeckbuddy-linux 119 | path: ./build/MoonDeckBuddy*AppImage 120 | if-no-files-found: error 121 | -------------------------------------------------------------------------------- /.github/workflows/filename.yaml: -------------------------------------------------------------------------------- 1 | name: Generate binary filename 2 | on: 3 | workflow_call: 4 | inputs: 5 | original_event_name: 6 | required: true 7 | type: string 8 | outputs: 9 | filename: 10 | value: ${{ jobs.Filename.outputs.filename }} 11 | 12 | jobs: 13 | Filename: 14 | runs-on: ubuntu-latest 15 | outputs: 16 | version: ${{ steps.version.outputs.name }} 17 | hash: ${{ steps.hash.outputs.name }} 18 | filename: ${{ steps.filename.outputs.name }} 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | 23 | - name: Using the CMakeLists version 24 | if: inputs.original_event_name != 'pull_request' 25 | id: version 26 | run: echo "name=$(grep -E "\s+VERSION" CMakeLists.txt | xargs | cut -d' ' -f 2)" >> $GITHUB_OUTPUT 27 | 28 | - name: Using the commit SHA as a version 29 | if: inputs.original_event_name == 'pull_request' 30 | id: hash 31 | run: echo "name=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_OUTPUT 32 | 33 | - name: Generate name 34 | id: filename 35 | run: echo "name=MoonDeckBuddy-${{ steps.version.outputs.name }}${{ steps.hash.outputs.name }}" >> $GITHUB_OUTPUT 36 | 37 | - name: Print the filename base 38 | run: echo "${{ steps.filename.outputs.name }}" -------------------------------------------------------------------------------- /.github/workflows/formatting.yaml: -------------------------------------------------------------------------------- 1 | name: Formatting check 2 | on: [workflow_call] 3 | jobs: 4 | Formatting: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v4 9 | 10 | - name: Clang-format check 11 | run: find $PWD/src -type f \( -name "*.h" -o -name "*.cpp" \) -exec clang-format -style=file --dry-run --Werror {} + -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Build & Publish 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | Build: 8 | uses: ./.github/workflows/build.yaml 9 | 10 | Publish: 11 | runs-on: ubuntu-latest 12 | needs: [Build] 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | submodules: recursive 18 | 19 | - name: Fetch tags 20 | run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* || echo "No tags fetched" 21 | 22 | - name: Get project version 23 | id: project_version 24 | run: echo "VERSION=$(grep -E "\s+VERSION" CMakeLists.txt | xargs | cut -d' ' -f 2)" >> $GITHUB_OUTPUT 25 | 26 | - name: Get tag status 27 | id: tagstatus 28 | run: echo "TAG_EXISTS=$(git show-ref --tags --verify --quiet -- 'refs/tags/v${{ steps.project_version.outputs.VERSION }}' && echo 1 || echo 0)" >> $GITHUB_OUTPUT 29 | 30 | - name: Print version and status 31 | run: | 32 | echo "Version: ${{ steps.project_version.outputs.VERSION }}" 33 | echo "Aready exists: ${{ steps.tagstatus.outputs.TAG_EXISTS }}" 34 | 35 | - name: Download artifacts 36 | uses: actions/download-artifact@v4 37 | if: steps.tagstatus.outputs.TAG_EXISTS == 0 38 | with: 39 | path: dist 40 | merge-multiple: true 41 | 42 | - name: Tag release 43 | uses: rickstaa/action-create-tag@v1 44 | if: steps.tagstatus.outputs.TAG_EXISTS == 0 45 | with: 46 | tag: "v${{ steps.project_version.outputs.VERSION }}" 47 | 48 | - name: Release 49 | uses: softprops/action-gh-release@v2 50 | if: steps.tagstatus.outputs.TAG_EXISTS == 0 51 | with: 52 | tag_name: "v${{ steps.project_version.outputs.VERSION }}" 53 | files: dist/* 54 | draft: true 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | CMakeLists.txt.user 2 | build/ 3 | cmake-build-*/ 4 | dist/ 5 | .vscode/ 6 | .idea/ 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "resources/ssl"] 2 | path = resources/ssl 3 | url = https://github.com/FrogTheFrog/moondeck-keys.git 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------------------------------------------------------------------- 2 | # Project config 3 | #---------------------------------------------------------------------------------------------------------------------- 4 | 5 | cmake_minimum_required(VERSION 3.27) 6 | project(MoonDeckBuddy 7 | VERSION 1.8.2 8 | DESCRIPTION "A server-side buddy app to help control the PC and Steam from SteamDeck via the MoonDeck plugin." 9 | HOMEPAGE_URL "https://github.com/FrogTheFrog/moondeck-buddy" 10 | LANGUAGES C CXX) 11 | 12 | set(EXEC_NAME_BUDDY MoonDeckBuddy) 13 | set(EXEC_NAME_STREAM MoonDeckStream) 14 | 15 | set(CMAKE_CXX_STANDARD 20) 16 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 17 | 18 | set(CMAKE_AUTOMOC ON) 19 | set(CMAKE_AUTORCC ON) 20 | 21 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 22 | set(BUDDY_RESOURCES "${CMAKE_CURRENT_LIST_DIR}/resources") 23 | set(ENABLE_CLANG_TIDY OFF CACHE BOOL "Enable clang-tidy build (slow)") 24 | 25 | if(MSVC) 26 | # warning level 4 and all warnings as errors 27 | add_compile_options(/W4 /WX) 28 | else() 29 | # lots of warnings and all warnings as errors 30 | add_compile_options(-Wall -Wextra -pedantic -Werror) 31 | endif() 32 | 33 | #---------------------------------------------------------------------------------------------------------------------- 34 | # Build type parsing 35 | #---------------------------------------------------------------------------------------------------------------------- 36 | 37 | set(DEBUG_MODE FALSE) 38 | string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER) 39 | if(CMAKE_BUILD_TYPE_LOWER MATCHES "debug") 40 | set(DEBUG_MODE TRUE) 41 | endif() 42 | 43 | #---------------------------------------------------------------------------------------------------------------------- 44 | # CLang-Tidy 45 | #---------------------------------------------------------------------------------------------------------------------- 46 | 47 | if(ENABLE_CLANG_TIDY) 48 | if(NOT MSVC) 49 | message(STATUS "Clang-tidy build is enabled!") 50 | set(CMAKE_CXX_CLANG_TIDY clang-tidy) 51 | file(WRITE "${CMAKE_BINARY_DIR}/.clang-tidy" "Checks: '-*,llvm-twine-local'") 52 | endif() 53 | endif() 54 | 55 | #---------------------------------------------------------------------------------------------------------------------- 56 | # Pre-Install 57 | #---------------------------------------------------------------------------------------------------------------------- 58 | 59 | IF (WIN32) 60 | # General options 61 | set(CPACK_PACKAGE_VENDOR "MoonDeck") 62 | set(CPACK_PACKAGE_INSTALL_DIRECTORY ${PROJECT_NAME}) 63 | set(CPACK_PACKAGE_EXECUTABLES "${EXEC_NAME_BUDDY};${EXEC_NAME_BUDDY}") 64 | set(CPACK_OUTPUT_FILE_PREFIX "../dist") 65 | set(CPACK_GENERATOR "7Z;INNOSETUP") 66 | 67 | # INNOSETUP specific options 68 | set(CPACK_INNOSETUP_IGNORE_LICENSE_PAGE ON) 69 | set(CPACK_INNOSETUP_USE_MODERN_WIZARD ON) 70 | set(CPACK_INNOSETUP_SETUP_PrivilegesRequired "lowest") 71 | set(CPACK_INNOSETUP_SETUP_RestartApplications OFF) 72 | set(CPACK_INNOSETUP_SETUP_CloseApplications "Force") 73 | set(CPACK_INNOSETUP_SETUP_SetupIconFile "${CMAKE_CURRENT_SOURCE_DIR}/resources/icons/moondeckbuddy.ico") 74 | set(CPACK_INNOSETUP_SETUP_UninstallDisplayIcon "{app}/bin/${EXEC_NAME_BUDDY}.exe") 75 | set(CPACK_INNOSETUP_CODE_FILES "${BUDDY_RESOURCES}/windows/innocode.iss") 76 | set(CPACK_INNOSETUP_EXTRA_SCRIPTS "${BUDDY_RESOURCES}/windows/innoextra.iss") 77 | ELSEIF(NOT UNIX) 78 | message(FATAL_ERROR "OS is not supported!") 79 | ENDIF() 80 | 81 | #---------------------------------------------------------------------------------------------------------------------- 82 | # Subdirectories 83 | #---------------------------------------------------------------------------------------------------------------------- 84 | 85 | add_subdirectory(src) 86 | 87 | #---------------------------------------------------------------------------------------------------------------------- 88 | # Install 89 | #---------------------------------------------------------------------------------------------------------------------- 90 | 91 | IF(WIN32) 92 | find_package(Qt6 REQUIRED COMPONENTS Core) 93 | set(DEPLOY_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/deploy_${PROJECT_NAME}.cmake") 94 | set(BINARIES "${EXEC_NAME_BUDDY};${EXEC_NAME_STREAM}") 95 | 96 | file(GENERATE OUTPUT ${DEPLOY_SCRIPT} CONTENT " 97 | include(\"${QT_DEPLOY_SUPPORT}\") 98 | message(STATUS \"Running Qt deploy tool in working directory '${QT_DEPLOY_PREFIX}'\") 99 | 100 | set(BINARIES \"${BINARIES}\") 101 | foreach(BINARY \${BINARIES}) 102 | execute_process( 103 | COMMAND_ECHO STDOUT 104 | COMMAND \"\${__QT_DEPLOY_TOOL}\" \"\${QT_DEPLOY_BIN_DIR}/\${BINARY}.exe\" --verbose 2 --dir . --libdir bin --plugindir plugins --force --no-compiler-runtime 105 | WORKING_DIRECTORY \"\${QT_DEPLOY_PREFIX}\" 106 | RESULT_VARIABLE result 107 | ) 108 | if(result) 109 | message(FATAL_ERROR \"Executing \${__QT_DEPLOY_TOOL} failed: \${result}\") 110 | endif() 111 | endforeach() 112 | 113 | qt6_deploy_qt_conf(\"\${QT_DEPLOY_PREFIX}/\${QT_DEPLOY_BIN_DIR}/qt.conf\" 114 | PREFIX \"../\" 115 | BIN_DIR \"\${QT_DEPLOY_BIN_DIR}\" 116 | LIB_DIR \"\${QT_DEPLOY_LIB_DIR}\" 117 | PLUGINS_DIR \"\${QT_DEPLOY_PLUGINS_DIR}\" 118 | QML_DIR \"\${QT_DEPLOY_QML_DIR}\" 119 | ) 120 | ") 121 | 122 | foreach(BINARY ${BINARIES}) 123 | install(TARGETS ${BINARY}) 124 | endforeach() 125 | install(SCRIPT ${DEPLOY_SCRIPT}) 126 | 127 | include(CPack) 128 | ELSEIF(UNIX) 129 | install( 130 | TARGETS ${EXEC_NAME_BUDDY} ${EXEC_NAME_STREAM} 131 | RUNTIME DESTINATION bin 132 | ) 133 | 134 | install( 135 | FILES resources/linux/MoonDeckBuddy.desktop 136 | DESTINATION share/applications/ 137 | ) 138 | 139 | foreach(size 16;32;64;128;256) 140 | install( 141 | FILES resources/icons/moondeckbuddy-${size}.png 142 | DESTINATION share/icons/hicolor/${size}x${size}/apps/ 143 | RENAME moondeckbuddy.png 144 | ) 145 | endforeach() 146 | ELSE() 147 | message(FATAL_ERROR "OS is not supported!") 148 | ENDIF() 149 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MoonDeck Buddy ![Status](https://github.com/FrogTheFrog/moondeck-buddy/actions/workflows/publish.yaml/badge.svg) [![Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://discord.com/invite/U88fbeHyzt) 2 | 3 | A server-side part of the [MoonDeck](https://github.com/FrogTheFrog/moondeck) plugin for the SteamDeck. 4 | 5 | The main goal is to have reliable information about Steam state: 6 | * is Steam running or not; 7 | * what Steam game is running and whether the game we want is updating or not. 8 | 9 | Additionally, it allows to: 10 | * launch a Steam game; 11 | * close the Steam process; 12 | * shutdown, restart or suspend the PC. 13 | 14 | ## How to install it? 15 | 16 | See the [wiki](https://github.com/FrogTheFrog/moondeck-buddy/wiki) page for installation steps and other information! 17 | -------------------------------------------------------------------------------- /resources/icons/moondeckbuddy-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FrogTheFrog/moondeck-buddy/376352b17e4f879b6ba19df57afddbdadb45c506/resources/icons/moondeckbuddy-128.png -------------------------------------------------------------------------------- /resources/icons/moondeckbuddy-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FrogTheFrog/moondeck-buddy/376352b17e4f879b6ba19df57afddbdadb45c506/resources/icons/moondeckbuddy-16.png -------------------------------------------------------------------------------- /resources/icons/moondeckbuddy-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FrogTheFrog/moondeck-buddy/376352b17e4f879b6ba19df57afddbdadb45c506/resources/icons/moondeckbuddy-256.png -------------------------------------------------------------------------------- /resources/icons/moondeckbuddy-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FrogTheFrog/moondeck-buddy/376352b17e4f879b6ba19df57afddbdadb45c506/resources/icons/moondeckbuddy-32.png -------------------------------------------------------------------------------- /resources/icons/moondeckbuddy-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FrogTheFrog/moondeck-buddy/376352b17e4f879b6ba19df57afddbdadb45c506/resources/icons/moondeckbuddy-64.png -------------------------------------------------------------------------------- /resources/icons/moondeckbuddy.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FrogTheFrog/moondeck-buddy/376352b17e4f879b6ba19df57afddbdadb45c506/resources/icons/moondeckbuddy.ico -------------------------------------------------------------------------------- /resources/linux/AppRun: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | set -e 3 | 4 | HERE="$(readlink -f "$(dirname "$0")")" 5 | 6 | # Wayland plugin is not packaged, so defining this to get rid of the warning 7 | export QT_QPA_PLATFORM=xcb 8 | 9 | if [ "$1" = "--exec" ]; then 10 | BINARY_NAME="$2" 11 | ARGS="${@:3}" 12 | if [ -e "$HERE/usr/bin/$BINARY_NAME" ] ; then 13 | exec "$HERE/usr/bin/$BINARY_NAME" $ARGS 14 | else 15 | echo "$BINARY_NAME does not exist!" 16 | exit 1 17 | fi 18 | elif [ ! -z $APPIMAGE ] ; then 19 | BINARY_NAME=$(basename "$ARGV0") 20 | ARGS="$@" 21 | if [ -e "$HERE/usr/bin/$BINARY_NAME" ] ; then 22 | # Replace the app image env with the symlink as it will be used 23 | # to create an autostart script 24 | SYMLINK_PATH="$(realpath -s "$ARGV0")" 25 | APPIMAGE=$SYMLINK_PATH exec "$HERE/usr/bin/$BINARY_NAME" $ARGS 26 | else 27 | exec "$HERE/usr/bin/MoonDeckBuddy" $ARGS 28 | fi 29 | else 30 | ARGS="$@" 31 | exec "$HERE/usr/bin/MoonDeckBuddy" $ARGS 32 | fi -------------------------------------------------------------------------------- /resources/linux/MoonDeckBuddy.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=MoonDeckBuddy 3 | Type=Application 4 | Exec=MoonDeckBuddy 5 | Icon=moondeckbuddy 6 | Categories=Utility 7 | -------------------------------------------------------------------------------- /resources/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icons/moondeckbuddy.ico 5 | ssl/moondeck_cert.pem 6 | ssl/moondeck_key.pem 7 | 8 | -------------------------------------------------------------------------------- /resources/windows/innocode.iss: -------------------------------------------------------------------------------- 1 | function GetPreviousAppDir(): string; 2 | var InstallPath: string; 3 | var RegPath: string; 4 | begin 5 | RegPath := 'Software\Microsoft\Windows\CurrentVersion\Uninstall\MoonDeckBuddy_is1'; 6 | // Check the HKCU (user-level) registry for a previous installation 7 | if RegQueryStringValue(HKCU, RegPath, 'Inno Setup: App Path', InstallPath) then 8 | begin 9 | Result := RemoveQuotes(InstallPath); 10 | end 11 | // If not found, check the HKLM (system-level) registry for previous installation with admin rights 12 | else if RegQueryStringValue(HKLM, RegPath, 'Inno Setup: App Path', InstallPath) then 13 | begin 14 | Result := RemoveQuotes(InstallPath); 15 | end 16 | else 17 | begin 18 | // If neither registry key is found, return an empty string 19 | Result := ''; 20 | end; 21 | end; 22 | 23 | function InitializeSetup(): Boolean; 24 | var PreviousAppDir: string; 25 | var ResultCode: Integer; 26 | begin 27 | PreviousAppDir := GetPreviousAppDir(); 28 | if PreviousAppDir <> '' then 29 | begin 30 | // Return code does not matter as the AppMutex be a fallback 31 | Exec(Format('%s\bin\MoonDeckBuddy.exe', [PreviousAppDir]), '--close-all', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) 32 | end; 33 | 34 | Result := True 35 | end; 36 | 37 | procedure CurStepChanged(CurStep: TSetupStep); 38 | var Version: String; 39 | var ResultCode: Integer; 40 | var NsisUninstaller: string; 41 | begin 42 | if CurStep = ssInstall then 43 | begin 44 | if (GetVersionNumbersString(ExpandConstant('{app}\bin\MoonDeckBuddy.exe'), Version)) then 45 | begin 46 | Log(Format('Installed version: %s', [Version])); 47 | end 48 | else begin 49 | if RegQueryStringValue(HKCU, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\MoonDeckBuddy', 'UninstallString', NsisUninstaller) then 50 | begin 51 | NsisUninstaller := RemoveQuotes(NsisUninstaller) 52 | if Exec(NsisUninstaller, Format('/S _?=%s', [ExtractFileDir(NsisUninstaller)]), '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then 53 | begin 54 | if (ResultCode = 0) then 55 | begin 56 | DeleteFile(NsisUninstaller); 57 | end 58 | else 59 | begin 60 | MsgBox('Failed to uninstall older version. Please do so manually!', mbError, MB_OK); 61 | Abort(); 62 | end; 63 | end 64 | else 65 | begin 66 | MsgBox('Failed to uninstall older version. Please do so manually!', mbError, MB_OK); 67 | Abort(); 68 | end; 69 | end; 70 | end; 71 | end; 72 | end; 73 | 74 | procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); 75 | var ResultCode: Integer; 76 | begin 77 | if CurUninstallStep = usAppMutexCheck then 78 | begin 79 | // Return code does not matter as the AppMutex be a fallback 80 | Exec(ExpandConstant('{app}\bin\MoonDeckBuddy.exe'), '--close-all', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) 81 | end 82 | else if CurUninstallStep = usUninstall then 83 | begin 84 | if SuppressibleMsgBox('Do you want to remove settings?', mbConfirmation, MB_YESNO, IDYES) = IDYES then 85 | begin 86 | DeleteFile(ExpandConstant('{app}\bin\clients.json')); 87 | DeleteFile(ExpandConstant('{app}\bin\settings.json')); 88 | end; 89 | end; 90 | end; -------------------------------------------------------------------------------- /resources/windows/innoextra.iss: -------------------------------------------------------------------------------- 1 | [Run] 2 | Filename: "{app}\bin\MoonDeckBuddy.exe"; Description: "Enable autostart"; Flags: postinstall; Parameters: "--enable-autostart" 3 | Filename: "{app}\bin\MoonDeckBuddy.exe"; Description: "Launch application"; Flags: postinstall nowait 4 | 5 | [UninstallRun] 6 | Filename: "{app}\bin\MoonDeckBuddy.exe"; Parameters: "--disable-autostart"; RunOnceId: "Cleanup" 7 | 8 | [UninstallDelete] 9 | Type: files; Name: "{app}\bin\moondeckbuddy.log" 10 | Type: files; Name: "{app}\bin\moondeckstream.log" 11 | -------------------------------------------------------------------------------- /resources/windows/resources.rc: -------------------------------------------------------------------------------- 1 | IDI_ICON1 ICON "../icons/moondeckbuddy.ico" 2 | -------------------------------------------------------------------------------- /resources/windows/version.rc.in: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define VER_COMPANYNAME_STR "@CPACK_PACKAGE_VENDOR@\0" 4 | #define VER_FILEDESCRIPTION_STR "@EXEC_NAME@ - @PROJECT_DESCRIPTION@\0" 5 | #define VER_FILEVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0 6 | #define VER_FILEVERSION_STR "@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.0\0" 7 | #define VER_INTERNALNAME_STR "@EXEC_NAME@\0" 8 | #define VER_ORIGINALFILENAME_STR "@EXEC_NAME@.exe\0" 9 | #define VER_PRODUCTNAME_STR "@PROJECT_NAME@\0" 10 | #define VER_PRODUCTVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0 11 | #define VER_PRODUCTVERSION_STR "@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.0\0" 12 | 13 | VS_VERSION_INFO VERSIONINFO 14 | FILEVERSION VER_FILEVERSION 15 | PRODUCTVERSION VER_PRODUCTVERSION 16 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 17 | FILEFLAGS 0 18 | FILEOS VOS__WINDOWS32 19 | FILETYPE VFT_APP 20 | FILESUBTYPE VFT2_UNKNOWN 21 | BEGIN 22 | BLOCK "StringFileInfo" 23 | BEGIN 24 | BLOCK "040904E4" 25 | BEGIN 26 | VALUE "CompanyName", VER_COMPANYNAME_STR 27 | VALUE "FileDescription", VER_FILEDESCRIPTION_STR 28 | VALUE "FileVersion", VER_FILEVERSION_STR 29 | VALUE "InternalName", VER_INTERNALNAME_STR 30 | VALUE "OriginalFilename", VER_ORIGINALFILENAME_STR 31 | VALUE "ProductName", VER_PRODUCTNAME_STR 32 | VALUE "ProductVersion", VER_PRODUCTVERSION_STR 33 | END 34 | END 35 | 36 | BLOCK "VarFileInfo" 37 | BEGIN 38 | /* The following line should only be modified for localized versions. */ 39 | /* It consists of any number of WORD,WORD pairs, with each pair */ 40 | /* describing a language,codepage combination supported by the file. */ 41 | /* */ 42 | /* For example, a file might have values "0x409,1252" indicating that it */ 43 | /* supports English language (0x409) in the Windows ANSI codepage (1252). */ 44 | 45 | VALUE "Translation", 0x409, 1252 46 | 47 | END 48 | END -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------------------------------------------------------------------- 2 | # Subdirectories 3 | #---------------------------------------------------------------------------------------------------------------------- 4 | 5 | add_subdirectory(buddy) 6 | add_subdirectory(lib) 7 | add_subdirectory(stream) 8 | -------------------------------------------------------------------------------- /src/buddy/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------------------------------------------------------------------- 2 | # External dependencies 3 | #---------------------------------------------------------------------------------------------------------------------- 4 | 5 | find_package(Qt6 REQUIRED COMPONENTS Core Widgets Network) 6 | qt_standard_project_setup() 7 | 8 | #---------------------------------------------------------------------------------------------------------------------- 9 | # Target config 10 | #---------------------------------------------------------------------------------------------------------------------- 11 | 12 | set(EXEC_NAME ${EXEC_NAME_BUDDY}) 13 | set(RESOURCES "${BUDDY_RESOURCES}/resources.qrc") 14 | 15 | if(WIN32) 16 | configure_file( 17 | "${BUDDY_RESOURCES}/windows/version.rc.in" 18 | "${CMAKE_CURRENT_BINARY_DIR}/version.rc" 19 | @ONLY) 20 | 21 | list(APPEND RESOURCES "${BUDDY_RESOURCES}/windows/resources.rc") 22 | list(APPEND RESOURCES "${CMAKE_CURRENT_BINARY_DIR}/version.rc") 23 | endif() 24 | 25 | add_executable(${EXEC_NAME} main.cpp routing.h routing.cpp ${RESOURCES}) 26 | target_link_libraries(${EXEC_NAME} PRIVATE Qt6::Core Qt6::Widgets Qt6::Network utilslib serverlib oslib osspecificlib sharedlib) 27 | target_compile_definitions(${EXEC_NAME} PRIVATE EXEC_VERSION="${PROJECT_VERSION}") 28 | 29 | if(NOT DEBUG_MODE) 30 | if(WIN32) 31 | set_target_properties(${EXEC_NAME} PROPERTIES WIN32_EXECUTABLE TRUE) 32 | endif() 33 | endif() 34 | -------------------------------------------------------------------------------- /src/buddy/routing.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // local includes 4 | #include "os/pccontrol.h" 5 | #include "os/sunshineapps.h" 6 | #include "server/httpserver.h" 7 | #include "server/pairingmanager.h" 8 | 9 | void setupRoutes(server::HttpServer& server, server::PairingManager& pairing_manager, os::PcControl& pc_control, 10 | os::SunshineApps& sunshine_apps, const QString& mac_address_override); 11 | -------------------------------------------------------------------------------- /src/lib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------------------------------------------------------------------- 2 | # Subdirectories 3 | #---------------------------------------------------------------------------------------------------------------------- 4 | 5 | add_subdirectory(shared) 6 | add_subdirectory(server) 7 | add_subdirectory(utils) 8 | add_subdirectory(os) 9 | -------------------------------------------------------------------------------- /src/lib/os/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------------------------------------------------------------------- 2 | # Lib config 3 | #---------------------------------------------------------------------------------------------------------------------- 4 | 5 | set(LIBNAME oslib) 6 | 7 | #---------------------------------------------------------------------------------------------------------------------- 8 | # External dependencies 9 | #---------------------------------------------------------------------------------------------------------------------- 10 | 11 | find_package(Qt6 COMPONENTS Core Widgets Network REQUIRED) 12 | qt_standard_project_setup() 13 | 14 | #---------------------------------------------------------------------------------------------------------------------- 15 | # Subdirectories 16 | #---------------------------------------------------------------------------------------------------------------------- 17 | 18 | add_subdirectory(shared) 19 | 20 | IF(WIN32) 21 | add_subdirectory(win) 22 | ELSEIF(UNIX) 23 | add_subdirectory(linux) 24 | ELSE() 25 | message(FATAL_ERROR "OS is not supported!") 26 | ENDIF() 27 | 28 | add_subdirectory(steam) 29 | 30 | #---------------------------------------------------------------------------------------------------------------------- 31 | # Header/Source files 32 | #---------------------------------------------------------------------------------------------------------------------- 33 | 34 | file(GLOB HEADERS CONFIGURE_DEPENDS "include/os/*.h") 35 | file(GLOB SOURCES CONFIGURE_DEPENDS "*.cpp") 36 | 37 | #---------------------------------------------------------------------------------------------------------------------- 38 | # Target config 39 | #---------------------------------------------------------------------------------------------------------------------- 40 | 41 | add_library(${LIBNAME} ${HEADERS} ${SOURCES}) 42 | target_link_libraries(${LIBNAME} PRIVATE Qt6::Core Qt6::Widgets Qt6::Network osspecificlib sharedlib utilslib PUBLIC ossharedlib ossteamlib) 43 | target_include_directories(${LIBNAME} PUBLIC include) 44 | -------------------------------------------------------------------------------- /src/lib/os/autostarthandler.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/autostarthandler.h" 3 | 4 | // system/Qt includes 5 | #include 6 | 7 | // os-specific includes 8 | #if defined(Q_OS_WIN) 9 | #include "os/win/nativeautostarthandler.h" 10 | #elif defined(Q_OS_LINUX) 11 | #include "os/linux/nativeautostarthandler.h" 12 | #else 13 | #error OS is not supported! 14 | #endif 15 | 16 | namespace os 17 | { 18 | AutoStartHandler::AutoStartHandler(const shared::AppMetadata& app_meta) 19 | : m_impl{std::make_unique(app_meta)} 20 | { 21 | } 22 | 23 | AutoStartHandler::~AutoStartHandler() = default; 24 | 25 | void AutoStartHandler::setAutoStart(bool enable) 26 | { 27 | m_impl->setAutoStart(enable); 28 | } 29 | 30 | bool AutoStartHandler::isAutoStartEnabled() const 31 | { 32 | return m_impl->isAutoStartEnabled(); 33 | } 34 | } // namespace os 35 | -------------------------------------------------------------------------------- /src/lib/os/include/os/autostarthandler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | 6 | // forward declarations 7 | namespace shared 8 | { 9 | class AppMetadata; 10 | } 11 | namespace os 12 | { 13 | class NativeAutoStartHandlerInterface; 14 | } 15 | 16 | namespace os 17 | { 18 | class AutoStartHandler final 19 | { 20 | public: 21 | explicit AutoStartHandler(const shared::AppMetadata& app_meta); 22 | ~AutoStartHandler(); 23 | 24 | void setAutoStart(bool enable); 25 | bool isAutoStartEnabled() const; 26 | 27 | private: 28 | std::unique_ptr m_impl; 29 | }; 30 | } // namespace os 31 | -------------------------------------------------------------------------------- /src/lib/os/include/os/networkinfo.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | 6 | namespace os 7 | { 8 | class NetworkInfo 9 | { 10 | public: 11 | static QString getMacAddress(const QHostAddress& host_address); 12 | }; 13 | } // namespace os 14 | -------------------------------------------------------------------------------- /src/lib/os/include/os/pccontrol.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | #include 6 | 7 | // local includes 8 | #include "os/autostarthandler.h" 9 | #include "os/pcstatehandler.h" 10 | #include "os/steamhandler.h" 11 | 12 | // forward declarations 13 | namespace utils 14 | { 15 | class AppSettings; 16 | } // namespace utils 17 | namespace os 18 | { 19 | class AutoStartHandlerInterface; 20 | class StreamStateHandlerInterface; 21 | } // namespace os 22 | 23 | namespace os 24 | { 25 | class PcControl : public QObject 26 | { 27 | Q_OBJECT 28 | Q_DISABLE_COPY(PcControl) 29 | 30 | public: 31 | explicit PcControl(const utils::AppSettings& app_settings); 32 | ~PcControl() override; 33 | 34 | bool launchSteam(bool big_picture_mode); 35 | enums::SteamUiMode getSteamUiMode() const; 36 | bool closeSteam(); 37 | 38 | bool launchSteamApp(std::uint64_t app_id); 39 | std::optional> getAppData() const; 40 | 41 | std::optional> getNonSteamAppData(std::uint64_t user_id) const; 42 | 43 | bool shutdownPC(); 44 | bool restartPC(); 45 | bool suspendOrHibernatePC(); 46 | bool endStream(); 47 | 48 | enums::StreamState getStreamState() const; 49 | enums::PcState getPcState() const; 50 | 51 | void setAutoStart(bool enable); 52 | bool isAutoStartEnabled() const; 53 | 54 | signals: 55 | void signalShowTrayMessage(const QString& title, const QString& message, QSystemTrayIcon::MessageIcon icon, 56 | int milliseconds_timeout_hint); 57 | 58 | private slots: 59 | void slotHandleSteamClosed(); 60 | void slotHandleStreamStateChange(); 61 | 62 | private: 63 | const utils::AppSettings& m_app_settings; 64 | AutoStartHandler m_auto_start_handler; 65 | PcStateHandler m_pc_state_handler; 66 | SteamHandler m_steam_handler; 67 | std::unique_ptr m_stream_state_handler; 68 | }; 69 | } // namespace os 70 | -------------------------------------------------------------------------------- /src/lib/os/include/os/pcstatehandler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // local includes 4 | #include "shared/enums.h" 5 | 6 | // forward declarations 7 | namespace os 8 | { 9 | class NativePcStateHandlerInterface; 10 | } 11 | 12 | namespace os 13 | { 14 | class PcStateHandler : public QObject 15 | { 16 | Q_OBJECT 17 | Q_DISABLE_COPY(PcStateHandler) 18 | 19 | public: 20 | explicit PcStateHandler(std::unique_ptr native_handler); 21 | ~PcStateHandler() override; 22 | 23 | enums::PcState getState() const; 24 | 25 | bool shutdownPC(uint grace_period_in_sec); 26 | bool restartPC(uint grace_period_in_sec); 27 | bool suspendPC(uint grace_period_in_sec); 28 | bool hibernatePC(uint grace_period_in_sec); 29 | 30 | private: 31 | using NativeMethod = bool (NativePcStateHandlerInterface::*)(); 32 | bool doChangeState(uint grace_period_in_sec, const QString& cant_do_entry, const QString& failed_to_do_entry, 33 | NativeMethod can_do_method, NativeMethod do_method, enums::PcState new_state); 34 | 35 | enums::PcState m_state{enums::PcState::Normal}; 36 | std::unique_ptr m_native_handler; 37 | }; 38 | } // namespace os 39 | -------------------------------------------------------------------------------- /src/lib/os/include/os/sleepinhibitor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | #include 6 | 7 | // forward declarations 8 | namespace os 9 | { 10 | class NativeSleepInhibitorInterface; 11 | } 12 | 13 | namespace os 14 | { 15 | class SleepInhibitor final 16 | { 17 | public: 18 | explicit SleepInhibitor(const QString& app_name); 19 | ~SleepInhibitor(); 20 | 21 | private: 22 | std::unique_ptr m_impl; 23 | }; 24 | } // namespace os 25 | -------------------------------------------------------------------------------- /src/lib/os/include/os/steamhandler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // local includes 4 | #include "os/steam/steamprocesstracker.h" 5 | #include "shared/enums.h" 6 | 7 | // forward declarations 8 | namespace utils 9 | { 10 | class AppSettings; 11 | } // namespace utils 12 | namespace os 13 | { 14 | class SteamAppWatcher; 15 | } // namespace os 16 | 17 | namespace os 18 | { 19 | class SteamHandler : public QObject 20 | { 21 | Q_OBJECT 22 | Q_DISABLE_COPY(SteamHandler) 23 | 24 | public: 25 | explicit SteamHandler(const utils::AppSettings& app_settings, 26 | std::unique_ptr process_handler_interface); 27 | ~SteamHandler() override; 28 | 29 | bool launchSteam(bool big_picture_mode); 30 | enums::SteamUiMode getSteamUiMode() const; 31 | bool close(); 32 | 33 | std::optional> getAppData() const; 34 | bool launchApp(std::uint64_t app_id); 35 | void clearSessionData(); 36 | 37 | std::optional> getNonSteamAppData(std::uint64_t user_id) const; 38 | 39 | signals: 40 | void signalSteamClosed(); 41 | 42 | private slots: 43 | void slotSteamProcessStateChanged(); 44 | 45 | private: 46 | struct SessionData 47 | { 48 | std::unique_ptr m_steam_app_watcher; 49 | }; 50 | 51 | const utils::AppSettings& m_app_settings; 52 | SteamProcessTracker m_steam_process_tracker; 53 | SessionData m_session_data; 54 | }; 55 | } // namespace os 56 | -------------------------------------------------------------------------------- /src/lib/os/include/os/streamstatehandler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // local includes 4 | #include "os/streamstatehandlerinterface.h" 5 | #include "shared/enums.h" 6 | #include "utils/heartbeat.h" 7 | 8 | namespace os 9 | { 10 | class StreamStateHandler : public StreamStateHandlerInterface 11 | { 12 | Q_OBJECT 13 | Q_DISABLE_COPY(StreamStateHandler) 14 | 15 | public: 16 | explicit StreamStateHandler(const QString& heartbeat_key); 17 | ~StreamStateHandler() override = default; 18 | 19 | bool endStream() override; 20 | enums::StreamState getCurrentState() const override; 21 | 22 | private slots: 23 | void slotHandleProcessStateChanges(); 24 | 25 | private: 26 | enums::StreamState m_state{enums::StreamState::NotStreaming}; 27 | utils::Heartbeat m_helper_heartbeat; 28 | }; 29 | } // namespace os 30 | -------------------------------------------------------------------------------- /src/lib/os/include/os/streamstatehandlerinterface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | 6 | // local includes 7 | #include "shared/enums.h" 8 | 9 | namespace os 10 | { 11 | class StreamStateHandlerInterface : public QObject 12 | { 13 | Q_OBJECT 14 | 15 | public: 16 | ~StreamStateHandlerInterface() override = default; 17 | 18 | virtual bool endStream() = 0; 19 | virtual enums::StreamState getCurrentState() const = 0; 20 | 21 | signals: 22 | void signalStreamStateChanged(); 23 | }; 24 | } // namespace os 25 | -------------------------------------------------------------------------------- /src/lib/os/include/os/sunshineapps.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | #include 6 | 7 | namespace os 8 | { 9 | class SunshineApps 10 | { 11 | Q_DISABLE_COPY(SunshineApps) 12 | 13 | public: 14 | explicit SunshineApps(QString filepath); 15 | virtual ~SunshineApps() = default; 16 | 17 | std::optional> load(); 18 | 19 | private: 20 | QString m_filepath; 21 | }; 22 | } // namespace os 23 | -------------------------------------------------------------------------------- /src/lib/os/include/os/systemtray.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | // forward declarations 10 | namespace os 11 | { 12 | class PcControl; 13 | } 14 | 15 | namespace os 16 | { 17 | class SystemTray : public QObject 18 | { 19 | Q_OBJECT 20 | Q_DISABLE_COPY(SystemTray) 21 | 22 | public: 23 | explicit SystemTray(const QIcon& icon, QString app_name, PcControl& pc_control); 24 | ~SystemTray() override = default; 25 | 26 | signals: 27 | void signalQuitApp(); 28 | 29 | public slots: 30 | void slotShowTrayMessage(const QString& title, const QString& message, QSystemTrayIcon::MessageIcon icon, 31 | int millisecondsTimeoutHint); 32 | 33 | private slots: 34 | void slotTryAttach(); 35 | 36 | private: 37 | // Note: ctor/dtor order is important! 38 | QAction m_autostart_action; 39 | QAction m_quit_action; 40 | QMenu m_menu; 41 | std::unique_ptr m_tray_icon; 42 | 43 | QTimer m_tray_attach_retry_timer; 44 | uint m_retry_counter{0}; 45 | 46 | const QIcon& m_icon; 47 | QString m_app_name; 48 | PcControl& m_pc_control; 49 | }; 50 | } // namespace os 51 | -------------------------------------------------------------------------------- /src/lib/os/linux/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------------------------------------------------------------------- 2 | # Lib config 3 | #---------------------------------------------------------------------------------------------------------------------- 4 | 5 | set(LIBNAME osspecificlib) 6 | 7 | #---------------------------------------------------------------------------------------------------------------------- 8 | # External dependencies 9 | #---------------------------------------------------------------------------------------------------------------------- 10 | 11 | find_package(Qt6 COMPONENTS Core DBus REQUIRED) 12 | find_library(PROC2_LIBRARY NAMES proc2 REQUIRED) 13 | qt_standard_project_setup() 14 | 15 | #---------------------------------------------------------------------------------------------------------------------- 16 | # Header/Source files 17 | #---------------------------------------------------------------------------------------------------------------------- 18 | 19 | file(GLOB HEADERS CONFIGURE_DEPENDS "include/os/linux/*.h") 20 | file(GLOB SOURCES CONFIGURE_DEPENDS "*.cpp") 21 | 22 | #---------------------------------------------------------------------------------------------------------------------- 23 | # Target config 24 | #---------------------------------------------------------------------------------------------------------------------- 25 | 26 | add_library(${LIBNAME} ${HEADERS} ${SOURCES}) 27 | target_link_libraries(${LIBNAME} PRIVATE Qt6::Core Qt6::DBus ${PROC2_LIBRARY} ossharedlib sharedlib) 28 | target_include_directories(${LIBNAME} PUBLIC include) 29 | -------------------------------------------------------------------------------- /src/lib/os/linux/include/os/linux/nativeautostarthandler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | 6 | // local includes 7 | #include "os/shared/nativeautostarthandlerinterface.h" 8 | 9 | // forward declarations 10 | namespace shared 11 | { 12 | class AppMetadata; 13 | } 14 | 15 | namespace os 16 | { 17 | class NativeAutoStartHandler : public NativeAutoStartHandlerInterface 18 | { 19 | Q_DISABLE_COPY(NativeAutoStartHandler) 20 | 21 | public: 22 | explicit NativeAutoStartHandler(const shared::AppMetadata& app_meta); 23 | ~NativeAutoStartHandler() override = default; 24 | 25 | void setAutoStart(bool enable) override; 26 | bool isAutoStartEnabled() const override; 27 | 28 | private: 29 | const shared::AppMetadata& m_app_meta; 30 | }; 31 | } // namespace os 32 | -------------------------------------------------------------------------------- /src/lib/os/linux/include/os/linux/nativepcstatehandler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | 6 | // local includes 7 | #include "os/shared/nativepcstatehandlerinterface.h" 8 | 9 | namespace os 10 | { 11 | class NativePcStateHandler 12 | : public QObject 13 | , public NativePcStateHandlerInterface 14 | { 15 | Q_OBJECT 16 | Q_DISABLE_COPY(NativePcStateHandler) 17 | 18 | public: 19 | explicit NativePcStateHandler(); 20 | ~NativePcStateHandler() override = default; 21 | 22 | bool canShutdownPC() override; 23 | bool canRestartPC() override; 24 | bool canSuspendPC() override; 25 | bool canHibernatePC() override; 26 | 27 | bool shutdownPC() override; 28 | bool restartPC() override; 29 | bool suspendPC() override; 30 | bool hibernatePC() override; 31 | 32 | private: 33 | QDBusInterface m_logind_bus; 34 | }; 35 | } // namespace os 36 | -------------------------------------------------------------------------------- /src/lib/os/linux/include/os/linux/nativeprocesshandler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // local includes 4 | #include "os/shared/nativeprocesshandlerinterface.h" 5 | 6 | namespace os 7 | { 8 | class NativeProcessHandler : public NativeProcessHandlerInterface 9 | { 10 | Q_DISABLE_COPY(NativeProcessHandler) 11 | 12 | public: 13 | explicit NativeProcessHandler() = default; 14 | ~NativeProcessHandler() override = default; 15 | 16 | std::vector getPids() const override; 17 | QString getExecPath(uint pid) const override; 18 | QDateTime getStartTime(uint pid) const override; 19 | void close(uint pid) const override; 20 | void terminate(uint pid) const override; 21 | }; 22 | } // namespace os 23 | -------------------------------------------------------------------------------- /src/lib/os/linux/include/os/linux/nativesleepinhibitor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | 6 | // local includes 7 | #include "os/shared/nativesleepinhibitorinterface.h" 8 | 9 | namespace os 10 | { 11 | class NativeSleepInhibitor : public NativeSleepInhibitorInterface 12 | { 13 | public: 14 | explicit NativeSleepInhibitor(const QString& app_name); 15 | ~NativeSleepInhibitor() override = default; 16 | 17 | private: 18 | QDBusUnixFileDescriptor m_file_descriptor; 19 | }; 20 | } // namespace os 21 | -------------------------------------------------------------------------------- /src/lib/os/linux/nativeautostarthandler.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/linux/nativeautostarthandler.h" 3 | 4 | // system/Qt includes 5 | #include 6 | 7 | // local includes 8 | #include "shared/appmetadata.h" 9 | 10 | namespace 11 | { 12 | QString getAutoStartContents(const shared::AppMetadata& app_meta) 13 | { 14 | QString contents; 15 | QTextStream stream(&contents); 16 | 17 | stream << "[Desktop Entry]" << Qt::endl; 18 | stream << "Type=Application" << Qt::endl; 19 | stream << "Name=" << app_meta.getAppName() << Qt::endl; 20 | stream << "Exec=" << app_meta.getAutoStartExec() << Qt::endl; 21 | stream << "Icon=" << app_meta.getAppName() << Qt::endl; 22 | 23 | return contents; 24 | } 25 | } // namespace 26 | 27 | namespace os 28 | { 29 | NativeAutoStartHandler::NativeAutoStartHandler(const shared::AppMetadata& app_meta) 30 | : m_app_meta{app_meta} 31 | { 32 | } 33 | 34 | void NativeAutoStartHandler::setAutoStart(bool enable) 35 | { 36 | const auto dir{m_app_meta.getAutoStartDir()}; 37 | QFile file{m_app_meta.getAutoStartPath()}; 38 | 39 | if (file.exists() && !file.remove()) 40 | { 41 | qFatal("Failed to remove %s", qUtf8Printable(m_app_meta.getAutoStartPath())); 42 | return; 43 | } 44 | 45 | if (enable) 46 | { 47 | const QDir autostart_dir; 48 | if (!autostart_dir.mkpath(dir)) 49 | { 50 | qFatal("Failed at mkpath %s", qUtf8Printable(dir)); 51 | return; 52 | } 53 | 54 | if (!file.open(QIODevice::WriteOnly)) 55 | { 56 | qFatal("Failed to open %s", qUtf8Printable(m_app_meta.getAutoStartPath())); 57 | return; 58 | } 59 | 60 | file.write(getAutoStartContents(m_app_meta).toUtf8()); 61 | } 62 | } 63 | 64 | bool NativeAutoStartHandler::isAutoStartEnabled() const 65 | { 66 | QFile file{m_app_meta.getAutoStartPath()}; 67 | 68 | if (!file.exists()) 69 | { 70 | return false; 71 | } 72 | 73 | if (!file.open(QIODevice::ReadOnly)) 74 | { 75 | qFatal("Failed to open %s", qUtf8Printable(m_app_meta.getAutoStartPath())); 76 | return false; 77 | } 78 | 79 | return file.readAll() == getAutoStartContents(m_app_meta); 80 | } 81 | } // namespace os 82 | -------------------------------------------------------------------------------- /src/lib/os/linux/nativepcstatehandler.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/linux/nativepcstatehandler.h" 3 | 4 | // system/Qt includes 5 | #include 6 | 7 | // local includes 8 | #include "shared/loggingcategories.h" 9 | 10 | namespace 11 | { 12 | // NOLINTNEXTLINE(*-swappable-parameters) 13 | bool canDoQuery(QDBusInterface& bus, const QString& log_entry, const QString& query) 14 | { 15 | if (!bus.isValid()) 16 | { 17 | return false; 18 | } 19 | 20 | const QString actual_query{QStringLiteral("Can") + query}; 21 | const QDBusReply reply{bus.call(QDBus::Block, actual_query)}; 22 | if (!reply.isValid()) 23 | { 24 | qCWarning(lc::os).nospace() << "got invalid reply for " << log_entry << " (" << actual_query 25 | << "): " << reply.error(); 26 | return false; 27 | } 28 | 29 | if (reply.value() != QStringLiteral("yes")) 30 | { 31 | qCWarning(lc::os).nospace() << "got unexpected reply for " << log_entry << " (" << actual_query 32 | << "): " << reply.value(); 33 | return false; 34 | } 35 | 36 | return true; 37 | } 38 | 39 | bool doQuery(QDBusInterface& bus, const QString& log_entry, const QString& query) 40 | { 41 | if (!canDoQuery(bus, log_entry, query)) 42 | { 43 | return false; 44 | } 45 | 46 | const bool polkit_interactive{true}; 47 | const QDBusReply reply{bus.call(QDBus::Block, query, polkit_interactive)}; 48 | if (!reply.isValid()) 49 | { 50 | qCWarning(lc::os).nospace() << "got invalid reply for " << log_entry << " (" << query << "): " << reply.error(); 51 | return false; 52 | } 53 | 54 | return true; 55 | } 56 | } // namespace 57 | 58 | namespace os 59 | { 60 | NativePcStateHandler::NativePcStateHandler() 61 | : m_logind_bus{"org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", 62 | QDBusConnection::systemBus()} 63 | { 64 | if (!m_logind_bus.isValid()) 65 | { 66 | qCWarning(lc::os) << "logind bus is invalid!"; 67 | } 68 | } 69 | 70 | bool NativePcStateHandler::canShutdownPC() 71 | { 72 | return canDoQuery(m_logind_bus, "shutdown", "PowerOff"); 73 | } 74 | 75 | bool NativePcStateHandler::canRestartPC() 76 | { 77 | return canDoQuery(m_logind_bus, "restart", "Reboot"); 78 | } 79 | 80 | bool NativePcStateHandler::canSuspendPC() 81 | { 82 | return canDoQuery(m_logind_bus, "suspend", "Suspend"); 83 | } 84 | 85 | bool NativePcStateHandler::canHibernatePC() 86 | { 87 | return canDoQuery(m_logind_bus, "hibernate", "Hibernate"); 88 | } 89 | 90 | bool NativePcStateHandler::shutdownPC() 91 | { 92 | return doQuery(m_logind_bus, "shutdown", "PowerOff"); 93 | } 94 | 95 | bool NativePcStateHandler::restartPC() 96 | { 97 | return doQuery(m_logind_bus, "restart", "Reboot"); 98 | } 99 | 100 | bool NativePcStateHandler::suspendPC() 101 | { 102 | return doQuery(m_logind_bus, "suspend", "Suspend"); 103 | } 104 | 105 | bool NativePcStateHandler::hibernatePC() 106 | { 107 | return doQuery(m_logind_bus, "hibernate", "Hibernate"); 108 | } 109 | } // namespace os 110 | -------------------------------------------------------------------------------- /src/lib/os/linux/nativeprocesshandler.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/linux/nativeprocesshandler.h" 3 | 4 | // system/Qt includes 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | // local includes 15 | #include "shared/loggingcategories.h" 16 | 17 | namespace 18 | { 19 | std::optional getBootTime() 20 | { 21 | static unsigned int boot_time{0}; 22 | if (boot_time == 0) 23 | { 24 | stat_info* stat_info{nullptr}; 25 | if (const int error = procps_stat_new(&stat_info); error < 0) 26 | { 27 | qWarning(lc::os) << "Failed at procps_stat_new: " << lc::getErrorString(error * -1); 28 | return std::nullopt; 29 | } 30 | 31 | boot_time = STAT_GET(stat_info, STAT_SYS_TIME_OF_BOOT, ul_int); 32 | procps_stat_unref(&stat_info); 33 | } 34 | return boot_time; 35 | } 36 | 37 | template 38 | ItemValue getPidItem(const uint pid, const pids_item item, const ItemValue& fallback, Getter&& getter) 39 | { 40 | std::array items{item}; 41 | 42 | pids_info* info{nullptr}; 43 | if (const int error = procps_pids_new(&info, items.data(), items.size()); error < 0) 44 | { 45 | qWarning(lc::os) << "Failed at procps_pids_new for" << pid << "-" << lc::getErrorString(error * -1); 46 | return fallback; 47 | } 48 | 49 | const auto cleanup{qScopeGuard([&]() { procps_pids_unref(&info); })}; 50 | 51 | std::array pids{pid}; 52 | const auto* result{procps_pids_select(info, pids.data(), pids.size(), PIDS_SELECT_PID)}; 53 | 54 | if (!result) 55 | { 56 | qWarning(lc::os) << "Failed at procps_pids_select for" << pid << "-" << lc::getErrorString(errno); 57 | return fallback; 58 | } 59 | 60 | if (!result->counts || result->counts->total <= 0) 61 | { 62 | return fallback; 63 | } 64 | 65 | const auto* head_ptr{result->stacks && result->stacks[0]->head ? result->stacks[0]->head : nullptr}; 66 | if (!head_ptr) 67 | { 68 | qWarning(lc::os) << "Failed at procps_pids_select for" << pid << "-" << lc::getErrorString(errno); 69 | return fallback; 70 | } 71 | 72 | return std::forward(getter)(head_ptr->result); 73 | } 74 | 75 | uint getParentPid(const uint pid) 76 | { 77 | return getPidItem(pid, PIDS_ID_PPID, 0u, 78 | [](const auto& result) { return result.s_int >= 0 ? static_cast(result.s_int) : 0u; }); 79 | } 80 | 81 | QDateTime getStartTime(const uint pid) 82 | { 83 | return getPidItem(pid, PIDS_TIME_START, QDateTime{}, 84 | [](const auto& result) 85 | { 86 | const auto boot_time{getBootTime()}; 87 | if (!boot_time) 88 | { 89 | return QDateTime{}; 90 | } 91 | 92 | const auto milliseconds{static_cast(std::round((result.real) * 1000.0))}; 93 | const auto datetime{QDateTime::fromSecsSinceEpoch(*boot_time)}; 94 | return datetime.addMSecs(milliseconds); 95 | }); 96 | } 97 | 98 | std::vector getPids() 99 | { 100 | const QDir proc_dir{"/proc"}; 101 | const auto dirs{proc_dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot)}; 102 | std::vector pids; 103 | 104 | pids.reserve(dirs.size()); 105 | 106 | for (const auto& dir : dirs) 107 | { 108 | bool converted{false}; 109 | const uint pid = dir.toUInt(&converted); 110 | 111 | if (!converted) 112 | { 113 | continue; 114 | } 115 | 116 | pids.push_back(pid); 117 | } 118 | 119 | return pids; 120 | } 121 | 122 | std::vector getParentPids(const std::vector& pids) 123 | { 124 | std::vector parent_pids; 125 | std::transform(std::cbegin(pids), std::cend(pids), std::back_inserter(parent_pids), getParentPid); 126 | return parent_pids; 127 | } 128 | 129 | std::vector getRelatedPids(uint pid) 130 | { 131 | const std::vector all_pids{getPids()}; 132 | const std::vector parent_pids{getParentPids(all_pids)}; 133 | 134 | Q_ASSERT(all_pids.size() == parent_pids.size()); 135 | std::vector related_pids; 136 | 137 | // Start searching for children from current pid 138 | related_pids.push_back(pid); 139 | 140 | for (std::size_t i = 0; i < related_pids.size(); ++i) 141 | { 142 | const uint related_pid{related_pids[i]}; 143 | for (std::size_t k = 0; k < parent_pids.size(); ++k) 144 | { 145 | const uint parent_pid{parent_pids[k]}; 146 | if (related_pid != parent_pid) 147 | { 148 | continue; 149 | } 150 | 151 | const uint process_pid{all_pids[k]}; 152 | if (process_pid == related_pid) 153 | { 154 | // Is this even possible? To be your own parent? 155 | continue; 156 | } 157 | 158 | related_pids.push_back(process_pid); 159 | } 160 | } 161 | 162 | return related_pids; 163 | } 164 | } // namespace 165 | 166 | namespace os 167 | { 168 | std::vector NativeProcessHandler::getPids() const 169 | { 170 | return ::getPids(); 171 | } 172 | 173 | QString NativeProcessHandler::getExecPath(uint pid) const 174 | { 175 | const QFileInfo info{"/proc/" + QString::number(pid) + "/exe"}; 176 | return QFileInfo{info.symLinkTarget()}.canonicalFilePath(); 177 | } 178 | 179 | QDateTime NativeProcessHandler::getStartTime(uint pid) const 180 | { 181 | return ::getStartTime(pid); 182 | } 183 | 184 | void NativeProcessHandler::close(uint pid) const 185 | { 186 | const auto related_pids{getRelatedPids(pid)}; 187 | for (const auto related_pid : related_pids) 188 | { 189 | if (kill(static_cast(related_pid), SIGTERM) < 0) 190 | { 191 | const auto error{errno}; 192 | if (error != ESRCH) 193 | { 194 | qWarning(lc::os) << "Failed to close process" << related_pid << "-" << lc::getErrorString(error); 195 | } 196 | } 197 | } 198 | } 199 | 200 | void NativeProcessHandler::terminate(uint pid) const 201 | { 202 | const auto related_pids{getRelatedPids(pid)}; 203 | for (const auto related_pid : related_pids) 204 | { 205 | if (kill(static_cast(related_pid), SIGKILL) < 0) 206 | { 207 | const auto error{errno}; 208 | if (error != ESRCH) 209 | { 210 | qWarning(lc::os) << "Failed to terminate process" << related_pid << "-" << lc::getErrorString(error); 211 | } 212 | } 213 | } 214 | } 215 | } // namespace os 216 | -------------------------------------------------------------------------------- /src/lib/os/linux/nativesleepinhibitor.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/linux/nativesleepinhibitor.h" 3 | 4 | // system/Qt includes 5 | #include 6 | #include 7 | 8 | // local includes 9 | #include "shared/loggingcategories.h" 10 | 11 | namespace os 12 | { 13 | NativeSleepInhibitor::NativeSleepInhibitor(const QString& app_name) 14 | { 15 | QDBusInterface manager_bus{"org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", 16 | QDBusConnection::systemBus()}; 17 | if (!manager_bus.isValid()) 18 | { 19 | qCWarning(lc::os) << "Could not create QDBusInterface instance for inhibiting sleep!"; 20 | return; 21 | } 22 | 23 | const auto method{QStringLiteral("Inhibit")}; 24 | const auto what{QStringLiteral("idle:sleep")}; 25 | const auto who{app_name}; 26 | const auto why{QStringLiteral("Gaming Session")}; 27 | const auto mode{QStringLiteral("block")}; 28 | 29 | const QDBusReply reply{manager_bus.call(QDBus::Block, method, what, who, why, mode)}; 30 | if (!reply.isValid()) 31 | { 32 | qCWarning(lc::os).nospace() << "got invalid reply for " << method << " request: " << reply.error(); 33 | return; 34 | } 35 | 36 | m_file_descriptor = reply.value(); 37 | } 38 | } // namespace os 39 | -------------------------------------------------------------------------------- /src/lib/os/networkinfo.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/networkinfo.h" 3 | 4 | // system/Qt includes 5 | #if defined(Q_OS_WIN) 6 | // clang-format off 7 | #include 8 | #include 9 | #include 10 | // clang-format on 11 | #include 12 | #endif 13 | #include 14 | 15 | // local includes 16 | #include "shared/loggingcategories.h" 17 | 18 | namespace 19 | { 20 | QString getMacAddressGeneric(const QHostAddress& host_address) 21 | { 22 | static const QSet allowed_types{QNetworkInterface::Ethernet, QNetworkInterface::Wifi}; 23 | 24 | for (const QNetworkInterface& iface : QNetworkInterface::allInterfaces()) 25 | { 26 | if (allowed_types.contains(iface.type()) && iface.flags().testFlag(QNetworkInterface::IsRunning)) 27 | { 28 | for (const QHostAddress& address_entry : QNetworkInterface::allAddresses()) 29 | { 30 | if (address_entry.isEqual(host_address)) 31 | { 32 | return iface.hardwareAddress(); 33 | } 34 | } 35 | } 36 | } 37 | 38 | return {}; 39 | } 40 | } // namespace 41 | 42 | namespace os 43 | { 44 | QString NetworkInfo::getMacAddress(const QHostAddress& host_address) 45 | { 46 | #if defined(Q_OS_WIN) 47 | ULONG buffer_size{0}; 48 | std::vector buffer; 49 | PIP_ADAPTER_ADDRESSES addresses{nullptr}; 50 | 51 | ULONG result = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, nullptr, addresses, &buffer_size); 52 | if (result == ERROR_BUFFER_OVERFLOW) 53 | { 54 | buffer.resize(buffer_size); 55 | addresses = reinterpret_cast(buffer.data()); 56 | result = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, nullptr, addresses, &buffer_size); 57 | } 58 | 59 | if (result != NO_ERROR) 60 | { 61 | qCWarning(lc::os) << "GetAdaptersAddresses failed with error (using fallback):" << lc::getErrorString(result); 62 | return getMacAddressGeneric(host_address); 63 | } 64 | 65 | while (addresses != nullptr) 66 | { 67 | for (auto* unicast_address = addresses->FirstUnicastAddress; unicast_address != nullptr; 68 | unicast_address = unicast_address->Next) 69 | { 70 | if (getnameinfo(unicast_address->Address.lpSockaddr, unicast_address->Address.iSockaddrLength, nullptr, 0, 71 | nullptr, 0, NI_NUMERICHOST) 72 | != 0) 73 | { 74 | qCWarning(lc::os) << "WSAGetLastError failed with error:" << lc::getErrorString(WSAGetLastError()); 75 | return {}; 76 | } 77 | 78 | const QHostAddress address_entry(unicast_address->Address.lpSockaddr); 79 | if (host_address.isEqual(address_entry)) 80 | { 81 | if (addresses->PhysicalAddressLength > 0) 82 | { 83 | QString mac_address = QString("%1:%2:%3:%4:%5:%6") 84 | .arg(addresses->PhysicalAddress[0], 2, 16, QLatin1Char('0')) 85 | .arg(addresses->PhysicalAddress[1], 2, 16, QLatin1Char('0')) 86 | .arg(addresses->PhysicalAddress[2], 2, 16, QLatin1Char('0')) 87 | .arg(addresses->PhysicalAddress[3], 2, 16, QLatin1Char('0')) 88 | .arg(addresses->PhysicalAddress[4], 2, 16, QLatin1Char('0')) 89 | .arg(addresses->PhysicalAddress[5], 2, 16, QLatin1Char('0')) 90 | .toUpper(); 91 | 92 | return mac_address; 93 | } 94 | 95 | qCDebug(lc::os) << "Using fallback MAC detection, because no physical address found for" 96 | << address_entry; 97 | return getMacAddressGeneric(address_entry); 98 | } 99 | } 100 | addresses = addresses->Next; 101 | } 102 | #endif 103 | return getMacAddressGeneric(host_address); 104 | } 105 | } // namespace os 106 | -------------------------------------------------------------------------------- /src/lib/os/pccontrol.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/pccontrol.h" 3 | 4 | // os-specific includes 5 | #if defined(Q_OS_WIN) 6 | #include "os/win/nativepcstatehandler.h" 7 | #include "os/win/nativeprocesshandler.h" 8 | #elif defined(Q_OS_LINUX) 9 | #include "os/linux/nativepcstatehandler.h" 10 | #include "os/linux/nativeprocesshandler.h" 11 | #else 12 | #error OS is not supported! 13 | #endif 14 | 15 | // local includes 16 | #include "os/pcstatehandler.h" 17 | #include "os/steam/steamprocesstracker.h" 18 | #include "os/streamstatehandler.h" 19 | #include "shared/loggingcategories.h" 20 | #include "utils/appsettings.h" 21 | 22 | namespace 23 | { 24 | constexpr int DEFAULT_TIMEOUT_S{10}; 25 | constexpr int DEFAULT_TIMEOUT_MS{DEFAULT_TIMEOUT_S * 1000}; 26 | } // namespace 27 | 28 | namespace os 29 | { 30 | PcControl::PcControl(const utils::AppSettings& app_settings) 31 | : m_app_settings{app_settings} 32 | , m_auto_start_handler{m_app_settings.getAppMetadata()} 33 | , m_pc_state_handler{std::make_unique()} 34 | , m_steam_handler{m_app_settings, std::make_unique()} 35 | , m_stream_state_handler{std::make_unique( 36 | m_app_settings.getAppMetadata().getAppName(shared::AppMetadata::App::Stream))} 37 | { 38 | Q_ASSERT(m_stream_state_handler != nullptr); 39 | 40 | connect(&m_steam_handler, &SteamHandler::signalSteamClosed, this, &PcControl::slotHandleSteamClosed); 41 | connect(m_stream_state_handler.get(), &StreamStateHandler::signalStreamStateChanged, this, 42 | &PcControl::slotHandleStreamStateChange); 43 | } 44 | 45 | // For forward declarations 46 | PcControl::~PcControl() = default; 47 | 48 | bool PcControl::launchSteam(const bool big_picture_mode) 49 | { 50 | return m_steam_handler.launchSteam(big_picture_mode); 51 | } 52 | 53 | enums::SteamUiMode PcControl::getSteamUiMode() const 54 | { 55 | return m_steam_handler.getSteamUiMode(); 56 | } 57 | 58 | bool PcControl::closeSteam() 59 | { 60 | return m_steam_handler.close(); 61 | } 62 | 63 | bool PcControl::launchSteamApp(std::uint64_t app_id) 64 | { 65 | return m_steam_handler.launchApp(app_id); 66 | } 67 | 68 | std::optional> PcControl::getAppData() const 69 | { 70 | return m_steam_handler.getAppData(); 71 | } 72 | 73 | std::optional> PcControl::getNonSteamAppData(const std::uint64_t user_id) const 74 | { 75 | return m_steam_handler.getNonSteamAppData(user_id); 76 | } 77 | 78 | bool PcControl::shutdownPC() 79 | { 80 | if (m_pc_state_handler.shutdownPC(DEFAULT_TIMEOUT_S)) 81 | { 82 | closeSteam(); 83 | endStream(); 84 | emit signalShowTrayMessage("Shutdown in progress", 85 | m_app_settings.getAppMetadata().getAppName() + " is putting you to sleep :)", 86 | QSystemTrayIcon::MessageIcon::Information, DEFAULT_TIMEOUT_MS); 87 | return true; 88 | } 89 | 90 | return false; 91 | } 92 | 93 | bool PcControl::restartPC() 94 | { 95 | if (m_pc_state_handler.restartPC(DEFAULT_TIMEOUT_S)) 96 | { 97 | closeSteam(); 98 | endStream(); 99 | emit signalShowTrayMessage("Restart in progress", 100 | m_app_settings.getAppMetadata().getAppName() + " is giving you new life :?", 101 | QSystemTrayIcon::MessageIcon::Information, DEFAULT_TIMEOUT_MS); 102 | return true; 103 | } 104 | 105 | return false; 106 | } 107 | 108 | bool PcControl::suspendOrHibernatePC() 109 | { 110 | const bool hibernation{m_app_settings.getPreferHibernation()}; 111 | const bool result{hibernation ? m_pc_state_handler.hibernatePC(DEFAULT_TIMEOUT_S) 112 | : m_pc_state_handler.suspendPC(DEFAULT_TIMEOUT_S)}; 113 | if (result) 114 | { 115 | if (m_app_settings.getCloseSteamBeforeSleep()) 116 | { 117 | closeSteam(); 118 | } 119 | endStream(); 120 | 121 | emit signalShowTrayMessage( 122 | hibernation ? "Hibernation in progress" : "Suspend in progress", 123 | m_app_settings.getAppMetadata().getAppName() 124 | + (hibernation ? " is about to put you into hard sleep :O" : " is about to suspend you real hard :P"), 125 | QSystemTrayIcon::MessageIcon::Information, DEFAULT_TIMEOUT_MS); 126 | return true; 127 | } 128 | 129 | return false; 130 | } 131 | 132 | bool PcControl::endStream() 133 | { 134 | return m_stream_state_handler->endStream(); 135 | } 136 | 137 | enums::StreamState PcControl::getStreamState() const 138 | { 139 | return m_stream_state_handler->getCurrentState(); 140 | } 141 | 142 | enums::PcState PcControl::getPcState() const 143 | { 144 | return m_pc_state_handler.getState(); 145 | } 146 | 147 | void PcControl::setAutoStart(bool enable) 148 | { 149 | m_auto_start_handler.setAutoStart(enable); 150 | } 151 | 152 | bool PcControl::isAutoStartEnabled() const 153 | { 154 | return m_auto_start_handler.isAutoStartEnabled(); 155 | } 156 | 157 | void PcControl::slotHandleSteamClosed() 158 | { 159 | endStream(); 160 | } 161 | 162 | void PcControl::slotHandleStreamStateChange() 163 | { 164 | switch (m_stream_state_handler->getCurrentState()) 165 | { 166 | case enums::StreamState::NotStreaming: 167 | { 168 | qCInfo(lc::os) << "Stream has ended."; 169 | break; 170 | } 171 | case enums::StreamState::Streaming: 172 | { 173 | qCInfo(lc::os) << "Stream started."; 174 | break; 175 | } 176 | case enums::StreamState::StreamEnding: 177 | { 178 | qCInfo(lc::os) << "Stream is ending."; 179 | m_steam_handler.clearSessionData(); 180 | break; 181 | } 182 | } 183 | } 184 | } // namespace os 185 | -------------------------------------------------------------------------------- /src/lib/os/pcstatehandler.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/pcstatehandler.h" 3 | 4 | // system/Qt includes 5 | #include 6 | 7 | // local includes 8 | #include "os/shared/nativepcstatehandlerinterface.h" 9 | #include "shared/loggingcategories.h" 10 | 11 | namespace 12 | { 13 | const int SEC_TO_MS{1000}; 14 | 15 | int getTimeoutTime(uint grace_period_in_sec) 16 | { 17 | return static_cast(grace_period_in_sec) * SEC_TO_MS; 18 | } 19 | } // namespace 20 | 21 | namespace os 22 | { 23 | PcStateHandler::PcStateHandler(std::unique_ptr native_handler) 24 | : m_native_handler{std::move(native_handler)} 25 | { 26 | Q_ASSERT(m_native_handler != nullptr); 27 | } 28 | 29 | PcStateHandler::~PcStateHandler() = default; 30 | 31 | enums::PcState PcStateHandler::getState() const 32 | { 33 | return m_state; 34 | } 35 | 36 | bool PcStateHandler::shutdownPC(uint grace_period_in_sec) 37 | { 38 | return doChangeState(grace_period_in_sec, "shut down", "shutdown", &NativePcStateHandlerInterface::canShutdownPC, 39 | &NativePcStateHandlerInterface::shutdownPC, enums::PcState::ShuttingDown); 40 | } 41 | 42 | bool PcStateHandler::restartPC(uint grace_period_in_sec) 43 | { 44 | return doChangeState(grace_period_in_sec, "restarted", "restart", &NativePcStateHandlerInterface::canRestartPC, 45 | &NativePcStateHandlerInterface::restartPC, enums::PcState::Restarting); 46 | } 47 | 48 | bool PcStateHandler::suspendPC(uint grace_period_in_sec) 49 | { 50 | return doChangeState(grace_period_in_sec, "suspended", "suspend", &NativePcStateHandlerInterface::canSuspendPC, 51 | &NativePcStateHandlerInterface::suspendPC, enums::PcState::Suspending); 52 | } 53 | 54 | bool PcStateHandler::hibernatePC(uint grace_period_in_sec) 55 | { 56 | return doChangeState(grace_period_in_sec, "hibernated", "hibernate", &NativePcStateHandlerInterface::canHibernatePC, 57 | &NativePcStateHandlerInterface::hibernatePC, enums::PcState::Suspending); 58 | } 59 | 60 | bool PcStateHandler::doChangeState(uint grace_period_in_sec, const QString& cant_do_entry, 61 | // NOLINTNEXTLINE(*-swappable-parameters) 62 | const QString& failed_to_do_entry, NativeMethod can_do_method, 63 | NativeMethod do_method, enums::PcState new_state) 64 | { 65 | if (m_state != enums::PcState::Normal) 66 | { 67 | qCDebug(lc::os) << "PC is already changing state. Aborting request."; 68 | return false; 69 | } 70 | 71 | if (!(m_native_handler.get()->*can_do_method)()) 72 | { 73 | qCWarning(lc::os).nospace() << "PC cannot be " << cant_do_entry << "!"; 74 | return false; 75 | } 76 | 77 | QTimer::singleShot(getTimeoutTime(grace_period_in_sec), this, 78 | [this, failed_to_do_entry, do_method]() 79 | { 80 | qCInfo(lc::os) << "Resetting PC state back to normal."; 81 | m_state = enums::PcState::Normal; 82 | 83 | if (!(m_native_handler.get()->*do_method)()) 84 | { 85 | qCWarning(lc::os).nospace() << "Failed to " << failed_to_do_entry << " PC!"; 86 | } 87 | }); 88 | 89 | m_state = new_state; 90 | return true; 91 | } 92 | } // namespace os 93 | -------------------------------------------------------------------------------- /src/lib/os/shared/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------------------------------------------------------------------- 2 | # Lib config 3 | #---------------------------------------------------------------------------------------------------------------------- 4 | 5 | set(LIBNAME ossharedlib) 6 | 7 | #---------------------------------------------------------------------------------------------------------------------- 8 | # External dependencies 9 | #---------------------------------------------------------------------------------------------------------------------- 10 | 11 | find_package(Qt6 COMPONENTS Core REQUIRED) 12 | qt_standard_project_setup() 13 | 14 | #---------------------------------------------------------------------------------------------------------------------- 15 | # Header/Source files 16 | #---------------------------------------------------------------------------------------------------------------------- 17 | 18 | file(GLOB HEADERS CONFIGURE_DEPENDS "include/os/shared/*.h") 19 | file(GLOB SOURCES CONFIGURE_DEPENDS "*.cpp") 20 | 21 | #---------------------------------------------------------------------------------------------------------------------- 22 | # Target config 23 | #---------------------------------------------------------------------------------------------------------------------- 24 | 25 | add_library(${LIBNAME} ${HEADERS} ${SOURCES}) 26 | target_link_libraries(${LIBNAME} PRIVATE Qt6::Core sharedlib) 27 | target_include_directories(${LIBNAME} PUBLIC include) 28 | -------------------------------------------------------------------------------- /src/lib/os/shared/include/os/shared/nativeautostarthandlerinterface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace os 4 | { 5 | class NativeAutoStartHandlerInterface 6 | { 7 | public: 8 | virtual ~NativeAutoStartHandlerInterface() = default; 9 | 10 | virtual void setAutoStart(bool enable) = 0; 11 | virtual bool isAutoStartEnabled() const = 0; 12 | }; 13 | } // namespace os 14 | -------------------------------------------------------------------------------- /src/lib/os/shared/include/os/shared/nativepcstatehandlerinterface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace os 4 | { 5 | class NativePcStateHandlerInterface 6 | { 7 | public: 8 | virtual ~NativePcStateHandlerInterface() = default; 9 | 10 | virtual bool canShutdownPC() = 0; 11 | virtual bool canRestartPC() = 0; 12 | virtual bool canSuspendPC() = 0; 13 | virtual bool canHibernatePC() = 0; 14 | 15 | virtual bool shutdownPC() = 0; 16 | virtual bool restartPC() = 0; 17 | virtual bool suspendPC() = 0; 18 | virtual bool hibernatePC() = 0; 19 | }; 20 | } // namespace os 21 | -------------------------------------------------------------------------------- /src/lib/os/shared/include/os/shared/nativeprocesshandlerinterface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | #include 6 | 7 | namespace os 8 | { 9 | class NativeProcessHandlerInterface 10 | { 11 | public: 12 | virtual ~NativeProcessHandlerInterface() = default; 13 | 14 | virtual std::vector getPids() const = 0; 15 | virtual QString getExecPath(uint pid) const = 0; 16 | virtual QDateTime getStartTime(uint pid) const = 0; 17 | virtual void close(uint pid) const = 0; 18 | virtual void terminate(uint pid) const = 0; 19 | }; 20 | } // namespace os 21 | -------------------------------------------------------------------------------- /src/lib/os/shared/include/os/shared/nativesleepinhibitorinterface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace os 4 | { 5 | class NativeSleepInhibitorInterface 6 | { 7 | public: 8 | virtual ~NativeSleepInhibitorInterface() = default; 9 | }; 10 | } // namespace os 11 | -------------------------------------------------------------------------------- /src/lib/os/sleepinhibitor.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/sleepinhibitor.h" 3 | 4 | // system/Qt includes 5 | #include 6 | 7 | // os-specific includes 8 | #if defined(Q_OS_WIN) 9 | #include "os/win/nativesleepinhibitor.h" 10 | #elif defined(Q_OS_LINUX) 11 | #include "os/linux/nativesleepinhibitor.h" 12 | #else 13 | #error OS is not supported! 14 | #endif 15 | 16 | namespace os 17 | { 18 | SleepInhibitor::SleepInhibitor(const QString& app_name) 19 | : m_impl{std::make_unique(app_name)} 20 | { 21 | } 22 | 23 | // For forward declarations 24 | SleepInhibitor::~SleepInhibitor() = default; 25 | } // namespace os 26 | -------------------------------------------------------------------------------- /src/lib/os/steam/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------------------------------------------------------------------- 2 | # Lib config 3 | #---------------------------------------------------------------------------------------------------------------------- 4 | 5 | set(LIBNAME ossteamlib) 6 | 7 | #---------------------------------------------------------------------------------------------------------------------- 8 | # External dependencies 9 | #---------------------------------------------------------------------------------------------------------------------- 10 | 11 | find_package(Qt6 COMPONENTS Core REQUIRED) 12 | qt_standard_project_setup() 13 | 14 | #---------------------------------------------------------------------------------------------------------------------- 15 | # Header/Source files 16 | #---------------------------------------------------------------------------------------------------------------------- 17 | 18 | file(GLOB HEADERS CONFIGURE_DEPENDS "include/os/steam/*.h") 19 | file(GLOB SOURCES CONFIGURE_DEPENDS "*.cpp") 20 | 21 | #---------------------------------------------------------------------------------------------------------------------- 22 | # Target config 23 | #---------------------------------------------------------------------------------------------------------------------- 24 | 25 | add_library(${LIBNAME} ${HEADERS} ${SOURCES}) 26 | target_link_libraries(${LIBNAME} PRIVATE Qt6::Core sharedlib ossharedlib) 27 | target_include_directories(${LIBNAME} PUBLIC include) 28 | -------------------------------------------------------------------------------- /src/lib/os/steam/include/os/steam/steamappwatcher.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | 6 | // local includes 7 | #include "shared/enums.h" 8 | 9 | // forward declarations 10 | namespace os 11 | { 12 | class SteamProcessTracker; 13 | } // namespace os 14 | 15 | namespace os 16 | { 17 | class SteamAppWatcher : public QObject 18 | { 19 | Q_OBJECT 20 | 21 | public: 22 | explicit SteamAppWatcher(const SteamProcessTracker& process_tracker, std::uint64_t app_id); 23 | ~SteamAppWatcher() override; 24 | 25 | static enums::AppState getAppState(const SteamProcessTracker& process_tracker, std::uint64_t app_id, 26 | enums::AppState prev_state = enums::AppState::Stopped); 27 | 28 | enums::AppState getAppState() const; 29 | std::uint64_t getAppId() const; 30 | 31 | private slots: 32 | void slotCheckState(); 33 | 34 | private: 35 | const SteamProcessTracker& m_process_tracker; 36 | std::uint64_t m_app_id; 37 | 38 | enums::AppState m_current_state{enums::AppState::Stopped}; 39 | QTimer m_check_timer; 40 | uint m_delay_counter{0}; 41 | }; 42 | } // namespace os 43 | -------------------------------------------------------------------------------- /src/lib/os/steam/include/os/steam/steamcontentlogtracker.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // local includes 4 | #include "steamlogtracker.h" 5 | 6 | namespace os 7 | { 8 | class SteamContentLogTracker : public SteamLogTracker 9 | { 10 | Q_OBJECT 11 | 12 | public: 13 | enum class AppState 14 | { 15 | Stopped, 16 | Running, 17 | Updating 18 | }; 19 | Q_ENUM(AppState) 20 | 21 | enum class AppStateChange 22 | { 23 | UpdateRequired, 24 | UpdateQueued, 25 | UpdateRunning, 26 | UpdateStarted, 27 | UpdateOptional, 28 | FullyInstalled, 29 | AppRunning, 30 | FilesMissing, 31 | Uninstalling, 32 | Uninstalled, 33 | ComponentInUse, 34 | Terminating, 35 | PrefetchingInfo 36 | }; 37 | Q_ENUM(AppStateChange) 38 | 39 | explicit SteamContentLogTracker(const std::filesystem::path& logs_dir, QDateTime first_entry_time_filter); 40 | ~SteamContentLogTracker() override = default; 41 | 42 | AppState getAppState(std::uint64_t app_id) const; 43 | 44 | protected: 45 | void onLogChanged(const std::vector& new_lines) override; 46 | 47 | private: 48 | std::map m_app_states; 49 | }; 50 | } // namespace os 51 | -------------------------------------------------------------------------------- /src/lib/os/steam/include/os/steam/steamgameprocesslogtracker.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | 6 | // local includes 7 | #include "steamlogtracker.h" 8 | 9 | namespace os 10 | { 11 | class SteamGameProcessLogTracker : public SteamLogTracker 12 | { 13 | Q_OBJECT 14 | 15 | public: 16 | explicit SteamGameProcessLogTracker(const std::filesystem::path& logs_dir, QDateTime first_entry_time_filter); 17 | ~SteamGameProcessLogTracker() override = default; 18 | 19 | bool isAnyProcessRunning(std::uint64_t app_id) const; 20 | 21 | protected: 22 | void onLogChanged(const std::vector& new_lines) override; 23 | 24 | private: 25 | std::map> m_app_id_to_process_ids; 26 | }; 27 | } // namespace os 28 | -------------------------------------------------------------------------------- /src/lib/os/steam/include/os/steam/steamlogtracker.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | #include 6 | #include 7 | 8 | namespace os 9 | { 10 | class SteamLogTracker : public QObject 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | enum class TimeFormat 16 | { 17 | YYYY_MM_DD_hh_mm_ss, 18 | }; 19 | Q_ENUM(TimeFormat) 20 | 21 | explicit SteamLogTracker(std::filesystem::path main_filename, std::filesystem::path backup_filename, 22 | QDateTime first_entry_time_filter, 23 | TimeFormat time_format = TimeFormat::YYYY_MM_DD_hh_mm_ss); 24 | ~SteamLogTracker() override = default; 25 | 26 | public slots: 27 | void slotCheckLog(); 28 | 29 | protected: 30 | virtual void onLogChanged(const std::vector& new_lines) = 0; 31 | static std::uint64_t appIdFromString(const QString& app_id); 32 | 33 | private: 34 | std::filesystem::path m_main_filename; 35 | std::filesystem::path m_backup_filename; 36 | QDateTime m_first_entry_time_filter; 37 | TimeFormat m_time_format; 38 | qint64 m_last_prev_size{0}; 39 | qint64 m_last_read_pos{0}; 40 | bool m_initialized{false}; 41 | }; 42 | } // namespace os 43 | -------------------------------------------------------------------------------- /src/lib/os/steam/include/os/steam/steamprocesstracker.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | 6 | // local includes 7 | #include "os/steam/steamcontentlogtracker.h" 8 | #include "os/steam/steamgameprocesslogtracker.h" 9 | #include "os/steam/steamshaderlogtracker.h" 10 | #include "os/steam/steamwebhelperlogtracker.h" 11 | 12 | // forward declarations 13 | namespace os 14 | { 15 | class NativeProcessHandlerInterface; 16 | } // namespace os 17 | 18 | namespace os 19 | { 20 | class SteamProcessTracker : public QObject 21 | { 22 | Q_OBJECT 23 | Q_DISABLE_COPY(SteamProcessTracker) 24 | 25 | public: 26 | struct LogTrackers 27 | { 28 | QTimer m_read_timer; 29 | SteamWebHelperLogTracker m_web_helper; 30 | SteamContentLogTracker m_content_log; 31 | SteamGameProcessLogTracker m_gameprocess_log; 32 | SteamShaderLogTracker m_shader_log; 33 | }; 34 | 35 | explicit SteamProcessTracker(std::unique_ptr native_handler); 36 | ~SteamProcessTracker() override; 37 | 38 | void close(); 39 | void terminate(); 40 | 41 | bool isRunning() const; 42 | uint getPid() const; 43 | QDateTime getStartTime() const; 44 | const LogTrackers* getLogTrackers() const; 45 | std::filesystem::path getSteamDir() const; 46 | 47 | signals: 48 | void signalProcessStateChanged(); 49 | 50 | public slots: 51 | void slotCheckState(); 52 | 53 | private slots: 54 | void slotCheckLogs(); 55 | 56 | private: 57 | struct ProcessData 58 | { 59 | uint m_pid{0}; 60 | QDateTime m_start_time; 61 | std::unique_ptr m_log_trackers; 62 | std::filesystem::path m_steam_dir; 63 | }; 64 | 65 | ProcessData m_data; 66 | QTimer m_check_timer; 67 | 68 | std::unique_ptr m_native_handler; 69 | }; 70 | } // namespace os 71 | -------------------------------------------------------------------------------- /src/lib/os/steam/include/os/steam/steamshaderlogtracker.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | 6 | // local includes 7 | #include "steamlogtracker.h" 8 | 9 | namespace os 10 | { 11 | class SteamShaderLogTracker : public SteamLogTracker 12 | { 13 | Q_OBJECT 14 | 15 | public: 16 | explicit SteamShaderLogTracker(const std::filesystem::path& logs_dir, QDateTime first_entry_time_filter); 17 | ~SteamShaderLogTracker() override = default; 18 | 19 | bool isAppCompilingShaders(std::uint64_t app_id) const; 20 | 21 | protected: 22 | void onLogChanged(const std::vector& new_lines) override; 23 | 24 | private: 25 | std::set m_apps_with_compiling_shaders; 26 | }; 27 | } // namespace os 28 | -------------------------------------------------------------------------------- /src/lib/os/steam/include/os/steam/steamwebhelperlogtracker.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // local includes 4 | #include "shared/enums.h" 5 | #include "steamlogtracker.h" 6 | 7 | namespace os 8 | { 9 | class SteamWebHelperLogTracker : public SteamLogTracker 10 | { 11 | Q_OBJECT 12 | 13 | public: 14 | explicit SteamWebHelperLogTracker(const std::filesystem::path& logs_dir, QDateTime first_entry_time_filter); 15 | ~SteamWebHelperLogTracker() override = default; 16 | 17 | enums::SteamUiMode getSteamUiMode() const; 18 | 19 | protected: 20 | void onLogChanged(const std::vector& new_lines) override; 21 | 22 | private: 23 | enums::SteamUiMode m_ui_mode{enums::SteamUiMode::Unknown}; 24 | }; 25 | } // namespace os 26 | -------------------------------------------------------------------------------- /src/lib/os/steam/steamappwatcher.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/steam/steamappwatcher.h" 3 | 4 | // local includes 5 | #include "os/steam/steamprocesstracker.h" 6 | #include "shared/loggingcategories.h" 7 | 8 | #include 9 | 10 | namespace os 11 | { 12 | SteamAppWatcher::SteamAppWatcher(const SteamProcessTracker& process_tracker, const std::uint64_t app_id) 13 | : m_process_tracker{process_tracker} 14 | , m_app_id{app_id} 15 | { 16 | connect(&m_check_timer, &QTimer::timeout, this, &SteamAppWatcher::slotCheckState); 17 | 18 | m_check_timer.setInterval(500); 19 | m_check_timer.setSingleShot(true); 20 | 21 | qCInfo(lc::os) << "Started watching AppID:" << m_app_id; 22 | slotCheckState(); 23 | } 24 | 25 | SteamAppWatcher::~SteamAppWatcher() 26 | { 27 | qCInfo(lc::os) << "Stopped watching AppID:" << m_app_id; 28 | } 29 | 30 | enums::AppState SteamAppWatcher::getAppState(const SteamProcessTracker& process_tracker, const std::uint64_t app_id, 31 | const enums::AppState prev_state) 32 | { 33 | const auto* log_trackers{process_tracker.getLogTrackers()}; 34 | if (log_trackers == nullptr) 35 | { 36 | return enums::AppState::Stopped; 37 | } 38 | 39 | auto new_state{enums::AppState::Stopped}; 40 | const auto content_state{log_trackers->m_content_log.getAppState(app_id)}; 41 | 42 | if (content_state == SteamContentLogTracker::AppState::Updating 43 | || log_trackers->m_shader_log.isAppCompilingShaders(app_id)) 44 | { 45 | new_state = enums::AppState::Updating; 46 | } 47 | else if (content_state == SteamContentLogTracker::AppState::Running) 48 | { 49 | new_state = enums::AppState::Running; 50 | } 51 | else if (log_trackers->m_gameprocess_log.isAnyProcessRunning(app_id)) 52 | { 53 | // Try to preserve the latest state from other logs, unless this is the only data available 54 | new_state = prev_state == enums::AppState::Stopped ? enums::AppState::Running : prev_state; 55 | } 56 | 57 | return new_state; 58 | } 59 | 60 | enums::AppState SteamAppWatcher::getAppState() const 61 | { 62 | return m_current_state; 63 | } 64 | 65 | std::uint64_t SteamAppWatcher::getAppId() const 66 | { 67 | return m_app_id; 68 | } 69 | 70 | void SteamAppWatcher::slotCheckState() 71 | { 72 | const auto auto_start_timer{qScopeGuard([this]() { m_check_timer.start(); })}; 73 | 74 | if (const auto new_state{getAppState(m_process_tracker, m_app_id, m_current_state)}; new_state != m_current_state) 75 | { 76 | if (new_state == enums::AppState::Stopped && m_delay_counter < 5) 77 | { 78 | ++m_delay_counter; 79 | return; 80 | } 81 | 82 | qCInfo(lc::os) << "[TRACKING] New app state for AppID" << m_app_id 83 | << "detected:" << lc::qEnumToString(m_current_state) << "->" << lc::qEnumToString(new_state); 84 | m_current_state = new_state; 85 | m_delay_counter = 0; 86 | } 87 | } 88 | } // namespace os 89 | -------------------------------------------------------------------------------- /src/lib/os/steam/steamcontentlogtracker.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/steam/steamcontentlogtracker.h" 3 | 4 | // system/Qt includes 5 | #include 6 | 7 | // local includes 8 | #include "shared/loggingcategories.h" 9 | 10 | namespace os 11 | { 12 | namespace 13 | { 14 | std::map 15 | remapStateChanges(const std::map>& input) 16 | { 17 | using AppStateChange = SteamContentLogTracker::AppStateChange; 18 | using AppState = SteamContentLogTracker::AppState; 19 | 20 | const auto map_state{[](const AppStateChange state_change) 21 | { 22 | switch (state_change) 23 | { 24 | case AppStateChange::UpdateRunning: 25 | case AppStateChange::UpdateStarted: 26 | return AppState::Updating; 27 | case AppStateChange::AppRunning: 28 | return AppState::Running; 29 | case AppStateChange::UpdateRequired: 30 | case AppStateChange::UpdateQueued: 31 | case AppStateChange::UpdateOptional: 32 | case AppStateChange::FullyInstalled: 33 | case AppStateChange::FilesMissing: 34 | case AppStateChange::Uninstalling: 35 | case AppStateChange::Uninstalled: 36 | case AppStateChange::ComponentInUse: 37 | case AppStateChange::Terminating: 38 | case AppStateChange::PrefetchingInfo: 39 | return AppState::Stopped; 40 | } 41 | 42 | Q_UNREACHABLE(); 43 | }}; 44 | 45 | std::map new_app_states; 46 | for (const auto& [app_id, state_changes] : input) 47 | { 48 | qCDebug(lc::os) << "New state changes for AppID" << app_id << "detected:" << state_changes; 49 | 50 | AppState new_state{AppState::Stopped}; 51 | for (const auto state_change : state_changes) 52 | { 53 | const auto state{map_state(state_change)}; 54 | if (state != AppState::Stopped) 55 | { 56 | new_state = state; 57 | if (new_state == AppState::Running) 58 | { 59 | break; 60 | } 61 | } 62 | } 63 | 64 | new_app_states[app_id] = new_state; 65 | } 66 | return new_app_states; 67 | } 68 | } // namespace 69 | 70 | SteamContentLogTracker::SteamContentLogTracker(const std::filesystem::path& logs_dir, QDateTime first_entry_time_filter) 71 | : SteamLogTracker(logs_dir / "content_log.txt", logs_dir / "content_log.previous.txt", 72 | std::move(first_entry_time_filter)) 73 | { 74 | } 75 | 76 | SteamContentLogTracker::AppState SteamContentLogTracker::getAppState(const std::uint64_t app_id) const 77 | { 78 | const auto it = m_app_states.find(app_id); 79 | return it != std::end(m_app_states) ? it->second : AppState::Stopped; 80 | } 81 | 82 | void SteamContentLogTracker::onLogChanged(const std::vector& new_lines) 83 | { 84 | static const auto known_states{ 85 | []() 86 | { 87 | static const QRegularExpression capital_letter_regex{R"(([A-Z]))"}; 88 | const auto enum_size{QMetaEnum::fromType().keyCount()}; 89 | std::map states; 90 | for (int i = 0; i < enum_size; ++i) 91 | { 92 | const auto value{static_cast(QMetaEnum::fromType().value(i))}; 93 | QString key{QMetaEnum::fromType().key(i)}; 94 | key = key.replace(capital_letter_regex, R"( \1)").trimmed(); 95 | 96 | states[key] = value; 97 | } 98 | 99 | return states; 100 | }()}; 101 | 102 | std::map> new_change_states; 103 | for (const QString& line : new_lines) 104 | { 105 | static const QRegularExpression mode_regex{R"(AppID\s(\d+)\sstate\schanged\s:\s(.*),)"}; 106 | const auto match{mode_regex.match(line)}; 107 | if (match.hasMatch()) 108 | { 109 | const auto app_id{appIdFromString(match.captured(1))}; 110 | if (app_id == 0) 111 | { 112 | qCWarning(lc::os) << "Failed to get AppID from" << line; 113 | continue; 114 | } 115 | 116 | QVector mapped_change_states; 117 | const auto change_states{match.captured(2).split(',')}; 118 | for (const auto& state : change_states) 119 | { 120 | auto it = known_states.find(state); 121 | if (it == std::end(known_states)) 122 | { 123 | qCWarning(lc::os) << "Unmapped state for AppID" << app_id << "found:" << state; 124 | continue; 125 | } 126 | 127 | mapped_change_states.append(it->second); 128 | } 129 | 130 | new_change_states[app_id] = std::move(mapped_change_states); 131 | } 132 | } 133 | 134 | const auto new_app_states = remapStateChanges(new_change_states); 135 | for (const auto [app_id, app_state] : new_app_states) 136 | { 137 | auto it = m_app_states.find(app_id); 138 | if (it == std::end(m_app_states)) 139 | { 140 | if (app_state == AppState::Stopped) 141 | { 142 | continue; 143 | } 144 | 145 | qCInfo(lc::os) << "New app state for AppID" << app_id << "detected:" << lc::qEnumToString(AppState::Stopped) 146 | << "->" << lc::qEnumToString(app_state); 147 | m_app_states[app_id] = app_state; 148 | continue; 149 | } 150 | 151 | if (it->second == app_state) 152 | { 153 | continue; 154 | } 155 | 156 | qCInfo(lc::os) << "New app state for AppID" << app_id << "detected:" << lc::qEnumToString(it->second) << "->" 157 | << lc::qEnumToString(app_state); 158 | if (app_state == AppState::Stopped) 159 | { 160 | m_app_states.erase(it); 161 | } 162 | else 163 | { 164 | it->second = app_state; 165 | } 166 | } 167 | } 168 | } // namespace os 169 | -------------------------------------------------------------------------------- /src/lib/os/steam/steamgameprocesslogtracker.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/steam/steamgameprocesslogtracker.h" 3 | 4 | // system/Qt includes 5 | #include 6 | 7 | // local includes 8 | #include "shared/loggingcategories.h" 9 | 10 | namespace os 11 | { 12 | SteamGameProcessLogTracker::SteamGameProcessLogTracker(const std::filesystem::path& logs_dir, 13 | QDateTime first_entry_time_filter) 14 | : SteamLogTracker(logs_dir / "gameprocess_log.txt", logs_dir / "gameprocess_log.previous.txt", 15 | std::move(first_entry_time_filter)) 16 | { 17 | } 18 | 19 | bool SteamGameProcessLogTracker::isAnyProcessRunning(const std::uint64_t app_id) const 20 | { 21 | return m_app_id_to_process_ids.contains(app_id); 22 | } 23 | 24 | void SteamGameProcessLogTracker::onLogChanged(const std::vector& new_lines) 25 | { 26 | std::set added_app_ids; 27 | std::set removed_app_ids; 28 | 29 | for (const QString& line : new_lines) 30 | { 31 | static const QRegularExpression add_regex{R"(AppID (\d+) adding PID (\d+))"}; 32 | if (const auto match{add_regex.match(line)}; match.hasMatch()) 33 | { 34 | const auto app_id{appIdFromString(match.captured(1))}; 35 | if (app_id == 0) 36 | { 37 | qCWarning(lc::os) << "Failed to get AppID from" << line; 38 | continue; 39 | } 40 | 41 | const auto pid{match.captured(2).toUInt()}; 42 | if (pid == 0) 43 | { 44 | qCWarning(lc::os) << "Failed to get PID from" << line; 45 | continue; 46 | } 47 | 48 | auto data_it = m_app_id_to_process_ids.find(app_id); 49 | if (data_it == std::end(m_app_id_to_process_ids)) 50 | { 51 | data_it = m_app_id_to_process_ids.emplace(app_id, std::set{}).first; 52 | 53 | if (!removed_app_ids.contains(app_id)) 54 | { 55 | added_app_ids.insert(app_id); 56 | } 57 | removed_app_ids.erase(app_id); 58 | } 59 | 60 | data_it->second.insert(pid); 61 | continue; 62 | } 63 | 64 | static const QRegularExpression remove_regex{ 65 | R"((?:Game (\d+) going away.* PID (\d+))|(?:AppID (\d+) no longer.* PID (\d+)))"}; 66 | if (const auto match{remove_regex.match(line)}; match.hasMatch()) 67 | { 68 | const auto app_id{appIdFromString(match.hasCaptured(1) ? match.captured(1) : match.captured(3))}; 69 | if (app_id == 0) 70 | { 71 | qCWarning(lc::os) << "Failed to get AppID from" << line; 72 | continue; 73 | } 74 | 75 | const auto pid{(match.hasCaptured(2) ? match.captured(2) : match.captured(4)).toUInt()}; 76 | if (pid == 0) 77 | { 78 | qCWarning(lc::os) << "Failed to get PID from" << line; 79 | continue; 80 | } 81 | 82 | auto data_it = m_app_id_to_process_ids.find(app_id); 83 | if (data_it == std::end(m_app_id_to_process_ids)) 84 | { 85 | qCWarning(lc::os) << "Trying to remove PID" << pid << "from" << app_id << "but AppID is not tracked!"; 86 | continue; 87 | } 88 | 89 | data_it->second.erase(pid); 90 | if (data_it->second.empty()) 91 | { 92 | m_app_id_to_process_ids.erase(data_it); 93 | 94 | if (!added_app_ids.contains(app_id)) 95 | { 96 | removed_app_ids.insert(app_id); 97 | } 98 | added_app_ids.erase(app_id); 99 | } 100 | } 101 | } 102 | 103 | for (const auto& app_id : added_app_ids) 104 | { 105 | qCInfo(lc::os) << "Running processes added for AppID:" << app_id; 106 | } 107 | 108 | for (const auto& app_id : removed_app_ids) 109 | { 110 | qCInfo(lc::os) << "Running processes removed for AppID:" << app_id; 111 | } 112 | } 113 | } // namespace os 114 | -------------------------------------------------------------------------------- /src/lib/os/steam/steamlogtracker.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/steam/steamlogtracker.h" 3 | 4 | // system/Qt includes 5 | #include 6 | #include 7 | #include 8 | 9 | // local includes 10 | #include "shared/loggingcategories.h" 11 | 12 | namespace 13 | { 14 | bool openForReading(QFile& file) 15 | { 16 | if (!file.exists()) 17 | { 18 | qCDebug(lc::os) << "file" << file.fileName() << "does not exist yet."; 19 | return false; 20 | } 21 | 22 | if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) 23 | { 24 | qCWarning(lc::os) << "file" << file.fileName() << "could not be opened!"; 25 | return false; 26 | } 27 | 28 | return true; 29 | } 30 | 31 | QDateTime getDateTimeFromLogLine(const QString& line, const os::SteamLogTracker::TimeFormat time_format) 32 | { 33 | using enum os::SteamLogTracker::TimeFormat; 34 | 35 | switch (time_format) 36 | { 37 | case YYYY_MM_DD_hh_mm_ss: 38 | { 39 | static const QRegularExpression time_regex{R"(^\[(\d{4})-(\d{2})-(\d{2})\s(\d{2}):(\d{2}):(\d{2})\])"}; 40 | if (const auto match{time_regex.match(line)}; match.hasMatch()) 41 | { 42 | const QDate date{match.captured(1).toInt(), match.captured(2).toInt(), match.captured(3).toInt()}; 43 | const QTime time{match.captured(4).toInt(), match.captured(5).toInt(), match.captured(6).toInt()}; 44 | return QDateTime{date, time}; 45 | } 46 | } 47 | } 48 | 49 | return QDateTime{}; 50 | } 51 | 52 | qint64 readAllLines(std::vector& lines, QFile& file) 53 | { 54 | QTextStream stream{&file}; 55 | QString line; 56 | 57 | stream.seek(0); 58 | 59 | while (stream.readLineInto(&line)) 60 | { 61 | if (!line.isEmpty()) 62 | { 63 | lines.push_back(line); 64 | } 65 | } 66 | 67 | return stream.pos(); 68 | } 69 | 70 | bool isLineBeforeDatetime(const QString& line, const QDateTime& datetime, 71 | const os::SteamLogTracker::TimeFormat time_format) 72 | { 73 | const QDateTime logtime{getDateTimeFromLogLine(line, time_format)}; 74 | if (!logtime.isValid()) 75 | { 76 | return false; 77 | } 78 | 79 | return logtime < datetime; 80 | } 81 | 82 | bool isLineAtOrAfterDatetime(const QString& line, const QDateTime& datetime, 83 | const os::SteamLogTracker::TimeFormat time_format) 84 | { 85 | const QDateTime logtime{getDateTimeFromLogLine(line, time_format)}; 86 | if (!logtime.isValid()) 87 | { 88 | return false; 89 | } 90 | 91 | return logtime >= datetime; 92 | } 93 | 94 | void filterLines(std::vector& lines, QDateTime& datetime, const os::SteamLogTracker::TimeFormat time_format) 95 | { 96 | auto line_rit = std::rbegin(lines); 97 | for (; line_rit != std::rend(lines); ++line_rit) 98 | { 99 | if (isLineBeforeDatetime(*line_rit, datetime, time_format)) 100 | { 101 | break; 102 | } 103 | } 104 | 105 | auto line_it = line_rit == std::rend(lines) ? std::begin(lines) : line_rit.base() /* next element */; 106 | for (; line_it != std::end(lines); ++line_it) 107 | { 108 | if (isLineAtOrAfterDatetime(*line_it, datetime, time_format)) 109 | { 110 | datetime = {}; 111 | break; 112 | } 113 | } 114 | 115 | lines.erase(std::begin(lines), line_it); 116 | } 117 | 118 | qint64 readRemainingLines(std::vector& lines, QFile& file, QDateTime& first_entry_time_filter, 119 | const os::SteamLogTracker::TimeFormat time_format, const qint64 start_offset) 120 | { 121 | QTextStream stream{&file}; 122 | QString line; 123 | 124 | // Start reading from the last position we've read. 125 | stream.seek(start_offset); 126 | 127 | while (stream.readLineInto(&line)) 128 | { 129 | if (!line.isEmpty()) 130 | { 131 | if (first_entry_time_filter.isValid()) 132 | { 133 | if (!isLineAtOrAfterDatetime(line, first_entry_time_filter, time_format)) 134 | { 135 | continue; 136 | } 137 | 138 | first_entry_time_filter = {}; 139 | } 140 | 141 | lines.push_back(line); 142 | } 143 | } 144 | 145 | return stream.pos(); 146 | } 147 | } // namespace 148 | 149 | namespace os 150 | { 151 | SteamLogTracker::SteamLogTracker(std::filesystem::path main_filename, std::filesystem::path backup_filename, 152 | QDateTime first_entry_time_filter, TimeFormat time_format) 153 | : m_main_filename{std::move(main_filename)} 154 | , m_backup_filename{std::move(backup_filename)} 155 | , m_first_entry_time_filter{std::move(first_entry_time_filter)} 156 | , m_time_format{time_format} 157 | { 158 | } 159 | 160 | void SteamLogTracker::slotCheckLog() 161 | { 162 | QFile main_file{m_main_filename}; 163 | if (!openForReading(main_file)) 164 | { 165 | return; 166 | } 167 | 168 | const auto current_main_file_size{main_file.size()}; 169 | const bool was_main_file_appended{current_main_file_size > m_last_prev_size}; 170 | const bool was_main_file_switched_with_backup{current_main_file_size < m_last_prev_size}; 171 | 172 | if (!m_initialized) 173 | { 174 | qCInfo(lc::os) << "performing initial log read for files" << m_main_filename.generic_string() << "and" 175 | << m_backup_filename.generic_string(); 176 | 177 | QFile backup_file{m_backup_filename}; 178 | if (!openForReading(backup_file)) 179 | { 180 | if (backup_file.exists()) 181 | { 182 | // Warning already logged. 183 | return; 184 | } 185 | 186 | qCInfo(lc::os) << "skipping file" << m_backup_filename.generic_string() 187 | << "for initial read, because it does not exist."; 188 | } 189 | 190 | std::vector lines; 191 | if (backup_file.isOpen()) 192 | { 193 | readAllLines(lines, backup_file); 194 | } 195 | 196 | m_last_read_pos = readAllLines(lines, main_file); 197 | m_last_prev_size = current_main_file_size; 198 | 199 | filterLines(lines, m_first_entry_time_filter, m_time_format); 200 | onLogChanged(lines); 201 | m_initialized = true; 202 | return; 203 | } 204 | 205 | if (was_main_file_appended) 206 | { 207 | qCDebug(lc::os) << "file" << m_main_filename.generic_string() << "was appended."; 208 | 209 | std::vector lines; 210 | m_last_read_pos = 211 | readRemainingLines(lines, main_file, m_first_entry_time_filter, m_time_format, m_last_read_pos); 212 | m_last_prev_size = current_main_file_size; 213 | 214 | onLogChanged(lines); 215 | return; 216 | } 217 | 218 | if (was_main_file_switched_with_backup) 219 | { 220 | qCDebug(lc::os) << "file" << m_main_filename.generic_string() << "was switched with" 221 | << m_backup_filename.generic_string(); 222 | 223 | QFile backup_file{m_backup_filename}; 224 | if (!openForReading(backup_file)) 225 | { 226 | return; 227 | } 228 | 229 | std::vector lines; 230 | readRemainingLines(lines, backup_file, m_first_entry_time_filter, m_time_format, m_last_read_pos); 231 | m_last_read_pos = readRemainingLines(lines, main_file, m_first_entry_time_filter, m_time_format, 0); 232 | m_last_prev_size = current_main_file_size; 233 | 234 | onLogChanged(lines); 235 | return; 236 | } 237 | 238 | qCDebug(lc::os) << "file" << m_main_filename.generic_string() << "did not change."; 239 | } 240 | 241 | std::uint64_t SteamLogTracker::appIdFromString(const QString& app_id) 242 | { 243 | static_assert(sizeof(qulonglong) == sizeof(std::uint64_t)); 244 | return app_id.toULongLong(); 245 | } 246 | } // namespace os 247 | -------------------------------------------------------------------------------- /src/lib/os/steam/steamprocesstracker.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/steam/steamprocesstracker.h" 3 | 4 | // system/Qt includes 5 | #include 6 | #include 7 | #include 8 | 9 | // local includes 10 | #include "os/shared/nativeprocesshandlerinterface.h" 11 | #include "shared/loggingcategories.h" 12 | 13 | namespace 14 | { 15 | std::filesystem::path getSteamDir(const QString& exec_path) 16 | { 17 | QDir dir{exec_path}; 18 | for (int i = 0; i < 3; ++i) // Go up only 3 levels 19 | { 20 | static const QStringList required_dirs{"logs", "userdata", "steamui"}; 21 | if (std::ranges::all_of(required_dirs, [&dir](const auto& path) { return dir.exists(path); })) 22 | { 23 | return dir.filesystemCanonicalPath(); 24 | } 25 | 26 | if (!dir.cdUp()) 27 | { 28 | break; 29 | } 30 | } 31 | 32 | return {}; 33 | } 34 | } // namespace 35 | 36 | namespace os 37 | { 38 | SteamProcessTracker::SteamProcessTracker(std::unique_ptr native_handler) 39 | : m_native_handler{std::move(native_handler)} 40 | { 41 | Q_ASSERT(m_native_handler); 42 | 43 | connect(&m_check_timer, &QTimer::timeout, this, &SteamProcessTracker::slotCheckState); 44 | 45 | m_check_timer.setInterval(1000); 46 | m_check_timer.setSingleShot(true); 47 | 48 | QTimer::singleShot(0, this, &SteamProcessTracker::slotCheckState); 49 | } 50 | 51 | // For forward declarations 52 | SteamProcessTracker::~SteamProcessTracker() = default; 53 | 54 | void SteamProcessTracker::close() 55 | { 56 | slotCheckState(); 57 | if (isRunning()) 58 | { 59 | m_native_handler->close(m_data.m_pid); 60 | } 61 | } 62 | 63 | void SteamProcessTracker::terminate() 64 | { 65 | slotCheckState(); 66 | if (isRunning()) 67 | { 68 | m_native_handler->terminate(m_data.m_pid); 69 | } 70 | } 71 | 72 | bool SteamProcessTracker::isRunning() const 73 | { 74 | return m_data.m_pid != 0; 75 | } 76 | 77 | uint SteamProcessTracker::getPid() const 78 | { 79 | return m_data.m_pid; 80 | } 81 | 82 | QDateTime SteamProcessTracker::getStartTime() const 83 | { 84 | return m_data.m_start_time; 85 | } 86 | 87 | const SteamProcessTracker::LogTrackers* SteamProcessTracker::getLogTrackers() const 88 | { 89 | return m_data.m_log_trackers.get(); 90 | } 91 | 92 | std::filesystem::path SteamProcessTracker::getSteamDir() const 93 | { 94 | return m_data.m_steam_dir; 95 | } 96 | 97 | void SteamProcessTracker::slotCheckState() 98 | { 99 | m_check_timer.stop(); 100 | const auto auto_start_timer{qScopeGuard([this]() { m_check_timer.start(); })}; 101 | 102 | if (isRunning()) 103 | { 104 | const auto start_time = m_native_handler->getStartTime(m_data.m_pid); 105 | if (m_data.m_start_time == start_time) 106 | { 107 | return; 108 | } 109 | 110 | m_data = {}; 111 | emit signalProcessStateChanged(); 112 | } 113 | 114 | const auto pids{m_native_handler->getPids()}; 115 | for (const auto pid : pids) 116 | { 117 | const QString exec_path{m_native_handler->getExecPath(pid)}; 118 | if (exec_path.isEmpty()) 119 | { 120 | continue; 121 | } 122 | 123 | // clang-format off 124 | static const QRegularExpression exec_regex{ 125 | R"((?:.+?steam\.exe$))" // Windows 126 | R"(|)" // OR 127 | R"((?:.*?steam.+?steam$))", // Linux 128 | QRegularExpression::CaseInsensitiveOption}; 129 | // clang-format on 130 | 131 | if (!exec_path.contains(exec_regex)) 132 | { 133 | continue; 134 | } 135 | qCInfo(lc::os) << "Found a matching Steam process. PATH:" << exec_path << "| PID:" << pid; 136 | 137 | auto cleanup{qScopeGuard([this]() { m_data = {}; })}; 138 | 139 | m_data.m_steam_dir = ::getSteamDir(exec_path); 140 | if (m_data.m_steam_dir.empty()) 141 | { 142 | qCInfo(lc::os) << "Could not resolve steam directory for running Steam process, PID:" << pid; 143 | continue; 144 | } 145 | 146 | const auto steam_log_dir{m_data.m_steam_dir / "logs"}; 147 | if (!std::filesystem::exists(steam_log_dir)) 148 | { 149 | qCInfo(lc::os) << "Could not resolve steam logs directory for running Steam process, PID:" << pid; 150 | continue; 151 | } 152 | 153 | m_data.m_start_time = m_native_handler->getStartTime(pid); 154 | if (!m_data.m_start_time.isValid()) 155 | { 156 | qCWarning(lc::os) << "Could not resolve start time for running Steam process! PID:" << pid; 157 | break; 158 | } 159 | 160 | cleanup.dismiss(); 161 | 162 | m_data.m_pid = pid; 163 | m_data.m_log_trackers.reset(new LogTrackers{QTimer{}, 164 | SteamWebHelperLogTracker{steam_log_dir, m_data.m_start_time}, 165 | SteamContentLogTracker{steam_log_dir, m_data.m_start_time}, 166 | SteamGameProcessLogTracker{steam_log_dir, m_data.m_start_time}, 167 | SteamShaderLogTracker{steam_log_dir, m_data.m_start_time}}); 168 | 169 | connect(&m_data.m_log_trackers->m_read_timer, &QTimer::timeout, this, &SteamProcessTracker::slotCheckLogs); 170 | m_data.m_log_trackers->m_read_timer.setSingleShot(true); 171 | m_data.m_log_trackers->m_read_timer.setInterval(1000); 172 | slotCheckLogs(); 173 | 174 | emit signalProcessStateChanged(); 175 | break; 176 | } 177 | } 178 | 179 | void SteamProcessTracker::slotCheckLogs() 180 | { 181 | if (m_data.m_log_trackers) 182 | { 183 | m_data.m_log_trackers->m_read_timer.stop(); 184 | const auto auto_start_timer{qScopeGuard([this]() { m_data.m_log_trackers->m_read_timer.start(); })}; 185 | 186 | m_data.m_log_trackers->m_web_helper.slotCheckLog(); 187 | m_data.m_log_trackers->m_content_log.slotCheckLog(); 188 | m_data.m_log_trackers->m_gameprocess_log.slotCheckLog(); 189 | m_data.m_log_trackers->m_shader_log.slotCheckLog(); 190 | } 191 | } 192 | } // namespace os 193 | -------------------------------------------------------------------------------- /src/lib/os/steam/steamshaderlogtracker.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/steam/steamshaderlogtracker.h" 3 | 4 | // system/Qt includes 5 | #include 6 | 7 | // local includes 8 | #include "shared/loggingcategories.h" 9 | 10 | namespace os 11 | { 12 | SteamShaderLogTracker::SteamShaderLogTracker(const std::filesystem::path& logs_dir, QDateTime first_entry_time_filter) 13 | : SteamLogTracker(logs_dir / "shader_log.txt", logs_dir / "shader_log.previous.txt", 14 | std::move(first_entry_time_filter)) 15 | { 16 | } 17 | 18 | bool SteamShaderLogTracker::isAppCompilingShaders(const std::uint64_t app_id) const 19 | { 20 | return m_apps_with_compiling_shaders.contains(app_id); 21 | } 22 | 23 | void SteamShaderLogTracker::onLogChanged(const std::vector& new_lines) 24 | { 25 | std::map new_shader_states; 26 | for (const QString& line : new_lines) 27 | { 28 | static const QRegularExpression regex{ 29 | R"((?:Starting processing job for app (\d+))|(?:Destroyed compile job (\d+)))"}; 30 | if (const auto match{regex.match(line)}; match.hasMatch()) 31 | { 32 | const bool started{match.hasCaptured(1)}; 33 | const auto app_id{appIdFromString(started ? match.captured(1) : match.captured(2))}; 34 | if (app_id == 0) 35 | { 36 | qCWarning(lc::os) << "Failed to get AppID from" << line; 37 | continue; 38 | } 39 | 40 | new_shader_states[app_id] = started; 41 | } 42 | } 43 | 44 | for (const auto [app_id, state] : new_shader_states) 45 | { 46 | auto data_it{m_apps_with_compiling_shaders.find(app_id)}; 47 | if (state) 48 | { 49 | if (data_it == std::end(m_apps_with_compiling_shaders)) 50 | { 51 | m_apps_with_compiling_shaders.insert(app_id); 52 | qCInfo(lc::os) << "Compiling shaders for AppID:" << app_id; 53 | } 54 | } 55 | else 56 | { 57 | if (data_it != std::end(m_apps_with_compiling_shaders)) 58 | { 59 | m_apps_with_compiling_shaders.erase(data_it); 60 | qCInfo(lc::os) << "Stopped compiling shaders for AppID:" << app_id; 61 | } 62 | } 63 | } 64 | } 65 | } // namespace os 66 | -------------------------------------------------------------------------------- /src/lib/os/steam/steamwebhelperlogtracker.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/steam/steamwebhelperlogtracker.h" 3 | 4 | // system/Qt includes 5 | #include 6 | 7 | // local includes 8 | #include "shared/loggingcategories.h" 9 | 10 | namespace os 11 | { 12 | SteamWebHelperLogTracker::SteamWebHelperLogTracker(const std::filesystem::path& logs_dir, 13 | QDateTime first_entry_time_filter) 14 | : SteamLogTracker(logs_dir / "webhelper.txt", logs_dir / "webhelper.previous.txt", 15 | std::move(first_entry_time_filter)) 16 | { 17 | } 18 | 19 | enums::SteamUiMode SteamWebHelperLogTracker::getSteamUiMode() const 20 | { 21 | return m_ui_mode; 22 | } 23 | 24 | void SteamWebHelperLogTracker::onLogChanged(const std::vector& new_lines) 25 | { 26 | enums::SteamUiMode new_ui_mode{m_ui_mode}; 27 | for (const QString& line : new_lines) 28 | { 29 | static const QRegularExpression initial_regex{R"(SP\s(?:(Desktop)|(BPM))_)"}; 30 | static const QRegularExpression default_regex{R"(SP\s(?:(Desktop)|(BPM))_.+?WasHidden\s(?:(0)|(1)))"}; 31 | const auto match{(new_ui_mode == enums::SteamUiMode::Unknown ? initial_regex : default_regex).match(line)}; 32 | if (match.hasMatch()) 33 | { 34 | constexpr int desktop_group{1}; 35 | constexpr int was_hidden_group{4}; 36 | if (match.hasCaptured(desktop_group)) 37 | { 38 | if (match.hasCaptured(was_hidden_group)) 39 | { 40 | // Desktop was hidden, but unless BPM is visible we assume it's Desktop mode still 41 | } 42 | else 43 | { 44 | new_ui_mode = enums::SteamUiMode::Desktop; 45 | } 46 | } 47 | else 48 | { 49 | if (match.hasCaptured(was_hidden_group)) 50 | { 51 | // BPM was hidden, we will fall back to Desktop by default 52 | new_ui_mode = enums::SteamUiMode::Desktop; 53 | } 54 | else 55 | { 56 | new_ui_mode = enums::SteamUiMode::BigPicture; 57 | } 58 | } 59 | } 60 | } 61 | 62 | if (new_ui_mode != m_ui_mode) 63 | { 64 | qCInfo(lc::os()) << "Steam UI mode change:" << lc::qEnumToString(m_ui_mode) << "->" 65 | << lc::qEnumToString(new_ui_mode); 66 | m_ui_mode = new_ui_mode; 67 | } 68 | } 69 | } // namespace os 70 | -------------------------------------------------------------------------------- /src/lib/os/streamstatehandler.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/streamstatehandler.h" 3 | 4 | namespace os 5 | { 6 | StreamStateHandler::StreamStateHandler(const QString& heartbeat_key) 7 | : m_helper_heartbeat{heartbeat_key} 8 | { 9 | connect(&m_helper_heartbeat, &utils::Heartbeat::signalStateChanged, this, 10 | &StreamStateHandler::slotHandleProcessStateChanges); 11 | m_helper_heartbeat.startListening(); 12 | } 13 | 14 | bool StreamStateHandler::endStream() 15 | { 16 | if (m_state == enums::StreamState::Streaming) 17 | { 18 | m_helper_heartbeat.terminate(); 19 | m_state = enums::StreamState::StreamEnding; 20 | emit signalStreamStateChanged(); 21 | } 22 | 23 | return true; 24 | } 25 | 26 | enums::StreamState StreamStateHandler::getCurrentState() const 27 | { 28 | return m_state; 29 | } 30 | 31 | void StreamStateHandler::slotHandleProcessStateChanges() 32 | { 33 | switch (m_state) 34 | { 35 | case enums::StreamState::NotStreaming: 36 | { 37 | if (m_helper_heartbeat.isAlive()) 38 | { 39 | m_state = enums::StreamState::Streaming; 40 | emit signalStreamStateChanged(); 41 | } 42 | break; 43 | } 44 | case enums::StreamState::Streaming: 45 | case enums::StreamState::StreamEnding: 46 | { 47 | if (!m_helper_heartbeat.isAlive()) 48 | { 49 | m_state = enums::StreamState::NotStreaming; 50 | emit signalStreamStateChanged(); 51 | } 52 | break; 53 | } 54 | } 55 | } 56 | } // namespace os 57 | -------------------------------------------------------------------------------- /src/lib/os/sunshineapps.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/sunshineapps.h" 3 | 4 | // system/Qt includes 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | // local includes 14 | #include "shared/loggingcategories.h" 15 | 16 | namespace 17 | { 18 | #if defined(Q_OS_LINUX) 19 | QString getConfigDir() 20 | { 21 | const auto xdg_config_env = qgetenv("XDG_CONFIG_HOME"); 22 | if (!xdg_config_env.isEmpty()) 23 | { 24 | const QDir xdg_cnnfig_dir{xdg_config_env}; 25 | if (xdg_cnnfig_dir.exists()) 26 | { 27 | return xdg_cnnfig_dir.absolutePath(); 28 | } 29 | } 30 | 31 | return QDir::cleanPath(QDir::homePath() + "/.config"); 32 | } 33 | #endif 34 | } // namespace 35 | 36 | namespace os 37 | { 38 | SunshineApps::SunshineApps(QString filepath) 39 | : m_filepath{std::move(filepath)} 40 | { 41 | } 42 | 43 | // NOLINTNEXTLINE(*-cognitive-complexity) 44 | std::optional> SunshineApps::load() 45 | { 46 | QString filepath{m_filepath}; 47 | if (filepath.isEmpty()) // Fallback to places where we could expect the file to exist 48 | { 49 | #if defined(Q_OS_WIN) 50 | const QSettings settings(R"(HKEY_LOCAL_MACHINE\Software\LizardByte\Sunshine)", QSettings::NativeFormat); 51 | filepath = settings.value("Default").toString(); 52 | if (!filepath.isEmpty()) 53 | { 54 | filepath = QDir::cleanPath(filepath + "/config/apps.json"); 55 | } 56 | #elif defined(Q_OS_LINUX) 57 | filepath = QDir::cleanPath(getConfigDir() + "/sunshine/apps.json"); 58 | #else 59 | #error OS is not supported! 60 | #endif 61 | } 62 | 63 | qCDebug(lc::os) << "selected filepath for Sunshine apps:" << filepath; 64 | if (filepath.isEmpty()) 65 | { 66 | qCWarning(lc::os) << "filepath for Sunshine apps is empty!"; 67 | return std::nullopt; 68 | } 69 | 70 | QFile file{filepath}; 71 | if (!file.open(QFile::ReadOnly)) 72 | { 73 | qCWarning(lc::os) << "file" << filepath << "could not be opened! Reason:" << file.errorString(); 74 | return std::nullopt; 75 | } 76 | 77 | const auto data{file.readAll()}; 78 | 79 | QJsonParseError parser_error; 80 | const QJsonDocument json_data{QJsonDocument::fromJson(data, &parser_error)}; 81 | if (json_data.isNull()) 82 | { 83 | qCWarning(lc::os) << "failed to decode JSON data! Reason:" << parser_error.errorString() << "| data:" << data; 84 | return std::nullopt; 85 | } 86 | 87 | qCDebug(lc::os).noquote() << "Sunshine apps file content:" << Qt::endl << json_data.toJson(QJsonDocument::Indented); 88 | if (json_data.isObject()) 89 | { 90 | const auto json_object = json_data.object(); 91 | const auto apps_it{json_object.find("apps")}; 92 | if (apps_it != json_object.end() && apps_it->isArray()) 93 | { 94 | std::set parsed_apps{}; 95 | 96 | const auto apps = apps_it->toArray(); 97 | if (apps.isEmpty()) 98 | { 99 | qCDebug(lc::os) << "there are no Sunshine apps to parse."; 100 | return parsed_apps; 101 | } 102 | 103 | for (const auto& app : apps) 104 | { 105 | if (!app.isObject()) 106 | { 107 | qCDebug(lc::os) << "skipping entry as it's not an object:" << app; 108 | continue; 109 | } 110 | 111 | const auto app_object = app.toObject(); 112 | const auto name_it{app_object.find("name")}; 113 | if (name_it == app_object.end()) 114 | { 115 | qCDebug(lc::os) << "skipping entry as it does not contain \"name\" field:" << app_object; 116 | continue; 117 | } 118 | 119 | if (!name_it->isString()) 120 | { 121 | qCDebug(lc::os) << "skipping entry as the \"name\" field does not contain a string:" << *name_it; 122 | continue; 123 | } 124 | 125 | parsed_apps.insert(name_it->toString()); 126 | } 127 | 128 | qCDebug(lc::os) << "parsed the following Sunshine apps:" 129 | << QSet{std::begin(parsed_apps), std::end(parsed_apps)}; 130 | return parsed_apps; 131 | } 132 | } 133 | 134 | qCWarning(lc::os) << "file" << m_filepath << "could not be parsed!"; 135 | return std::nullopt; 136 | } 137 | } // namespace os -------------------------------------------------------------------------------- /src/lib/os/systemtray.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/systemtray.h" 3 | 4 | #if defined(Q_OS_LINUX) 5 | // system/Qt includes 6 | #include 7 | #endif 8 | 9 | // local includes 10 | #include "os/pccontrol.h" 11 | #include "shared/loggingcategories.h" 12 | 13 | namespace os 14 | { 15 | SystemTray::SystemTray(const QIcon& icon, QString app_name, PcControl& pc_control) 16 | : m_autostart_action{"Start on system startup"} 17 | , m_quit_action{"Exit"} 18 | , m_icon{icon} 19 | , m_app_name{std::move(app_name)} 20 | , m_pc_control{pc_control} 21 | { 22 | connect(&m_autostart_action, &QAction::triggered, this, 23 | [this]() 24 | { 25 | m_pc_control.setAutoStart(m_autostart_action.isChecked()); 26 | if (m_autostart_action.isChecked() != m_pc_control.isAutoStartEnabled()) 27 | { 28 | qCWarning(lc::utils) << "failed to enable/disable autostart!"; 29 | m_autostart_action.setChecked(m_pc_control.isAutoStartEnabled()); 30 | } 31 | }); 32 | connect(&m_menu, &QMenu::aboutToShow, this, 33 | [this]() 34 | { 35 | // Sync the state in case it was set via cmd 36 | m_autostart_action.setChecked(m_pc_control.isAutoStartEnabled()); 37 | }); 38 | connect(&m_quit_action, &QAction::triggered, this, &SystemTray::signalQuitApp); 39 | connect(&m_tray_attach_retry_timer, &QTimer::timeout, this, &SystemTray::slotTryAttach); 40 | 41 | m_autostart_action.setCheckable(true); 42 | 43 | m_menu.addAction(&m_autostart_action); 44 | m_menu.addAction(&m_quit_action); 45 | 46 | const auto retry_interval{5000}; 47 | m_tray_attach_retry_timer.setInterval(retry_interval); 48 | m_tray_attach_retry_timer.setSingleShot(true); 49 | slotTryAttach(); 50 | } 51 | 52 | void SystemTray::slotShowTrayMessage(const QString& title, const QString& message, QSystemTrayIcon::MessageIcon icon, 53 | int millisecondsTimeoutHint) 54 | { 55 | if (!m_tray_icon) 56 | { 57 | return; 58 | } 59 | 60 | m_tray_icon->showMessage(title, message, icon, millisecondsTimeoutHint); 61 | } 62 | 63 | void SystemTray::slotTryAttach() 64 | { 65 | Q_ASSERT(m_tray_icon == nullptr); 66 | 67 | #if defined(Q_OS_LINUX) 68 | // workaround for https://bugreports.qt.io/browse/QTBUG-94871 69 | const QDBusInterface systrayHost(QLatin1String("org.kde.StatusNotifierWatcher"), 70 | QLatin1String("/StatusNotifierWatcher"), 71 | QLatin1String("org.kde.StatusNotifierWatcher")); 72 | if (!systrayHost.isValid() || !systrayHost.property("IsStatusNotifierHostRegistered").toBool()) 73 | { 74 | constexpr int max_retries{25}; 75 | if (m_retry_counter++ < max_retries) 76 | { 77 | m_tray_attach_retry_timer.start(); 78 | return; 79 | } 80 | } 81 | #endif 82 | 83 | if (!QSystemTrayIcon::isSystemTrayAvailable()) 84 | { 85 | qCWarning(lc::utils) << "failed to initialize system tray..."; 86 | return; 87 | } 88 | 89 | m_tray_icon = std::make_unique(); 90 | m_tray_icon->setIcon(m_icon); 91 | m_tray_icon->setContextMenu(&m_menu); 92 | m_tray_icon->setVisible(true); 93 | m_tray_icon->setToolTip(m_app_name); 94 | } 95 | } // namespace os 96 | -------------------------------------------------------------------------------- /src/lib/os/win/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------------------------------------------------------------------- 2 | # Lib config 3 | #---------------------------------------------------------------------------------------------------------------------- 4 | 5 | set(LIBNAME osspecificlib) 6 | 7 | #---------------------------------------------------------------------------------------------------------------------- 8 | # External dependencies 9 | #---------------------------------------------------------------------------------------------------------------------- 10 | 11 | find_package(Qt6 COMPONENTS Core REQUIRED) 12 | qt_standard_project_setup() 13 | 14 | find_library(PSAPI Psapi REQUIRED) 15 | find_library(POWRPROF PowrProf REQUIRED) 16 | add_compile_definitions(PSAPI_VERSION=1) 17 | 18 | #---------------------------------------------------------------------------------------------------------------------- 19 | # Header/Source files 20 | #---------------------------------------------------------------------------------------------------------------------- 21 | 22 | file(GLOB HEADERS CONFIGURE_DEPENDS "include/os/win/*.h") 23 | file(GLOB SOURCES CONFIGURE_DEPENDS "*.cpp") 24 | 25 | #---------------------------------------------------------------------------------------------------------------------- 26 | # Target config 27 | #---------------------------------------------------------------------------------------------------------------------- 28 | 29 | add_library(${LIBNAME} ${HEADERS} ${SOURCES}) 30 | target_link_libraries(${LIBNAME} PRIVATE Qt6::Core Psapi PowrProf ossharedlib sharedlib ws2_32 iphlpapi) 31 | target_include_directories(${LIBNAME} PUBLIC include) 32 | -------------------------------------------------------------------------------- /src/lib/os/win/include/os/win/nativeautostarthandler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | 6 | // local includes 7 | #include "os/shared/nativeautostarthandlerinterface.h" 8 | 9 | // forward declarations 10 | namespace shared 11 | { 12 | class AppMetadata; 13 | } 14 | 15 | namespace os 16 | { 17 | class NativeAutoStartHandler : public NativeAutoStartHandlerInterface 18 | { 19 | Q_DISABLE_COPY(NativeAutoStartHandler) 20 | 21 | public: 22 | explicit NativeAutoStartHandler(const shared::AppMetadata& app_meta); 23 | virtual ~NativeAutoStartHandler() override = default; 24 | 25 | void setAutoStart(bool enable) override; 26 | bool isAutoStartEnabled() const override; 27 | 28 | private: 29 | const shared::AppMetadata& m_app_meta; 30 | }; 31 | } // namespace os 32 | -------------------------------------------------------------------------------- /src/lib/os/win/include/os/win/nativepcstatehandler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | 6 | // local includes 7 | #include "os/shared/nativepcstatehandlerinterface.h" 8 | 9 | namespace os 10 | { 11 | class NativePcStateHandler 12 | : public QObject 13 | , public NativePcStateHandlerInterface 14 | { 15 | Q_OBJECT 16 | Q_DISABLE_COPY(NativePcStateHandler) 17 | 18 | public: 19 | explicit NativePcStateHandler(); 20 | ~NativePcStateHandler() override = default; 21 | 22 | bool canShutdownPC() override; 23 | bool canRestartPC() override; 24 | bool canSuspendPC() override; 25 | bool canHibernatePC() override; 26 | 27 | bool shutdownPC() override; 28 | bool restartPC() override; 29 | bool suspendPC() override; 30 | bool hibernatePC() override; 31 | 32 | private: 33 | bool m_privilege_acquired; 34 | }; 35 | } // namespace os 36 | -------------------------------------------------------------------------------- /src/lib/os/win/include/os/win/nativeprocesshandler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // local includes 4 | #include "os/shared/nativeprocesshandlerinterface.h" 5 | 6 | namespace os 7 | { 8 | class NativeProcessHandler : public NativeProcessHandlerInterface 9 | { 10 | Q_DISABLE_COPY(NativeProcessHandler) 11 | 12 | public: 13 | explicit NativeProcessHandler() = default; 14 | ~NativeProcessHandler() override = default; 15 | 16 | std::vector getPids() const override; 17 | QString getExecPath(uint pid) const override; 18 | QDateTime getStartTime(uint pid) const override; 19 | void close(uint pid) const override; 20 | void terminate(uint pid) const override; 21 | }; 22 | } // namespace os 23 | -------------------------------------------------------------------------------- /src/lib/os/win/include/os/win/nativesleepinhibitor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | 6 | // local includes 7 | #include "os/shared/nativesleepinhibitorinterface.h" 8 | 9 | namespace os 10 | { 11 | class NativeSleepInhibitor : public NativeSleepInhibitorInterface 12 | { 13 | public: 14 | explicit NativeSleepInhibitor(const QString& app_name); 15 | ~NativeSleepInhibitor() override = default; 16 | }; 17 | } // namespace os 18 | -------------------------------------------------------------------------------- /src/lib/os/win/nativeautostarthandler.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/win/nativeautostarthandler.h" 3 | 4 | // system/Qt includes 5 | #include 6 | #include 7 | #include 8 | 9 | // local includes 10 | #include "shared/appmetadata.h" 11 | #include "shared/loggingcategories.h" 12 | 13 | namespace os 14 | { 15 | NativeAutoStartHandler::NativeAutoStartHandler(const shared::AppMetadata& app_meta) 16 | : m_app_meta{app_meta} 17 | { 18 | } 19 | 20 | void NativeAutoStartHandler::setAutoStart(bool enable) 21 | { 22 | const auto dir{m_app_meta.getAutoStartDir()}; 23 | QFile file{m_app_meta.getAutoStartPath()}; 24 | 25 | if (file.exists() && !file.remove()) 26 | { 27 | qFatal("Failed to remove %s", qUtf8Printable(m_app_meta.getAutoStartPath())); 28 | return; 29 | } 30 | 31 | if (enable) 32 | { 33 | if (!QFile::link(m_app_meta.getAutoStartExec(), m_app_meta.getAutoStartPath())) 34 | { 35 | qFatal("Failed to create link for %s -> %s", qUtf8Printable(m_app_meta.getAutoStartExec()), 36 | qUtf8Printable(m_app_meta.getAutoStartPath())); 37 | return; 38 | } 39 | } 40 | } 41 | 42 | bool NativeAutoStartHandler::isAutoStartEnabled() const 43 | { 44 | if (!QFile::exists(m_app_meta.getAutoStartPath())) 45 | { 46 | return false; 47 | } 48 | 49 | const QFileInfo info(m_app_meta.getAutoStartPath()); 50 | if (!info.isShortcut()) 51 | { 52 | return false; 53 | } 54 | 55 | return QFileInfo(info.symLinkTarget()).canonicalFilePath() 56 | == QFileInfo(m_app_meta.getAutoStartExec()).canonicalFilePath(); 57 | } 58 | } // namespace os 59 | -------------------------------------------------------------------------------- /src/lib/os/win/nativepcstatehandler.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/win/nativepcstatehandler.h" 3 | 4 | // A SEPARATE WINDOWS INCLUDE BECAUSE OF THE SMELL! 5 | #include 6 | 7 | // system/Qt includes 8 | #include 9 | 10 | // local includes 11 | #include "shared/loggingcategories.h" 12 | 13 | namespace 14 | { 15 | bool acquirePrivilege() 16 | { 17 | HANDLE token_handle{nullptr}; 18 | TOKEN_PRIVILEGES token_privileges; 19 | 20 | if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token_handle) == FALSE) 21 | { 22 | qCWarning(lc::os) << "Failed to open process token! Reason: " << lc::getErrorString(GetLastError()); 23 | return false; 24 | } 25 | auto cleanup = qScopeGuard([&token_handle]() { CloseHandle(token_handle); }); 26 | 27 | { 28 | token_privileges.PrivilegeCount = 1; 29 | token_privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; 30 | if (LookupPrivilegeValueW(nullptr, SE_SHUTDOWN_NAME, &token_privileges.Privileges[0].Luid) == FALSE) 31 | { 32 | qCWarning(lc::os) << "Failed to lookup privilege value! Reason: " << lc::getErrorString(GetLastError()); 33 | return false; 34 | } 35 | } 36 | 37 | // Ignoring the useless return value... 38 | AdjustTokenPrivileges(token_handle, FALSE, &token_privileges, 0, nullptr, nullptr); 39 | 40 | const auto result = GetLastError(); 41 | if (result != ERROR_SUCCESS) 42 | { 43 | qCWarning(lc::os) << "Failed to adjust token privileges! Reason: " << lc::getErrorString(result); 44 | return false; 45 | } 46 | 47 | return true; 48 | } 49 | } // namespace 50 | 51 | namespace os 52 | { 53 | NativePcStateHandler::NativePcStateHandler() 54 | : m_privilege_acquired{acquirePrivilege()} 55 | { 56 | if (!m_privilege_acquired) 57 | { 58 | qCWarning(lc::os) << "failed to acquire shutdown/restart/suspend privilege!"; 59 | } 60 | } 61 | 62 | bool NativePcStateHandler::canShutdownPC() 63 | { 64 | return m_privilege_acquired; 65 | } 66 | 67 | bool NativePcStateHandler::canRestartPC() 68 | { 69 | return m_privilege_acquired; 70 | } 71 | 72 | bool NativePcStateHandler::canSuspendPC() 73 | { 74 | return m_privilege_acquired; 75 | } 76 | 77 | bool NativePcStateHandler::canHibernatePC() 78 | { 79 | return m_privilege_acquired; 80 | } 81 | 82 | bool NativePcStateHandler::shutdownPC() 83 | { 84 | if (!canShutdownPC()) 85 | { 86 | return false; 87 | } 88 | 89 | if (InitiateSystemShutdownW(nullptr, nullptr, 0, TRUE, FALSE) == FALSE) 90 | { 91 | qCWarning(lc::os) << "InitiateSystemShutdownW (shutdown) failed! Reason:" << lc::getErrorString(GetLastError()); 92 | return false; 93 | } 94 | 95 | return true; 96 | } 97 | 98 | bool NativePcStateHandler::restartPC() 99 | { 100 | if (!canRestartPC()) 101 | { 102 | return false; 103 | } 104 | 105 | if (InitiateSystemShutdownW(nullptr, nullptr, 0, TRUE, TRUE) == FALSE) 106 | { 107 | qCWarning(lc::os) << "InitiateSystemShutdownW (restart) failed! Reason:" << lc::getErrorString(GetLastError()); 108 | return false; 109 | } 110 | 111 | return true; 112 | } 113 | 114 | bool NativePcStateHandler::suspendPC() 115 | { 116 | if (!canSuspendPC()) 117 | { 118 | return false; 119 | } 120 | 121 | if (SetSuspendState(FALSE, TRUE, FALSE) == FALSE) 122 | { 123 | qCWarning(lc::os) << "SetSuspendState failed! Reason:" << lc::getErrorString(GetLastError()); 124 | return false; 125 | } 126 | 127 | return true; 128 | } 129 | 130 | bool NativePcStateHandler::hibernatePC() 131 | { 132 | if (!canHibernatePC()) 133 | { 134 | return false; 135 | } 136 | 137 | if (SetSuspendState(TRUE, TRUE, FALSE) == FALSE) 138 | { 139 | qCWarning(lc::os) << "SetSuspendState failed! Reason:" << lc::getErrorString(GetLastError()); 140 | return false; 141 | } 142 | 143 | return true; 144 | } 145 | } // namespace os 146 | -------------------------------------------------------------------------------- /src/lib/os/win/nativeprocesshandler.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/win/nativeprocesshandler.h" 3 | 4 | // A SEPARATE WINDOWS INCLUDE BECAUSE OF THE SMELL! 5 | #include 6 | 7 | // system/Qt includes 8 | #include 9 | 10 | // local includes 11 | #include "shared/loggingcategories.h" 12 | 13 | #include 14 | 15 | namespace 16 | { 17 | template 18 | auto useProcHandle(const uint pid, Getter&& getter) 19 | { 20 | HANDLE proc_handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, static_cast(pid)); 21 | auto cleanup = qScopeGuard( 22 | [&]() 23 | { 24 | if (proc_handle != nullptr) 25 | { 26 | CloseHandle(proc_handle); 27 | } 28 | }); 29 | 30 | return getter(proc_handle); 31 | } 32 | } // namespace 33 | 34 | namespace os 35 | { 36 | std::vector NativeProcessHandler::getPids() const 37 | { 38 | std::vector handles; 39 | for (auto i = 0; i < 3; ++i) 40 | { 41 | const int handle_size{1024}; 42 | // NOLINTNEXTLINE(*misplaced-widening-cast) 43 | handles.resize(static_cast(handle_size * (i + 1))); 44 | 45 | DWORD bytes_needed{0}; 46 | const DWORD buffer_in_bytes{static_cast(handles.size() * sizeof(decltype(handles)::value_type))}; 47 | 48 | if (EnumProcesses(handles.data(), buffer_in_bytes, &bytes_needed) == FALSE) 49 | { 50 | qFatal("Failed get a list of running processes! Reason: %s", 51 | qUtf8Printable(lc::getErrorString(GetLastError()))); 52 | } 53 | 54 | if (buffer_in_bytes == bytes_needed) 55 | { 56 | continue; 57 | } 58 | 59 | handles.resize(bytes_needed / sizeof(decltype(handles)::value_type)); 60 | 61 | std::vector pids; 62 | std::transform(std::cbegin(handles), std::cend(handles), std::back_inserter(pids), 63 | [](const DWORD handle) { return static_cast(handle); }); 64 | return pids; 65 | } 66 | 67 | qFatal("Failed get a list of running processes after 3 tries!"); 68 | return {}; 69 | } 70 | 71 | QString NativeProcessHandler::getExecPath(uint pid) const 72 | { 73 | return useProcHandle( 74 | pid, 75 | [pid](HANDLE handle) 76 | { 77 | static_assert(sizeof(wchar_t) == sizeof(char16_t), "Wide char is not 2 bytes :/"); 78 | 79 | if (!handle) 80 | { 81 | return QString{}; 82 | } 83 | 84 | DWORD data_written{MAX_PATH}; 85 | std::array buffer{}; 86 | 87 | const BOOL result = QueryFullProcessImageNameW(handle, 0, 88 | // NOLINTNEXTLINE(*-reinterpret-cast) 89 | reinterpret_cast(buffer.data()), &data_written); 90 | if (result == TRUE) 91 | { 92 | if (data_written > 0) 93 | { 94 | auto path{QString::fromUtf16(buffer.data(), data_written)}; 95 | qDebug(lc::osVerbose) << "QueryFullProcessImageNameW - PID:" << pid << "| Path:" << path; 96 | return path; 97 | } 98 | else 99 | { 100 | qDebug(lc::osVerbose) << "QueryFullProcessImageNameW failed to write data - PID:" << pid; 101 | } 102 | } 103 | else 104 | { 105 | qDebug(lc::osVerbose) << "QueryFullProcessImageNameW failed - PID:" << pid 106 | << "| ERROR:" << lc::getErrorString(GetLastError()); 107 | } 108 | 109 | return QString{}; 110 | }); 111 | } 112 | 113 | QDateTime NativeProcessHandler::getStartTime(uint pid) const 114 | { 115 | return useProcHandle(pid, 116 | [pid](HANDLE handle) 117 | { 118 | static_assert(sizeof(wchar_t) == sizeof(char16_t), "Wide char is not 2 bytes :/"); 119 | 120 | if (!handle) 121 | { 122 | return QDateTime{}; 123 | } 124 | 125 | FILETIME start_time{}; 126 | FILETIME exit_time{}; 127 | FILETIME kernel_time{}; 128 | FILETIME user_time{}; 129 | BOOL result = GetProcessTimes(handle, &start_time, &exit_time, &kernel_time, &user_time); 130 | 131 | if (result == TRUE) 132 | { 133 | SYSTEMTIME time_utc{}; 134 | result = FileTimeToSystemTime(&start_time, &time_utc); 135 | if (result == TRUE) 136 | { 137 | QDateTime datetime{QDate{time_utc.wYear, time_utc.wMonth, time_utc.wDay}, 138 | QTime{time_utc.wHour, time_utc.wMinute, time_utc.wSecond, 139 | time_utc.wMilliseconds}, 140 | QTimeZone::UTC}; 141 | datetime = datetime.toLocalTime(); 142 | qDebug(lc::osVerbose) 143 | << "FileTimeToSystemTime PID:" << pid << "| Time:" << datetime; 144 | return datetime; 145 | } 146 | 147 | qDebug(lc::osVerbose) << "FileTimeToSystemTime failed - PID:" << pid 148 | << "| ERROR:" << lc::getErrorString(GetLastError()); 149 | } 150 | else 151 | { 152 | qDebug(lc::osVerbose) << "GetProcessTimes failed - PID:" << pid 153 | << "| ERROR:" << lc::getErrorString(GetLastError()); 154 | } 155 | 156 | return QDateTime{}; 157 | }); 158 | } 159 | 160 | void NativeProcessHandler::close(uint pid) const 161 | { 162 | std::vector hwnds; 163 | { 164 | HWND hwnd{nullptr}; 165 | do 166 | { 167 | DWORD owner_pid{0}; 168 | 169 | hwnd = FindWindowExW(nullptr, hwnd, nullptr, nullptr); 170 | GetWindowThreadProcessId(hwnd, &owner_pid); 171 | 172 | if (static_cast(pid) == owner_pid) 173 | { 174 | hwnds.push_back(hwnd); 175 | } 176 | } while (hwnd != nullptr); 177 | } 178 | 179 | for (const auto& hwnd : hwnds) 180 | { 181 | if (PostMessageW(hwnd, WM_CLOSE, 0, 0) == FALSE) 182 | { 183 | qCDebug(lc::os).nospace() << "Failed to post message to process (pid: " << pid 184 | << ")! Reason: " << lc::getErrorString(GetLastError()); 185 | } 186 | } 187 | } 188 | 189 | void NativeProcessHandler::terminate(uint pid) const 190 | { 191 | HANDLE proc_handle = OpenProcess(PROCESS_TERMINATE, FALSE, static_cast(pid)); 192 | auto cleanup = qScopeGuard( 193 | [&]() 194 | { 195 | if (proc_handle != nullptr) 196 | { 197 | CloseHandle(proc_handle); 198 | } 199 | }); 200 | 201 | if (proc_handle == nullptr || TerminateProcess(proc_handle, 1) == FALSE) 202 | { 203 | qCWarning(lc::os).nospace() << "Failed to terminate process (pid: " << pid 204 | << ")! Reason: " << lc::getErrorString(GetLastError()); 205 | } 206 | } 207 | } // namespace os 208 | -------------------------------------------------------------------------------- /src/lib/os/win/nativesleepinhibitor.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "os/win/nativesleepinhibitor.h" 3 | 4 | namespace os 5 | { 6 | NativeSleepInhibitor::NativeSleepInhibitor(const QString& /* app_name */) 7 | { 8 | } 9 | } // namespace os 10 | -------------------------------------------------------------------------------- /src/lib/server/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------------------------------------------------------------------- 2 | # Lib config 3 | #---------------------------------------------------------------------------------------------------------------------- 4 | 5 | set(LIBNAME serverlib) 6 | 7 | #---------------------------------------------------------------------------------------------------------------------- 8 | # External dependencies 9 | #---------------------------------------------------------------------------------------------------------------------- 10 | 11 | find_package(Qt6 COMPONENTS Core HttpServer REQUIRED) 12 | qt_standard_project_setup() 13 | 14 | #---------------------------------------------------------------------------------------------------------------------- 15 | # Header/Source files 16 | #---------------------------------------------------------------------------------------------------------------------- 17 | 18 | file(GLOB HEADERS CONFIGURE_DEPENDS "include/server/*.h") 19 | file(GLOB SOURCES CONFIGURE_DEPENDS "*.cpp") 20 | 21 | #---------------------------------------------------------------------------------------------------------------------- 22 | # Target config 23 | #---------------------------------------------------------------------------------------------------------------------- 24 | 25 | add_library(${LIBNAME} ${HEADERS} ${SOURCES}) 26 | target_link_libraries(${LIBNAME} PRIVATE Qt6::Core Qt6::HttpServer sharedlib) 27 | target_include_directories(${LIBNAME} PUBLIC include) 28 | -------------------------------------------------------------------------------- /src/lib/server/clientids.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "server/clientids.h" 3 | 4 | // system/Qt includes 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | // local includes 12 | #include "shared/loggingcategories.h" 13 | 14 | namespace server 15 | { 16 | ClientIds::ClientIds(QString filepath) 17 | : m_filepath{std::move(filepath)} 18 | { 19 | } 20 | 21 | // NOLINTNEXTLINE(*-cognitive-complexity) 22 | void ClientIds::load() 23 | { 24 | m_ids.clear(); // Clear the ids regardless of whether the file exists or not 25 | 26 | QFile ids_file{m_filepath}; 27 | if (ids_file.exists()) 28 | { 29 | if (!ids_file.open(QFile::ReadOnly)) 30 | { 31 | qFatal("File exists, but could not be opened: \"%s\"", qUtf8Printable(m_filepath)); 32 | } 33 | 34 | const QByteArray data = ids_file.readAll(); 35 | 36 | QJsonParseError parser_error; 37 | const QJsonDocument json_data{QJsonDocument::fromJson(data, &parser_error)}; 38 | if (json_data.isNull()) 39 | { 40 | qFatal("Failed to decode JSON data! Reason: %s. Read data: %s", qUtf8Printable(parser_error.errorString()), 41 | qUtf8Printable(data)); 42 | } 43 | else if (!json_data.isEmpty()) 44 | { 45 | if (!json_data.isArray()) 46 | { 47 | qFatal("Client Ids file contains invalid JSON data!"); 48 | } 49 | 50 | bool some_ids_were_skipped{false}; 51 | const QJsonArray ids = json_data.array(); 52 | for (const auto& client_id : ids) 53 | { 54 | const QString client_id_string{client_id.isString() ? client_id.toString() : QString{}}; 55 | if (client_id_string.isEmpty()) 56 | { 57 | some_ids_were_skipped = true; 58 | continue; 59 | } 60 | 61 | m_ids.emplace(client_id_string); 62 | } 63 | 64 | if (some_ids_were_skipped) 65 | { 66 | qCWarning(lc::server) << "Client Ids file contained ids that were skipped!"; 67 | } 68 | } 69 | } 70 | } 71 | 72 | void ClientIds::save() 73 | { 74 | QJsonArray json_data; 75 | for (const auto& client_id : m_ids) 76 | { 77 | json_data.append(client_id); 78 | } 79 | 80 | QFile file{m_filepath}; 81 | if (!file.exists()) 82 | { 83 | const QFileInfo info(m_filepath); 84 | const QDir dir; 85 | if (!dir.mkpath(info.absolutePath())) 86 | { 87 | qFatal("Failed at mkpath: \"%s\".", qUtf8Printable(m_filepath)); 88 | } 89 | } 90 | 91 | if (!file.open(QFile::WriteOnly)) 92 | { 93 | qFatal("File could not be opened for writing: \"%s\".", qUtf8Printable(m_filepath)); 94 | } 95 | 96 | const QJsonDocument file_data{json_data}; 97 | file.write(file_data.toJson(QJsonDocument::Indented)); 98 | qCInfo(lc::server) << "Finished saving:" << m_filepath; 99 | } 100 | 101 | bool ClientIds::containsId(const QString& client_id) const 102 | { 103 | return m_ids.contains(client_id); 104 | } 105 | 106 | void ClientIds::addId(const QString& client_id) 107 | { 108 | m_ids.emplace(client_id); 109 | } 110 | 111 | void ClientIds::removeId(const QString& client_id) 112 | { 113 | m_ids.erase(client_id); 114 | } 115 | } // namespace server -------------------------------------------------------------------------------- /src/lib/server/httpserver.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "server/httpserver.h" 3 | 4 | // system/Qt includes 5 | #include 6 | #include 7 | #include 8 | 9 | // local includes 10 | #include "server/clientids.h" 11 | #include "shared/loggingcategories.h" 12 | 13 | namespace server 14 | { 15 | QString HttpServer::getAuthorizationId(const QHttpServerRequest& request) 16 | { 17 | auto auth = request.value("authorization").simplified(); 18 | 19 | const int id_start_index{6}; 20 | if (auth.size() > id_start_index && auth.first(id_start_index).toLower() == "basic ") 21 | { 22 | auto token = auth.sliced(id_start_index); 23 | auto client_id = QByteArray::fromBase64(token); 24 | 25 | if (!client_id.isEmpty()) 26 | { 27 | return client_id; 28 | } 29 | } 30 | 31 | return {}; 32 | } 33 | 34 | HttpServer::HttpServer(int api_version, ClientIds& client_ids) 35 | : m_api_version{api_version} 36 | , m_client_ids{client_ids} 37 | { 38 | Q_UNUSED(m_client_ids) 39 | } 40 | 41 | bool HttpServer::startServer(quint16 port, const QString& ssl_cert_file, const QString& ssl_key_file, 42 | QSsl::SslProtocol protocol) 43 | { 44 | auto ssl_server = std::make_unique(); 45 | { 46 | QFile cert_file{ssl_cert_file}; 47 | if (!cert_file.open(QFile::ReadOnly)) 48 | { 49 | qCWarning(lc::server) << "Failed to load SSL certificate from" << ssl_cert_file; 50 | return false; 51 | } 52 | 53 | QFile key_file{ssl_key_file}; 54 | if (!key_file.open(QFile::ReadOnly)) 55 | { 56 | qCWarning(lc::server) << "Failed to load SSL key from" << ssl_key_file; 57 | return false; 58 | } 59 | 60 | QSslConfiguration ssl_conf{QSslConfiguration::defaultConfiguration()}; 61 | ssl_conf.setLocalCertificate(QSslCertificate{cert_file.readAll()}); 62 | ssl_conf.setPrivateKey(QSslKey{key_file.readAll(), QSsl::Rsa}); 63 | ssl_conf.setProtocol(protocol); 64 | 65 | ssl_server->setSslConfiguration(ssl_conf); 66 | } 67 | 68 | if (!ssl_server->listen(QHostAddress::Any, port)) 69 | { 70 | qCWarning(lc::server) << "Server could not start listening at port" << port; 71 | return false; 72 | } 73 | 74 | if (!m_server.bind(ssl_server.get())) 75 | { 76 | qCWarning(lc::server) << "Failed to bind the ssl server!"; 77 | return false; 78 | } 79 | ssl_server.release(); // m_server has taken over the ownership! 80 | 81 | qCInfo(lc::server) << "Server started listening at port" << port; 82 | return true; 83 | } 84 | 85 | int HttpServer::getApiVersion() const 86 | { 87 | return m_api_version; 88 | } 89 | 90 | bool HttpServer::isAuthorized(const QHttpServerRequest& request) const 91 | { 92 | return m_client_ids.containsId(getAuthorizationId(request)); 93 | } 94 | } // namespace server -------------------------------------------------------------------------------- /src/lib/server/include/server/clientids.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | #include 6 | 7 | namespace server 8 | { 9 | class ClientIds 10 | { 11 | Q_DISABLE_COPY(ClientIds) 12 | 13 | public: 14 | explicit ClientIds(QString filepath); 15 | virtual ~ClientIds() = default; 16 | 17 | void load(); 18 | void save(); 19 | 20 | bool containsId(const QString& client_id) const; 21 | void addId(const QString& client_id); 22 | void removeId(const QString& client_id); 23 | 24 | private: 25 | QString m_filepath; 26 | std::set m_ids; 27 | }; 28 | } // namespace server 29 | -------------------------------------------------------------------------------- /src/lib/server/include/server/httpserver.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | 6 | // forward declaration 7 | namespace server 8 | { 9 | class ClientIds; 10 | } 11 | 12 | namespace server 13 | { 14 | class HttpServer 15 | { 16 | Q_DISABLE_COPY(HttpServer) 17 | 18 | public: 19 | static QString getAuthorizationId(const QHttpServerRequest& request); 20 | 21 | explicit HttpServer(int api_version, ClientIds& client_ids); 22 | virtual ~HttpServer() = default; 23 | 24 | bool startServer(quint16 port, const QString& ssl_cert_file, const QString& ssl_key_file, 25 | QSsl::SslProtocol protocol); 26 | 27 | int getApiVersion() const; 28 | bool isAuthorized(const QHttpServerRequest& request) const; 29 | 30 | template 31 | bool route(const QString& path_pattern, QHttpServerRequest::Methods method, Functor&& functor); 32 | 33 | template 34 | void afterRequest(ViewHandler&& view_handler); 35 | 36 | private: 37 | int m_api_version; 38 | ClientIds& m_client_ids; 39 | QHttpServer m_server; 40 | }; 41 | 42 | template 43 | bool HttpServer::route(const QString& path_pattern, QHttpServerRequest::Methods method, Functor&& functor) 44 | { 45 | static_assert(!std::is_member_function_pointer_v, "Member function pointer are not allowed!"); 46 | return m_server.route(path_pattern, method, std::forward(functor)); 47 | } 48 | 49 | template 50 | void HttpServer::afterRequest(ViewHandler&& view_handler) 51 | { 52 | return m_server.addAfterRequestHandler(&m_server, std::forward(view_handler)); 53 | } 54 | } // namespace server 55 | -------------------------------------------------------------------------------- /src/lib/server/include/server/pairingmanager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | 6 | // forward declaration 7 | namespace server 8 | { 9 | class ClientIds; 10 | } 11 | 12 | namespace server 13 | { 14 | class PairingManager : public QObject 15 | { 16 | Q_OBJECT 17 | 18 | public: 19 | explicit PairingManager(ClientIds& client_ids); 20 | ~PairingManager() override = default; 21 | 22 | bool isPaired(const QString& client_id) const; 23 | bool isPairing(const QString& client_id) const; 24 | bool isPairing() const; 25 | 26 | bool startPairing(const QString& id, const QString& hashed_id); 27 | bool abortPairing(const QString& id); 28 | 29 | signals: 30 | void signalRequestUserInputForPairing(); 31 | void signalAbortPairing(); 32 | 33 | public slots: 34 | void slotFinishPairing(uint pin); 35 | void slotPairingRejected(); 36 | 37 | private: 38 | struct PairingData 39 | { 40 | QString m_id; 41 | QString m_hashed_id; 42 | }; 43 | 44 | ClientIds& m_client_ids; 45 | std::optional m_pairing_data; 46 | }; 47 | } // namespace server 48 | -------------------------------------------------------------------------------- /src/lib/server/pairingmanager.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "server/pairingmanager.h" 3 | 4 | // local includes 5 | #include "server/clientids.h" 6 | #include "shared/loggingcategories.h" 7 | 8 | namespace server 9 | { 10 | PairingManager::PairingManager(ClientIds& client_ids) 11 | : m_client_ids{client_ids} 12 | { 13 | } 14 | 15 | bool PairingManager::isPaired(const QString& client_id) const 16 | { 17 | return m_client_ids.containsId(client_id); 18 | } 19 | 20 | bool PairingManager::isPairing(const QString& client_id) const 21 | { 22 | return m_pairing_data && m_pairing_data->m_id == client_id; 23 | } 24 | 25 | bool PairingManager::isPairing() const 26 | { 27 | return static_cast(m_pairing_data); 28 | } 29 | 30 | // NOLINTNEXTLINE(*-identifier-length) 31 | bool PairingManager::startPairing(const QString& id, const QString& hashed_id) 32 | { 33 | if (m_pairing_data) 34 | { 35 | qCWarning(lc::server) << "Cannot start pairing as" << m_pairing_data->m_id << "is currently being paired!"; 36 | return false; 37 | } 38 | 39 | if (id.isEmpty() || hashed_id.isEmpty()) 40 | { 41 | qCWarning(lc::server) << "Invalid id or hashed_id provided for pairing!"; 42 | return false; 43 | } 44 | 45 | if (m_client_ids.containsId(id)) 46 | { 47 | qCWarning(lc::server) << "Id" << id << "is already paired!"; 48 | return false; 49 | } 50 | 51 | m_pairing_data = {id, hashed_id}; 52 | emit signalRequestUserInputForPairing(); 53 | return true; 54 | } 55 | 56 | // NOLINTNEXTLINE(*-identifier-length) 57 | bool PairingManager::abortPairing(const QString& id) 58 | { 59 | if (!isPairing()) 60 | { 61 | return true; 62 | } 63 | 64 | if (!isPairing(id)) 65 | { 66 | qCWarning(lc::server) << "Cannot abort pairing for other id than" << id; 67 | return false; 68 | } 69 | 70 | qCDebug(lc::server) << "Aborting pairing for" << id; 71 | emit signalAbortPairing(); 72 | m_pairing_data = std::nullopt; 73 | return true; 74 | } 75 | 76 | void PairingManager::slotFinishPairing(uint pin) 77 | { 78 | if (!m_pairing_data) 79 | { 80 | qCWarning(lc::server) << "Pairing is not in progress!"; 81 | return; 82 | } 83 | 84 | const QString string_for_hashing{m_pairing_data->m_id + QString::number(pin)}; 85 | if (string_for_hashing.toUtf8().toBase64() != m_pairing_data->m_hashed_id) 86 | { 87 | qCWarning(lc::server) << "Pairing code does not match."; 88 | return; 89 | } 90 | 91 | m_client_ids.addId(m_pairing_data->m_id); 92 | m_client_ids.save(); 93 | 94 | m_pairing_data = std::nullopt; 95 | } 96 | 97 | void PairingManager::slotPairingRejected() 98 | { 99 | qCDebug(lc::server) << "Pairing was rejected."; 100 | m_pairing_data = std::nullopt; 101 | } 102 | 103 | } // namespace server 104 | -------------------------------------------------------------------------------- /src/lib/shared/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------------------------------------------------------------------- 2 | # Lib config 3 | #---------------------------------------------------------------------------------------------------------------------- 4 | 5 | set(LIBNAME sharedlib) 6 | 7 | #---------------------------------------------------------------------------------------------------------------------- 8 | # External dependencies 9 | #---------------------------------------------------------------------------------------------------------------------- 10 | 11 | find_package(Qt6 COMPONENTS Core REQUIRED) 12 | qt_standard_project_setup() 13 | 14 | #---------------------------------------------------------------------------------------------------------------------- 15 | # Header/Source files 16 | #---------------------------------------------------------------------------------------------------------------------- 17 | 18 | file(GLOB HEADERS CONFIGURE_DEPENDS "include/shared/*.h") 19 | file(GLOB SOURCES CONFIGURE_DEPENDS "*.cpp") 20 | 21 | #---------------------------------------------------------------------------------------------------------------------- 22 | # Target config 23 | #---------------------------------------------------------------------------------------------------------------------- 24 | 25 | add_library(${LIBNAME} ${HEADERS} ${SOURCES}) 26 | target_link_libraries(${LIBNAME} PRIVATE Qt6::Core) 27 | target_include_directories(${LIBNAME} PUBLIC include) 28 | -------------------------------------------------------------------------------- /src/lib/shared/appmetadata.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "shared/appmetadata.h" 3 | 4 | // system/Qt includes 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | // local includes 12 | #include "shared/loggingcategories.h" 13 | 14 | namespace 15 | { 16 | #if defined(Q_OS_LINUX) 17 | QString getConfigDir() 18 | { 19 | const auto xdg_config_env = qgetenv("XDG_CONFIG_HOME"); 20 | if (!xdg_config_env.isEmpty()) 21 | { 22 | const QDir xdg_cnnfig_dir{xdg_config_env}; 23 | if (xdg_cnnfig_dir.exists()) 24 | { 25 | return xdg_cnnfig_dir.absolutePath(); 26 | } 27 | } 28 | 29 | return QDir::cleanPath(QDir::homePath() + "/.config"); 30 | } 31 | 32 | QString getAppFilePath() 33 | { 34 | const auto app_image_env = qgetenv("APPIMAGE"); 35 | if (!app_image_env.isEmpty()) 36 | { 37 | return QString{app_image_env}; 38 | } 39 | 40 | return QCoreApplication::applicationFilePath(); 41 | } 42 | #endif 43 | } // namespace 44 | 45 | namespace shared 46 | { 47 | AppMetadata::AppMetadata(App app) 48 | : m_current_app{app} 49 | { 50 | // Delay logging until application start 51 | QTimer::singleShot(0, this, 52 | [this]() 53 | { 54 | qCDebug(lc::shared) << "getAppName() >> " << getAppName(); 55 | qCDebug(lc::shared) << "getLogDir() >> " << getLogDir(); 56 | qCDebug(lc::shared) << "getLogName() >> " << getLogName(); 57 | qCDebug(lc::shared) << "getLogPath() >> " << getLogPath(); 58 | qCDebug(lc::shared) << "getSettingsDir() >> " << getSettingsDir(); 59 | qCDebug(lc::shared) << "getSettingsName() >> " << getSettingsName(); 60 | qCDebug(lc::shared) << "getSettingsPath() >> " << getSettingsPath(); 61 | qCDebug(lc::shared) << "getAutoStartDir() >> " << getAutoStartDir(); 62 | qCDebug(lc::shared) << "getAutoStartPath() >> " << getAutoStartPath(); 63 | qCDebug(lc::shared) << "getAutoStartExec() >> " << getAutoStartExec(); 64 | }); 65 | } 66 | 67 | QString AppMetadata::getAppName() const 68 | { 69 | return getAppName(m_current_app); 70 | } 71 | 72 | // NOLINTNEXTLINE(*-static) 73 | QString AppMetadata::getAppName(App app) const 74 | { 75 | switch (app) 76 | { 77 | case App::Buddy: 78 | return QStringLiteral("MoonDeckBuddy"); 79 | case App::Stream: 80 | return QStringLiteral("MoonDeckStream"); 81 | } 82 | 83 | Q_ASSERT(false); 84 | return {}; 85 | } 86 | 87 | // NOLINTNEXTLINE(*-static) 88 | QString AppMetadata::getLogDir() const 89 | { 90 | #if defined(Q_OS_WIN) 91 | Q_ASSERT(QCoreApplication::instance() != nullptr); 92 | return QCoreApplication::applicationDirPath(); 93 | #elif defined(Q_OS_LINUX) 94 | return QStringLiteral("/tmp"); 95 | #else 96 | #error OS is not supported! 97 | #endif 98 | } 99 | 100 | QString AppMetadata::getLogName() const 101 | { 102 | return getAppName().toLower() + QStringLiteral(".log"); 103 | } 104 | 105 | QString AppMetadata::getLogPath() const 106 | { 107 | return QDir::cleanPath(getLogDir() + "/" + getLogName()); 108 | } 109 | 110 | // NOLINTNEXTLINE(*-static) 111 | QString AppMetadata::getSettingsDir() const 112 | { 113 | #if defined(Q_OS_WIN) 114 | Q_ASSERT(QCoreApplication::instance() != nullptr); 115 | return QCoreApplication::applicationDirPath(); 116 | #elif defined(Q_OS_LINUX) 117 | return QDir::cleanPath(getConfigDir() + "/" + getAppName(App::Buddy).toLower()); 118 | #else 119 | #error OS is not supported! 120 | #endif 121 | } 122 | 123 | // NOLINTNEXTLINE(*-static) 124 | QString AppMetadata::getSettingsName() const 125 | { 126 | return QStringLiteral("settings.json"); 127 | } 128 | 129 | QString AppMetadata::getSettingsPath() const 130 | { 131 | return QDir::cleanPath(getSettingsDir() + "/" + getSettingsName()); 132 | } 133 | 134 | // NOLINTNEXTLINE(*-static) 135 | QString AppMetadata::getAutoStartDir() const 136 | { 137 | #if defined(Q_OS_WIN) 138 | Q_ASSERT(QCoreApplication::instance() != nullptr); 139 | return QDir::cleanPath(QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + "/Startup"); 140 | #elif defined(Q_OS_LINUX) 141 | return QDir::cleanPath(getConfigDir() + "/autostart"); 142 | #else 143 | #error OS is not supported! 144 | #endif 145 | } 146 | 147 | QString AppMetadata::getAutoStartName() const 148 | { 149 | #if defined(Q_OS_WIN) 150 | return getAppName() + ".lnk"; 151 | #elif defined(Q_OS_LINUX) 152 | return getAppName().toLower() + ".desktop"; 153 | #else 154 | #error OS is not supported! 155 | #endif 156 | } 157 | 158 | QString AppMetadata::getAutoStartPath() const 159 | { 160 | return QDir::cleanPath(getAutoStartDir() + "/" + getAutoStartName()); 161 | } 162 | 163 | // NOLINTNEXTLINE(*-static) 164 | QString AppMetadata::getAutoStartExec() const 165 | { 166 | Q_ASSERT(QCoreApplication::instance() != nullptr); 167 | #if defined(Q_OS_WIN) 168 | return QCoreApplication::applicationFilePath(); 169 | #elif defined(Q_OS_LINUX) 170 | return getAppFilePath(); 171 | #else 172 | #error OS is not supported! 173 | #endif 174 | } 175 | 176 | QString AppMetadata::getDefaultSteamExecutable() const 177 | { 178 | #if defined(Q_OS_WIN) 179 | const QSettings settings(R"(HKEY_CURRENT_USER\Software\Valve\Steam)", QSettings::NativeFormat); 180 | return settings.value("SteamExe").toString(); 181 | #elif defined(Q_OS_LINUX) 182 | return "/usr/bin/steam"; 183 | #else 184 | #error OS is not supported! 185 | #endif 186 | } 187 | } // namespace shared -------------------------------------------------------------------------------- /src/lib/shared/include/shared/appmetadata.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | 6 | namespace shared 7 | { 8 | class AppMetadata final : public QObject 9 | { 10 | public: 11 | enum class App 12 | { 13 | Buddy, 14 | Stream 15 | }; 16 | 17 | explicit AppMetadata(App app); 18 | ~AppMetadata() override = default; 19 | 20 | QString getAppName() const; 21 | QString getAppName(App app) const; 22 | 23 | QString getLogDir() const; 24 | QString getLogName() const; 25 | QString getLogPath() const; 26 | 27 | QString getSettingsDir() const; 28 | QString getSettingsName() const; 29 | QString getSettingsPath() const; 30 | 31 | QString getAutoStartDir() const; 32 | QString getAutoStartName() const; 33 | QString getAutoStartPath() const; 34 | QString getAutoStartExec() const; 35 | 36 | QString getDefaultSteamExecutable() const; 37 | 38 | private: 39 | App m_current_app; 40 | }; 41 | } // namespace shared -------------------------------------------------------------------------------- /src/lib/shared/include/shared/enums.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | 6 | namespace enums 7 | { 8 | Q_NAMESPACE 9 | 10 | enum class SteamUiMode 11 | { 12 | Unknown, 13 | Desktop, 14 | BigPicture 15 | }; 16 | Q_ENUM_NS(SteamUiMode) 17 | 18 | enum class AppState 19 | { 20 | Stopped, 21 | Running, 22 | Updating 23 | }; 24 | Q_ENUM_NS(AppState) 25 | 26 | enum class PcState 27 | { 28 | Normal, 29 | Restarting, 30 | ShuttingDown, 31 | Suspending 32 | }; 33 | Q_ENUM_NS(PcState) 34 | 35 | enum class StreamState 36 | { 37 | NotStreaming, 38 | Streaming, 39 | StreamEnding 40 | }; 41 | Q_ENUM_NS(StreamState) 42 | } // namespace enums -------------------------------------------------------------------------------- /src/lib/shared/include/shared/loggingcategories.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | #include 6 | #include 7 | 8 | //! LC - logging categories 9 | namespace lc 10 | { 11 | template 12 | QString qEnumToString(const T value) 13 | { 14 | return QMetaEnum::fromType().valueToKey(static_cast(value)); 15 | } 16 | 17 | QString getErrorString(auto&& error) 18 | { 19 | return QString::fromStdString(std::system_category().message(static_cast(error))); 20 | } 21 | 22 | Q_DECLARE_LOGGING_CATEGORY(buddyMain); 23 | Q_DECLARE_LOGGING_CATEGORY(streamMain); 24 | Q_DECLARE_LOGGING_CATEGORY(server); 25 | Q_DECLARE_LOGGING_CATEGORY(shared); 26 | Q_DECLARE_LOGGING_CATEGORY(utils); 27 | Q_DECLARE_LOGGING_CATEGORY(os); 28 | Q_DECLARE_LOGGING_CATEGORY(osVerbose); 29 | } // namespace lc -------------------------------------------------------------------------------- /src/lib/shared/loggingcategories.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "shared/loggingcategories.h" 3 | 4 | namespace lc 5 | { 6 | Q_LOGGING_CATEGORY(buddyMain, "buddy.main", QtInfoMsg); 7 | Q_LOGGING_CATEGORY(streamMain, "buddy.stream", QtInfoMsg); 8 | Q_LOGGING_CATEGORY(server, "buddy.server", QtInfoMsg); 9 | Q_LOGGING_CATEGORY(shared, "buddy.shared", QtInfoMsg); 10 | Q_LOGGING_CATEGORY(utils, "buddy.utils", QtInfoMsg); 11 | Q_LOGGING_CATEGORY(os, "buddy.os", QtInfoMsg); 12 | Q_LOGGING_CATEGORY(osVerbose, "buddy.os.verbose", QtInfoMsg); 13 | } // namespace lc -------------------------------------------------------------------------------- /src/lib/utils/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------------------------------------------------------------------- 2 | # Lib config 3 | #---------------------------------------------------------------------------------------------------------------------- 4 | 5 | set(LIBNAME utilslib) 6 | 7 | #---------------------------------------------------------------------------------------------------------------------- 8 | # External dependencies 9 | #---------------------------------------------------------------------------------------------------------------------- 10 | 11 | find_package(Qt6 REQUIRED COMPONENTS Core Widgets) 12 | qt_standard_project_setup() 13 | 14 | #---------------------------------------------------------------------------------------------------------------------- 15 | # Header/Source files 16 | #---------------------------------------------------------------------------------------------------------------------- 17 | 18 | file(GLOB HEADERS CONFIGURE_DEPENDS "include/utils/*.h") 19 | file(GLOB SOURCES CONFIGURE_DEPENDS "*.cpp") 20 | 21 | #---------------------------------------------------------------------------------------------------------------------- 22 | # Target config 23 | #---------------------------------------------------------------------------------------------------------------------- 24 | 25 | add_library(${LIBNAME} ${HEADERS} ${SOURCES}) 26 | target_link_libraries(${LIBNAME} PRIVATE Qt6::Core Qt6::Widgets sharedlib) 27 | target_include_directories(${LIBNAME} PUBLIC include) 28 | -------------------------------------------------------------------------------- /src/lib/utils/heartbeat.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "utils/heartbeat.h" 3 | 4 | // system/Qt includes 5 | #include 6 | #include 7 | #include 8 | 9 | namespace 10 | { 11 | QString generateKeyHash(const QString& key, const QString& salt) 12 | { 13 | QByteArray data; 14 | 15 | data.append(key.toUtf8()); 16 | data.append(salt.toUtf8()); 17 | data = QCryptographicHash::hash(data, QCryptographicHash::Sha1).toHex(); 18 | 19 | return data; 20 | } 21 | 22 | constexpr int TIME_INDEX{0}; 23 | constexpr int TERMINATE_INDEX{1}; 24 | constexpr int HEARTBEAT_INTERVAL{250}; 25 | constexpr int HEARTBEAT_TIMEOUT{2000}; 26 | 27 | class HeartbeatAccessor final 28 | { 29 | Q_DISABLE_COPY(HeartbeatAccessor) 30 | 31 | public: 32 | explicit HeartbeatAccessor(QSharedMemory& shared_mem); 33 | ~HeartbeatAccessor(); 34 | 35 | void setTime(const QDateTime& time); 36 | QDateTime getTime() const; 37 | 38 | void setShouldTerminate(bool terminate); 39 | bool getShouldTerminate() const; 40 | 41 | private: 42 | QSharedMemory& m_shared_mem; 43 | }; 44 | 45 | HeartbeatAccessor::HeartbeatAccessor(QSharedMemory& shared_mem) 46 | : m_shared_mem{shared_mem} 47 | { 48 | if (!m_shared_mem.lock()) 49 | { 50 | qFatal("Failed to lock shared memory %s", qUtf8Printable(m_shared_mem.key())); 51 | } 52 | } 53 | 54 | HeartbeatAccessor::~HeartbeatAccessor() 55 | { 56 | if (!m_shared_mem.unlock()) 57 | { 58 | qFatal("Failed to unlock shared memory %s", qUtf8Printable(m_shared_mem.key())); 59 | } 60 | } 61 | 62 | void HeartbeatAccessor::setTime(const QDateTime& time) 63 | { 64 | const qint64 time_ms{time.toMSecsSinceEpoch()}; 65 | // NOLINTNEXTLINE(*-reinterpret-cast) 66 | qint64* mem_ptr{reinterpret_cast(m_shared_mem.data())}; 67 | // NOLINTNEXTLINE(*-pointer-arithmetic) 68 | mem_ptr[TIME_INDEX] = time_ms; 69 | } 70 | 71 | QDateTime HeartbeatAccessor::getTime() const 72 | { 73 | // NOLINTNEXTLINE(*-reinterpret-cast) 74 | const qint64* mem_ptr{reinterpret_cast(m_shared_mem.data())}; 75 | // NOLINTNEXTLINE(*-pointer-arithmetic) 76 | return QDateTime::fromMSecsSinceEpoch(mem_ptr[TIME_INDEX], QTimeZone::UTC); 77 | } 78 | 79 | void HeartbeatAccessor::setShouldTerminate(bool terminate) 80 | { 81 | // NOLINTNEXTLINE(*-reinterpret-cast) 82 | qint64* mem_ptr{reinterpret_cast(m_shared_mem.data())}; 83 | // NOLINTNEXTLINE(*-pointer-arithmetic) 84 | mem_ptr[TERMINATE_INDEX] = static_cast(terminate); 85 | } 86 | 87 | bool HeartbeatAccessor::getShouldTerminate() const 88 | { 89 | // NOLINTNEXTLINE(*-reinterpret-cast) 90 | const qint64* mem_ptr{reinterpret_cast(m_shared_mem.data())}; 91 | // NOLINTNEXTLINE(*-pointer-arithmetic) 92 | return static_cast(mem_ptr[TERMINATE_INDEX]); 93 | } 94 | } // namespace 95 | 96 | namespace utils 97 | { 98 | Heartbeat::Heartbeat(const QString& key) 99 | : m_shared_mem(generateKeyHash(key, "_heartbeat_key")) 100 | { 101 | m_timer.setInterval(HEARTBEAT_INTERVAL); 102 | m_timer.setSingleShot(true); 103 | 104 | // On UNIX the shared memory segment will survive a crash, so we have to make sure to clean it up in such cases. 105 | const auto try_create_shared_mem = [&]() 106 | { 107 | constexpr qsizetype size{sizeof(qint64) * 2}; 108 | auto result{m_shared_mem.create(size)}; 109 | #if defined(Q_OS_LINUX) 110 | if (!result) 111 | { 112 | m_shared_mem.attach(); 113 | m_shared_mem.detach(); 114 | result = m_shared_mem.create(size); 115 | } 116 | #endif 117 | return result; 118 | }; 119 | 120 | if (try_create_shared_mem()) 121 | { 122 | HeartbeatAccessor memory{m_shared_mem}; 123 | memory.setShouldTerminate(false); 124 | memory.setTime(QDateTime::currentDateTimeUtc().addDays(-1)); 125 | return; 126 | } 127 | 128 | if (m_shared_mem.error() != QSharedMemory::AlreadyExists) 129 | { 130 | qFatal("Failed to create shared memory for %s (%s). Reason: %s", qUtf8Printable(key), 131 | qUtf8Printable(m_shared_mem.key()), qUtf8Printable(m_shared_mem.errorString())); 132 | } 133 | 134 | if (!m_shared_mem.attach()) 135 | { 136 | qFatal("Failed to attach to shared memory %s (%s). Reason: %s", qUtf8Printable(key), 137 | qUtf8Printable(m_shared_mem.key()), qUtf8Printable(m_shared_mem.errorString())); 138 | } 139 | } 140 | 141 | Heartbeat::~Heartbeat() 142 | { 143 | if (m_is_beating) 144 | { 145 | HeartbeatAccessor memory{m_shared_mem}; 146 | // Set the final time so that the other process can quickly determine that the heartbeat is gone 147 | memory.setTime(QDateTime::currentDateTimeUtc().addMSecs(-HEARTBEAT_TIMEOUT).addMSecs(HEARTBEAT_INTERVAL)); 148 | } 149 | } 150 | 151 | void Heartbeat::startBeating() 152 | { 153 | if (m_is_listening) 154 | { 155 | qFatal("You cannot start heartbeating if you're a listener!"); 156 | } 157 | 158 | if (!m_is_beating) 159 | { 160 | m_is_beating = true; 161 | connect(&m_timer, &QTimer::timeout, this, [this]() { slotBeating(false); }); 162 | slotBeating(true); 163 | } 164 | } 165 | 166 | void Heartbeat::startListening() 167 | { 168 | if (m_is_beating) 169 | { 170 | qFatal("You cannot start listening if you're the heartbeat!"); 171 | } 172 | 173 | if (!m_is_listening) 174 | { 175 | m_is_listening = true; 176 | connect(&m_timer, &QTimer::timeout, this, &Heartbeat::slotListening); 177 | slotListening(); 178 | } 179 | } 180 | 181 | void Heartbeat::terminate() 182 | { 183 | HeartbeatAccessor memory{m_shared_mem}; 184 | memory.setShouldTerminate(true); 185 | } 186 | 187 | bool Heartbeat::isAlive() const 188 | { 189 | return m_is_beating || m_is_alive; 190 | } 191 | 192 | void Heartbeat::slotBeating(bool fresh_start) 193 | { 194 | m_timer.stop(); 195 | 196 | HeartbeatAccessor memory{m_shared_mem}; 197 | if (!fresh_start && memory.getShouldTerminate()) 198 | { 199 | emit signalShouldTerminate(); 200 | return; 201 | } 202 | 203 | memory.setShouldTerminate(false); 204 | memory.setTime(QDateTime::currentDateTimeUtc()); 205 | 206 | m_timer.start(); 207 | } 208 | 209 | void Heartbeat::slotListening() 210 | { 211 | m_timer.stop(); 212 | 213 | const HeartbeatAccessor memory{m_shared_mem}; 214 | const QDateTime last_beat{memory.getTime()}; 215 | const bool is_alive{last_beat.msecsTo(QDateTime::currentDateTimeUtc()) <= HEARTBEAT_TIMEOUT}; 216 | 217 | if (is_alive != m_is_alive) 218 | { 219 | m_is_alive = is_alive; 220 | emit signalStateChanged(); 221 | } 222 | 223 | m_timer.start(); 224 | } 225 | } // namespace utils 226 | -------------------------------------------------------------------------------- /src/lib/utils/include/utils/appsettings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | #include 6 | #include 7 | 8 | // local includes 9 | #include "shared/appmetadata.h" 10 | 11 | namespace utils 12 | { 13 | class AppSettings 14 | { 15 | Q_DISABLE_COPY(AppSettings) 16 | 17 | public: 18 | explicit AppSettings(const shared::AppMetadata& app_metadata); 19 | virtual ~AppSettings() = default; 20 | 21 | const shared::AppMetadata& getAppMetadata() const; 22 | quint16 getPort() const; 23 | const QString& getLoggingRules() const; 24 | const QString& getSunshineAppsFilepath() const; 25 | bool getPreferHibernation() const; 26 | QSsl::SslProtocol getSslProtocol() const; 27 | bool getCloseSteamBeforeSleep() const; 28 | QString getSteamExecutablePath() const; 29 | const QString& getMacAddressOverride() const; 30 | 31 | private: 32 | bool parseSettingsFile(const QString& filepath); 33 | void saveDefaultFile(const QString& filepath) const; 34 | 35 | const shared::AppMetadata& m_app_metadata; 36 | quint16 m_port; 37 | QString m_logging_rules; 38 | QString m_sunshine_apps_filepath; 39 | bool m_prefer_hibernation; 40 | QSsl::SslProtocol m_ssl_protocol; 41 | bool m_close_steam_before_sleep; 42 | QString m_steam_exec_override; 43 | QString m_mac_address_override; 44 | }; 45 | } // namespace utils 46 | -------------------------------------------------------------------------------- /src/lib/utils/include/utils/heartbeat.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | #include 6 | 7 | namespace utils 8 | { 9 | class Heartbeat final : public QObject 10 | { 11 | Q_OBJECT 12 | Q_DISABLE_COPY(Heartbeat) 13 | 14 | public: 15 | explicit Heartbeat(const QString& key); 16 | ~Heartbeat() override; 17 | 18 | void startBeating(); 19 | void startListening(); 20 | 21 | void terminate(); 22 | bool isAlive() const; 23 | 24 | signals: 25 | void signalShouldTerminate(); 26 | void signalStateChanged(); 27 | 28 | private slots: 29 | void slotBeating(bool fresh_start); 30 | void slotListening(); 31 | 32 | private: 33 | QSharedMemory m_shared_mem; 34 | QTimer m_timer; 35 | bool m_is_beating{false}; 36 | bool m_is_listening{false}; 37 | bool m_is_alive{false}; 38 | }; 39 | } // namespace utils 40 | -------------------------------------------------------------------------------- /src/lib/utils/include/utils/jsonvalueconverter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | #include 6 | #include 7 | 8 | namespace utils 9 | { 10 | 11 | template 12 | struct JsonValueConverter; 13 | 14 | template 15 | requires std::is_enum_v 16 | struct JsonValueConverter 17 | { 18 | static std::optional convert(const QJsonValue& json) 19 | { 20 | if (!json.isString()) 21 | { 22 | return std::nullopt; 23 | } 24 | 25 | bool success{false}; 26 | auto&& metaEnum = QMetaEnum::fromType(); 27 | const int enum_value{metaEnum.keyToValue(json.toString().toUtf8(), &success)}; 28 | if (!success) 29 | { 30 | return std::nullopt; 31 | } 32 | 33 | return static_cast(enum_value); 34 | } 35 | }; 36 | 37 | template<> 38 | struct JsonValueConverter 39 | { 40 | static std::optional convert(const QJsonValue& json) 41 | { 42 | return json.isString() ? std::make_optional(json.toString()) : std::nullopt; 43 | } 44 | }; 45 | 46 | template<> 47 | struct JsonValueConverter 48 | { 49 | static std::optional convert(const QJsonValue& json) 50 | { 51 | return json.isBool() ? std::make_optional(json.toBool()) : std::nullopt; 52 | } 53 | }; 54 | 55 | template<> 56 | struct JsonValueConverter 57 | { 58 | static std::optional convert(const QJsonValue& json, int min = std::numeric_limits::lowest(), 59 | int max = std::numeric_limits::max()) 60 | { 61 | if (json.isDouble()) 62 | { 63 | const int number{json.toInt()}; 64 | if (number >= min && number <= max) 65 | { 66 | return number; 67 | } 68 | } 69 | 70 | return std::nullopt; 71 | } 72 | }; 73 | 74 | template<> 75 | struct JsonValueConverter 76 | { 77 | static std::optional convert(const QJsonValue& json, uint min = 0, 78 | uint max = static_cast(std::numeric_limits::max())) 79 | { 80 | if (min <= static_cast(std::numeric_limits::max()) 81 | && max <= static_cast(std::numeric_limits::max())) 82 | { 83 | const auto int_number{JsonValueConverter::convert(json, static_cast(min), static_cast(max))}; 84 | if (int_number) 85 | { 86 | return static_cast(*int_number); 87 | } 88 | } 89 | 90 | return std::nullopt; 91 | } 92 | }; 93 | 94 | template 95 | std::optional getJsonValue(const QJsonObject& json, const char* field, auto&&... args) 96 | { 97 | const auto field_value{json.value(field)}; 98 | return JsonValueConverter::convert(field_value, std::forward(args)...); 99 | } 100 | 101 | template 102 | std::optional> getNullableJsonValue(const QJsonObject& json, const char* field, auto&&... args) 103 | { 104 | const auto field_value{json.value(field)}; 105 | return field_value.isNull() ? std::make_optional(std::optional()) 106 | : JsonValueConverter::convert(field_value, std::forward(args)...); 107 | } 108 | } // namespace utils 109 | -------------------------------------------------------------------------------- /src/lib/utils/include/utils/logsettings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | 6 | namespace utils 7 | { 8 | class LogSettings final 9 | { 10 | Q_DISABLE_COPY(LogSettings) 11 | 12 | public: 13 | static LogSettings& getInstance(); 14 | 15 | void init(const QString& filepath); 16 | const QString& getFilepath() const; 17 | 18 | void setLoggingRules(const QString& rules); 19 | 20 | private: 21 | explicit LogSettings() = default; 22 | 23 | QString m_filepath; 24 | }; 25 | } // namespace utils 26 | -------------------------------------------------------------------------------- /src/lib/utils/include/utils/pairinginput.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | #include 6 | #include 7 | 8 | namespace utils 9 | { 10 | class PairingInput : public QObject 11 | { 12 | Q_OBJECT 13 | Q_DISABLE_COPY(PairingInput) 14 | 15 | public: 16 | explicit PairingInput(); 17 | ~PairingInput() override = default; 18 | 19 | signals: 20 | void signalFinishPairing(uint pin); 21 | void signalPairingRejected(); 22 | 23 | public slots: 24 | void slotRequestUserInputForPairing(); 25 | void slotAbortPairing(); 26 | 27 | private: 28 | QInputDialog m_dialog; 29 | QTimer m_timeout; 30 | }; 31 | } // namespace utils 32 | -------------------------------------------------------------------------------- /src/lib/utils/include/utils/singleinstanceguard.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system/Qt includes 4 | #include 5 | #include 6 | 7 | namespace utils 8 | { 9 | // From https://stackoverflow.com/questions/5006547/qt-best-practice-for-a-single-instance-app-protection 10 | class SingleInstanceGuard final 11 | { 12 | Q_DISABLE_COPY(SingleInstanceGuard) 13 | 14 | public: 15 | explicit SingleInstanceGuard(const QString& key); 16 | ~SingleInstanceGuard(); 17 | 18 | bool isAnotherRunning(); 19 | bool tryToRun(); 20 | void release(); 21 | 22 | private: 23 | const QString m_mem_lock_key; 24 | const QString m_shared_mem_key; 25 | 26 | QSystemSemaphore m_mem_lock; 27 | QSharedMemory m_shared_mem; 28 | }; 29 | } // namespace utils 30 | -------------------------------------------------------------------------------- /src/lib/utils/include/utils/unixsignalhandler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace utils 4 | { 5 | void installSignalHandler(); 6 | } // namespace utils 7 | -------------------------------------------------------------------------------- /src/lib/utils/logsettings.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "utils/logsettings.h" 3 | 4 | // system/Qt includes 5 | #include 6 | #include 7 | 8 | // local includes 9 | #include "shared/loggingcategories.h" 10 | 11 | namespace 12 | { 13 | void swapFilesIfNeeded(const QString& filepath) 14 | { 15 | constexpr int max_size{2 * 1024 * 1024}; 16 | if (const QFileInfo file_info(filepath); file_info.size() > max_size) 17 | { 18 | const auto filename{file_info.fileName()}; 19 | const auto old_filename{filename + ".old"}; 20 | 21 | auto file_dir{file_info.absoluteDir()}; 22 | if (file_dir.exists(old_filename) && !file_dir.remove(old_filename)) 23 | { 24 | qFatal("File could not be removed: \"%s\".", qUtf8Printable(old_filename)); 25 | } 26 | 27 | if (!file_dir.rename(filename, old_filename)) 28 | { 29 | qFatal("File could not be renamed: \"%s\" -> \"%s\".", qUtf8Printable(filename), 30 | qUtf8Printable(old_filename)); 31 | } 32 | } 33 | } 34 | 35 | void appendEmptyLine(const QString& filepath) 36 | { 37 | if (const QFileInfo file_info(filepath); file_info.size() > 0) 38 | { 39 | QFile file(filepath); 40 | if (!file.open(QIODevice::WriteOnly | QIODevice::Append)) 41 | { 42 | qFatal("File could not be opened for writing: \"%s\".", qUtf8Printable(filepath)); 43 | } 44 | 45 | file.write("\n\n"); 46 | } 47 | } 48 | 49 | void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg) 50 | { 51 | const QString formatted_msg{qFormatLogMessage(type, context, msg)}; 52 | { 53 | QTextStream stream(stdout); 54 | stream << formatted_msg << Qt::endl; 55 | } 56 | 57 | const auto& filepath{utils::LogSettings::getInstance().getFilepath()}; 58 | swapFilesIfNeeded(filepath); 59 | 60 | QFile file(filepath); 61 | if (!file.open(QIODevice::WriteOnly | QIODevice::Append)) 62 | { 63 | qFatal("File could not be opened for writing: \"%s\".", qUtf8Printable(filepath)); 64 | } 65 | 66 | QTextStream stream(&file); 67 | stream << formatted_msg << Qt::endl; 68 | } 69 | } // namespace 70 | 71 | namespace utils 72 | { 73 | LogSettings& LogSettings::getInstance() 74 | { 75 | static LogSettings instance; 76 | return instance; 77 | } 78 | 79 | void LogSettings::init(const QString& filepath) 80 | { 81 | qSetMessagePattern("[%{time hh:mm:ss.zzz}] " 82 | "%{if-debug}DEBUG %{endif}" 83 | "%{if-info}INFO %{endif}" 84 | "%{if-warning}WARNING %{endif}" 85 | "%{if-critical}CRITICAL %{endif}" 86 | "%{if-fatal}FATAL %{endif}" 87 | "%{if-category}%{category}: %{endif}%{message}"); 88 | 89 | if (filepath.isEmpty()) 90 | { 91 | return; 92 | } 93 | 94 | m_filepath = filepath; 95 | swapFilesIfNeeded(filepath); 96 | appendEmptyLine(filepath); 97 | qInstallMessageHandler(messageHandler); 98 | 99 | qCInfo(lc::utils) << "Log location:" << m_filepath; 100 | } 101 | 102 | const QString& LogSettings::getFilepath() const 103 | { 104 | return m_filepath; 105 | } 106 | 107 | // NOLINTNEXTLINE(*-to-static) 108 | void LogSettings::setLoggingRules(const QString& rules) 109 | { 110 | if (!rules.isEmpty()) 111 | { 112 | QLoggingCategory::setFilterRules(rules); 113 | } 114 | } 115 | } // namespace utils 116 | -------------------------------------------------------------------------------- /src/lib/utils/pairinginput.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "utils/pairinginput.h" 3 | 4 | namespace 5 | { 6 | const int DEFAULT_TIMEOUT{1 * 60 * 1000 /*1 minute*/}; 7 | const int MIN_PIN_VALUE{1000}; 8 | const int MAX_PIN_VALUE{9999}; 9 | } // namespace 10 | 11 | namespace utils 12 | { 13 | PairingInput::PairingInput() 14 | { 15 | connect(&m_dialog, &QInputDialog::rejected, this, 16 | [this]() 17 | { 18 | m_timeout.stop(); 19 | emit signalPairingRejected(); 20 | }); 21 | connect(&m_dialog, &QInputDialog::accepted, this, 22 | [this]() 23 | { 24 | m_timeout.stop(); 25 | emit signalFinishPairing(m_dialog.intValue()); 26 | }); 27 | m_dialog.setInputMode(QInputDialog::IntInput); 28 | m_dialog.setIntMinimum(MIN_PIN_VALUE); 29 | m_dialog.setIntMaximum(MAX_PIN_VALUE); 30 | m_dialog.setWindowTitle("Pairing"); 31 | m_dialog.setLabelText("Please enter the pairing number as shown on SteamDeck."); 32 | 33 | connect(&m_timeout, &QTimer::timeout, &m_dialog, &QInputDialog::reject); 34 | m_timeout.setSingleShot(true); 35 | } 36 | 37 | void PairingInput::slotRequestUserInputForPairing() 38 | { 39 | m_dialog.close(); 40 | m_timeout.start(DEFAULT_TIMEOUT); 41 | m_dialog.open(); 42 | } 43 | 44 | void PairingInput::slotAbortPairing() 45 | { 46 | m_timeout.stop(); 47 | m_dialog.close(); 48 | } 49 | } // namespace utils 50 | -------------------------------------------------------------------------------- /src/lib/utils/singleinstanceguard.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "utils/singleinstanceguard.h" 3 | 4 | #if defined(QT_OS_WINDOWS) 5 | // A SEPARATE WINDOWS INCLUDE BECAUSE OF THE SMELL! 6 | #include 7 | #endif 8 | 9 | // system/Qt includes 10 | #include 11 | 12 | namespace 13 | { 14 | QString generateKeyHash(const QString& key, const QString& salt) 15 | { 16 | QByteArray data; 17 | 18 | data.append(key.toUtf8()); 19 | data.append(salt.toUtf8()); 20 | data = QCryptographicHash::hash(data, QCryptographicHash::Sha1).toHex(); 21 | 22 | return data; 23 | } 24 | } // namespace 25 | 26 | namespace utils 27 | { 28 | SingleInstanceGuard::SingleInstanceGuard(const QString& key) 29 | : m_mem_lock_key(generateKeyHash(key, "_mem_lock_key")) 30 | , m_shared_mem_key(generateKeyHash(key, "_shared_mem_key")) 31 | , m_mem_lock(m_mem_lock_key, QSystemSemaphore::Create) 32 | , m_shared_mem(m_shared_mem_key) 33 | { 34 | auto cleanup = qScopeGuard([this]() { m_mem_lock.release(); }); 35 | m_mem_lock.acquire(); 36 | { 37 | QSharedMemory fix(m_shared_mem_key); 38 | fix.attach(); 39 | } 40 | 41 | #if defined(QT_OS_WINDOWS) 42 | // OS will clean up this mutex (used by installer) 43 | CreateMutexA(nullptr, FALSE, key.toLatin1()); 44 | #endif 45 | } 46 | 47 | SingleInstanceGuard::~SingleInstanceGuard() 48 | { 49 | release(); 50 | } 51 | 52 | bool SingleInstanceGuard::isAnotherRunning() 53 | { 54 | if (m_shared_mem.isAttached()) 55 | { 56 | return false; 57 | } 58 | 59 | auto cleanup = qScopeGuard([this]() { m_mem_lock.release(); }); 60 | m_mem_lock.acquire(); 61 | 62 | const bool is_running = m_shared_mem.attach(); 63 | if (is_running) 64 | { 65 | m_shared_mem.detach(); 66 | } 67 | 68 | return is_running; 69 | } 70 | 71 | bool SingleInstanceGuard::tryToRun() 72 | { 73 | if (isAnotherRunning()) 74 | { 75 | return false; 76 | } 77 | 78 | const bool result = [this]() 79 | { 80 | auto cleanup = qScopeGuard([this]() { m_mem_lock.release(); }); 81 | m_mem_lock.acquire(); 82 | return m_shared_mem.create(sizeof(quint64)); 83 | }(); 84 | 85 | if (!result) 86 | { 87 | release(); 88 | return false; 89 | } 90 | 91 | return true; 92 | } 93 | 94 | void SingleInstanceGuard::release() 95 | { 96 | auto cleanup = qScopeGuard([this]() { m_mem_lock.release(); }); 97 | m_mem_lock.acquire(); 98 | 99 | if (m_shared_mem.isAttached()) 100 | { 101 | m_shared_mem.detach(); 102 | } 103 | } 104 | } // namespace utils 105 | -------------------------------------------------------------------------------- /src/lib/utils/unixsignalhandler.cpp: -------------------------------------------------------------------------------- 1 | // header file include 2 | #include "utils/unixsignalhandler.h" 3 | 4 | // system/Qt includes 5 | #include 6 | #include 7 | 8 | namespace 9 | { 10 | void handler(int code) 11 | { 12 | std::signal(code, SIG_DFL); 13 | QCoreApplication::quit(); 14 | } 15 | } // namespace 16 | 17 | namespace utils 18 | { 19 | void installSignalHandler() 20 | { 21 | Q_ASSERT(QCoreApplication::instance() != nullptr); 22 | 23 | std::signal(SIGINT, handler); 24 | std::signal(SIGTERM, handler); 25 | } 26 | } // namespace utils 27 | -------------------------------------------------------------------------------- /src/stream/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------------------------------------------------------------------- 2 | # External dependencies 3 | #---------------------------------------------------------------------------------------------------------------------- 4 | 5 | find_package(Qt6 COMPONENTS Core REQUIRED) 6 | qt_standard_project_setup() 7 | 8 | #---------------------------------------------------------------------------------------------------------------------- 9 | # Target config 10 | #---------------------------------------------------------------------------------------------------------------------- 11 | 12 | set(EXEC_NAME ${EXEC_NAME_STREAM}) 13 | set(RESOURCES "${BUDDY_RESOURCES}/resources.qrc") 14 | 15 | if(WIN32) 16 | configure_file( 17 | "${BUDDY_RESOURCES}/windows/version.rc.in" 18 | "${CMAKE_CURRENT_BINARY_DIR}/version.rc" 19 | @ONLY) 20 | 21 | list(APPEND RESOURCES "${BUDDY_RESOURCES}/windows/resources.rc") 22 | list(APPEND RESOURCES "${CMAKE_CURRENT_BINARY_DIR}/version.rc") 23 | endif() 24 | 25 | add_executable(${EXEC_NAME} main.cpp ${RESOURCES}) 26 | target_link_libraries(${EXEC_NAME} PRIVATE Qt6::Core utilslib sharedlib oslib) 27 | target_compile_definitions(${EXEC_NAME} PRIVATE EXEC_VERSION="${PROJECT_VERSION}") 28 | 29 | if(NOT DEBUG_MODE) 30 | if(WIN32) 31 | set_target_properties(${EXEC_NAME} PROPERTIES WIN32_EXECUTABLE TRUE) 32 | endif() 33 | endif() 34 | -------------------------------------------------------------------------------- /src/stream/main.cpp: -------------------------------------------------------------------------------- 1 | // system/Qt includes 2 | #include 3 | 4 | // local includes 5 | #include "os/sleepinhibitor.h" 6 | #include "shared/appmetadata.h" 7 | #include "shared/loggingcategories.h" 8 | #include "utils/heartbeat.h" 9 | #include "utils/logsettings.h" 10 | #include "utils/singleinstanceguard.h" 11 | #include "utils/unixsignalhandler.h" 12 | 13 | // NOLINTNEXTLINE(*-avoid-c-arrays) 14 | int main(int argc, char* argv[]) 15 | { 16 | const shared::AppMetadata app_meta{shared::AppMetadata::App::Stream}; 17 | utils::SingleInstanceGuard guard{app_meta.getAppName()}; 18 | 19 | QCoreApplication app{argc, argv}; 20 | QCoreApplication::setApplicationName(app_meta.getAppName()); 21 | QCoreApplication::setApplicationVersion(EXEC_VERSION); 22 | 23 | if (!guard.tryToRun()) 24 | { 25 | qCWarning(lc::streamMain) << "another instance of" << app_meta.getAppName() << "is already running!"; 26 | return EXIT_FAILURE; 27 | } 28 | 29 | utils::installSignalHandler(); 30 | utils::LogSettings::getInstance().init(app_meta.getLogPath()); 31 | qCInfo(lc::streamMain) << "startup. Version:" << EXEC_VERSION; 32 | 33 | const os::SleepInhibitor sleep_inhibitor{app_meta.getAppName()}; 34 | utils::Heartbeat heartbeat{app_meta.getAppName()}; 35 | QObject::connect(&heartbeat, &utils::Heartbeat::signalShouldTerminate, &app, &QCoreApplication::quit); 36 | heartbeat.startBeating(); 37 | 38 | QObject::connect(&app, &QCoreApplication::aboutToQuit, []() { qCInfo(lc::streamMain) << "shutdown."; }); 39 | qCInfo(lc::streamMain) << "startup finished."; 40 | 41 | return QCoreApplication::exec(); 42 | } 43 | --------------------------------------------------------------------------------