diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0811967b..8776f630 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,6 +50,7 @@ jobs: RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }} RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.MAIN_VITE_EXTERNAL_RESOURCES_URL }} - name: Build Windows if: matrix.os == 'windows-latest' @@ -62,6 +63,7 @@ jobs: RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }} RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.MAIN_VITE_EXTERNAL_RESOURCES_URL }} - name: Create artifact uses: actions/upload-artifact@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b91cc743..2f1d3188 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,6 +49,7 @@ jobs: RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }} RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.MAIN_VITE_EXTERNAL_RESOURCES_URL }} - name: Build Windows if: matrix.os == 'windows-latest' run: yarn build:win @@ -60,6 +61,7 @@ jobs: RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }} RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.MAIN_VITE_EXTERNAL_RESOURCES_URL }} - name: Create artifact uses: actions/upload-artifact@v4 with: diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index fd9bb148..b95b93cf 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -1,53 +1,172 @@ -import { IsNull, Not } from "typeorm"; import { gameRepository } from "@main/repository"; import { WindowManager } from "./window-manager"; import { createGame, updateGamePlaytime } from "./library-sync"; import type { GameRunning } from "@types"; import { PythonInstance } from "./download"; import { Game } from "@main/entity"; +import axios from "axios"; +import { exec } from "child_process"; + +const commands = { + findWineDir: `lsof -c wine 2>/dev/null | grep '/drive_c/windows$' | head -n 1 | awk '{for(i=9;i<=NF;i++) printf "%s ", $i; print ""}'`, + findWineExecutables: `lsof -c wine 2>/dev/null | grep '\\.exe$' | awk '{for(i=9;i<=NF;i++) printf "%s ", $i; print ""}'`, +}; export const gamesPlaytime = new Map< number, { lastTick: number; firstTick: number; lastSyncTick: number } >(); +interface ExecutableInfo { + name: string; + os: string; +} + +interface GameExecutables { + [key: string]: ExecutableInfo[]; +} + const TICKS_TO_UPDATE_API = 120; let currentTick = 1; -const getSystemProcessSet = async () => { - const processes = await PythonInstance.getProcessList(); +const gameExecutables = ( + await axios + .get( + import.meta.env.MAIN_VITE_EXTERNAL_RESOURCES_URL + + "/game-executables.json" + ) + .catch(() => { + return { data: {} }; + }) +).data as GameExecutables; - if (process.platform === "linux") - return new Set(processes.map((process) => process.name)); - return new Set(processes.map((process) => process.exe)); +const findGamePathByProcess = ( + processMap: Map>, + gameId: string +) => { + const executables = gameExecutables[gameId].filter((info) => { + if (process.platform === "linux" && info.os === "linux") return true; + return info.os === "win32"; + }); + + for (const executable of executables) { + const exe = executable.name.slice(executable.name.lastIndexOf("/") + 1); + + if (!exe) continue; + + const pathSet = processMap.get(exe); + + if (pathSet) { + const executableName = + process.platform === "win32" + ? executable.name.replace(/\//g, "\\") + : executable.name; + + pathSet.forEach((path) => { + if (path.toLowerCase().endsWith(executableName)) { + gameRepository.update( + { objectID: gameId, shop: "steam" }, + { executablePath: path } + ); + + if (process.platform === "linux") { + exec(commands.findWineDir, (err, out) => { + if (err) return; + + gameRepository.update( + { objectID: gameId, shop: "steam" }, + { + winePrefixPath: out.trim().replace("/drive_c/windows", ""), + } + ); + }); + } + } + }); + } + } }; -const getExecutable = (game: Game) => { - if (process.platform === "linux") - return game.executablePath?.split("/").at(-1); - return game.executablePath; +const getSystemProcessMap = async () => { + const processes = await PythonInstance.getProcessList(); + + const map = new Map>(); + + processes.forEach((process) => { + const key = process.name.toLowerCase(); + const value = process.exe; + + if (!key || !value) return; + + const currentSet = map.get(key) ?? new Set(); + map.set(key, currentSet.add(value)); + }); + + if (process.platform === "linux") { + await new Promise((res) => { + exec(commands.findWineExecutables, (err, out) => { + if (err) { + res(null); + return; + } + + const pathSet = new Set( + out + .trim() + .split("\n") + .map((path) => path.trim()) + ); + + pathSet.forEach((path) => { + if (path.startsWith("/usr")) return; + + const key = path.slice(path.lastIndexOf("/") + 1).toLowerCase(); + + if (!key || !path) return; + + const currentSet = map.get(key) ?? new Set(); + map.set(key, currentSet.add(path)); + }); + + res(null); + }); + }); + } + + return map; }; export const watchProcesses = async () => { const games = await gameRepository.find({ where: { - executablePath: Not(IsNull()), isDeleted: false, }, }); - if (games.length === 0) return; + if (!games.length) return; - const processSet = await getSystemProcessSet(); + const processMap = await getSystemProcessMap(); for (const game of games) { - const executable = getExecutable(game); + const executablePath = game.executablePath; - if (!executable) continue; + if (!executablePath) { + if (gameExecutables[game.objectID]) { + findGamePathByProcess(processMap, game.objectID); + } + continue; + } - const gameProcess = processSet.has(executable); + const executable = executablePath + .slice( + executablePath.lastIndexOf(process.platform === "win32" ? "\\" : "/") + + 1 + ) + .toLowerCase(); - if (gameProcess) { + const hasProcess = processMap.get(executable)?.has(executablePath); + + if (hasProcess) { if (gamesPlaytime.has(game.id)) { onTickGame(game); } else { diff --git a/src/main/vite-env.d.ts b/src/main/vite-env.d.ts index 86aa9d33..af40cf5f 100644 --- a/src/main/vite-env.d.ts +++ b/src/main/vite-env.d.ts @@ -6,6 +6,7 @@ interface ImportMetaEnv { readonly MAIN_VITE_ANALYTICS_API_URL: string; readonly MAIN_VITE_AUTH_URL: string; readonly MAIN_VITE_CHECKOUT_URL: string; + readonly MAIN_VITE_EXTERNAL_RESOURCES_URL: string; } interface ImportMeta { diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 1ec7507e..422e5192 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -126,7 +126,7 @@ export function App() { const $script = document.createElement("script"); $script.id = "external-resources"; - $script.src = `${import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL}?t=${Date.now()}`; + $script.src = `${import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL}/bundle.js?t=${Date.now()}`; document.head.appendChild($script); }); }, [fetchUserDetails, syncFriendRequests, updateUserDetails, dispatch]);