-
-
Notifications
You must be signed in to change notification settings - Fork 271
track game runtime by process and not just executable name #1225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,16 +24,20 @@ import app.gamenative.ui.enums.ConnectionState | |
| import app.gamenative.ui.screen.PluviaScreen | ||
| import app.gamenative.utils.ContainerUtils | ||
| import app.gamenative.utils.IntentLaunchManager | ||
| import app.gamenative.ui.screen.xserver.CORE_WINE_PROCESSES | ||
| import app.gamenative.utils.SteamUtils | ||
| import app.gamenative.utils.UpdateInfo | ||
| import com.materialkolor.PaletteStyle | ||
| import com.winlator.winhandler.OnGetProcessInfoListener | ||
| import com.winlator.winhandler.ProcessInfo | ||
| import com.winlator.xserver.Window | ||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||
| import `in`.dragonbra.javasteam.steam.handlers.steamapps.AppProcessInfo | ||
| import java.nio.file.Paths | ||
| import javax.inject.Inject | ||
| import kotlin.io.path.name | ||
| import kotlinx.coroutines.CancellationException | ||
| import kotlinx.coroutines.CompletableDeferred | ||
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.Job | ||
| import kotlinx.coroutines.async | ||
|
|
@@ -45,6 +49,8 @@ import kotlinx.coroutines.flow.asStateFlow | |
| import kotlinx.coroutines.flow.receiveAsFlow | ||
| import kotlinx.coroutines.flow.update | ||
| import kotlinx.coroutines.launch | ||
| import kotlinx.coroutines.withContext | ||
| import kotlinx.coroutines.withTimeoutOrNull | ||
| import timber.log.Timber | ||
|
|
||
| @HiltViewModel | ||
|
|
@@ -55,6 +61,55 @@ class MainViewModel @Inject constructor( | |
|
|
||
| companion object { | ||
| private const val KEY_CURRENT_SCREEN_ROUTE = "current_screen_route" | ||
| private const val PROCESS_LIST_TIMEOUT_MS = 2000L | ||
| } | ||
|
|
||
| private var hasSentPlayingNotification = false | ||
|
|
||
| private suspend fun isLaunchExeRunning(launchExeNames: List<String>): Boolean { | ||
| if (launchExeNames.isEmpty()) return false | ||
| val winHandler = PluviaApp.xServerView?.getxServer()?.winHandler ?: return false | ||
| val targets = launchExeNames.toSet() | ||
|
|
||
| return withContext(Dispatchers.IO) { | ||
| val previousListener = winHandler.getOnGetProcessInfoListener() | ||
| val snapshotDeferred = CompletableDeferred<List<ProcessInfo>>() | ||
| val lock = Any() | ||
| val currentList = mutableListOf<ProcessInfo>() | ||
| var expectedCount = -1 | ||
|
|
||
| val listener = OnGetProcessInfoListener { index, count, processInfo -> | ||
| previousListener?.onGetProcessInfo(index, count, processInfo) | ||
| synchronized(lock) { | ||
| if (count == 0 && processInfo == null) { | ||
| if (!snapshotDeferred.isCompleted) snapshotDeferred.complete(emptyList()) | ||
| return@synchronized | ||
| } | ||
| if (index == 0) { | ||
| currentList.clear() | ||
| expectedCount = count | ||
| } | ||
| if (processInfo != null) { | ||
| currentList.add(processInfo) | ||
| } | ||
| if (expectedCount >= 0 && currentList.size >= expectedCount && !snapshotDeferred.isCompleted) { | ||
| snapshotDeferred.complete(currentList.toList()) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| winHandler.setOnGetProcessInfoListener(listener) | ||
| try { | ||
| winHandler.listProcesses() | ||
| val snapshot = withTimeoutOrNull(PROCESS_LIST_TIMEOUT_MS) { | ||
| snapshotDeferred.await() | ||
| } ?: return@withContext false | ||
|
|
||
| snapshot.any { it.name.lowercase() in targets } | ||
| } finally { | ||
| winHandler.setOnGetProcessInfoListener(previousListener) | ||
| } | ||
| } | ||
|
Comment on lines
+69
to
+112
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normalize Line 108 compares raw process-list names against basename-only launch exes. In this codebase the same WinHandler process names are normalized elsewhere before comparison because they may include a path, quotes, or a ♻️ Suggested normalization+ private fun normalizeProcessName(name: String): String {
+ val base = name.trim().trim('"').substringAfterLast('/').substringAfterLast('\\')
+ return base.lowercase().removeSuffix(".exe")
+ }
+
private suspend fun isLaunchExeRunning(launchExeNames: List<String>): Boolean {
if (launchExeNames.isEmpty()) return false
val winHandler = PluviaApp.xServerView?.getxServer()?.winHandler ?: return false
- val targets = launchExeNames.toSet()
+ val targets = launchExeNames.map(::normalizeProcessName).toSet()
...
- snapshot.any { it.name.lowercase() in targets }
+ snapshot.any { normalizeProcessName(it.name) in targets }
} finally {🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| sealed class MainUiEvent { | ||
|
|
@@ -488,6 +543,7 @@ class MainViewModel @Inject constructor( | |
|
|
||
| val gameId = ContainerUtils.extractGameIdFromContainerId(appId) | ||
| Timber.tag("Exit").i("Got game id: $gameId") | ||
| hasSentPlayingNotification = false | ||
| SteamService.notifyRunningProcesses() | ||
| handleExitCloudSync(context, appId, gameId) | ||
|
|
||
|
|
@@ -614,7 +670,8 @@ class MainViewModel @Inject constructor( | |
| gameExe == windowExe | ||
| } | ||
|
|
||
| if (launchConfig != null) { | ||
| if (launchConfig != null && !hasSentPlayingNotification) { | ||
| hasSentPlayingNotification = true | ||
| val steamProcessId = Process.myPid() | ||
| val processes = mutableListOf<AppProcessInfo>() | ||
| var currentWindow: Window = window | ||
|
|
@@ -652,6 +709,34 @@ class MainViewModel @Inject constructor( | |
| Timber.tag("MainViewModel").i("Skipping Steam process notification - real Steam will handle this") | ||
| } | ||
| } | ||
| } else if (!hasSentPlayingNotification) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Race condition: Prompt for AI agents |
||
| val windowBase = window.className.substringAfterLast('\\') | ||
| .substringAfterLast('/').lowercase().removeSuffix(".exe") | ||
| if (windowBase in CORE_WINE_PROCESSES) return@let | ||
|
|
||
| val launchExeNames = SteamService.getWindowsLaunchInfos(gameId) | ||
| .map { Paths.get(it.executable.replace('\\', '/')).name.lowercase() } | ||
| if (isLaunchExeRunning(launchExeNames)) { | ||
| val shouldLaunchRealSteam = try { | ||
| val container = ContainerUtils.getContainer(context, appId) | ||
| container.isLaunchRealSteam() | ||
| } catch (e: Exception) { | ||
| false | ||
| } | ||
| if (!shouldLaunchRealSteam) { | ||
| hasSentPlayingNotification = true | ||
| val installedBranch = SteamService.getInstalledApp(gameId)?.branch ?: "public" | ||
| val processInfo = GameProcessInfo( | ||
| appId = gameId, | ||
| branch = installedBranch, | ||
| processes = listOf(AppProcessInfo(Process.myPid(), Process.myPid(), true)), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Fallback runtime notification reports the Android PID instead of the game/Wine process PID, causing incorrect process tracking. Prompt for AI agents |
||
| ) | ||
| Timber.tag("MainViewModel").i( | ||
| "Sending playing notification via process-list fallback for appId=%d", gameId, | ||
| ) | ||
| SteamService.notifyRunningProcesses(processInfo) | ||
|
Comment on lines
+729
to
+737
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use the matched guest PID here, not
🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
Comment on lines
+712
to
+739
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The fallback branch can still send duplicate playing notifications.
🧰 Tools🪛 detekt (1.23.8)[warning] 723-723: The caught exception is swallowed. The original exception could be lost. (detekt.exceptions.SwallowedException) 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's also the "parentProcess" which is worth considering if this is worth checking for too. I believe it comes from RunningAppInfo from javasteam
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, looked into parentProcess, it's null unfortunately. I don't know if this one is worth merging, but we should probably find a better way to track playtime rather than just checking the exe name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, happy to also look at this. I've created a thread here: https://discord.com/channels/1378308569287622737/1499071031795519548