Fixed issues with gamescope not properly launching.
This commit is contained in:
@@ -7,7 +7,12 @@ from datetime import datetime
|
||||
import fcntl
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Iterator, TextIO
|
||||
from typing import Callable, Iterator, Sequence, TextIO
|
||||
|
||||
|
||||
ActiveGameProbe = Callable[[], bool]
|
||||
MAX_PROCESS_ENVIRONMENT_BYTES = 1024 * 1024
|
||||
MAX_PROCESS_COMMAND_BYTES = 128 * 1024
|
||||
|
||||
|
||||
class OperationBusyError(RuntimeError):
|
||||
@@ -51,9 +56,22 @@ class LauncherPlayLease:
|
||||
def record_owned(self) -> bool:
|
||||
return self._record_owned
|
||||
|
||||
@property
|
||||
def inherited_fds(self) -> tuple[int, ...]:
|
||||
"""Keep the shared lease alive in the launched game supervisor."""
|
||||
|
||||
if self._handle is None:
|
||||
return ()
|
||||
return (self._handle.fileno(),)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def launcher_operation_lock(lock_path: Path, operation: str) -> Iterator[None]:
|
||||
def launcher_operation_lock(
|
||||
lock_path: Path,
|
||||
operation: str,
|
||||
*,
|
||||
active_game_probe: ActiveGameProbe | None = None,
|
||||
) -> Iterator[None]:
|
||||
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with lock_path.open("a+", encoding="utf-8") as handle:
|
||||
try:
|
||||
@@ -64,6 +82,11 @@ def launcher_operation_lock(lock_path: Path, operation: str) -> Iterator[None]:
|
||||
raise OperationBusyError(
|
||||
f"launcher files are busy ({owner}); wait for it to finish and try again"
|
||||
) from exc
|
||||
if active_game_probe is not None and active_game_probe():
|
||||
raise OperationBusyError(
|
||||
"launcher files are busy (active managed game client detected); "
|
||||
"close every game client and try again"
|
||||
)
|
||||
_write_owner(handle, operation)
|
||||
try:
|
||||
yield
|
||||
@@ -78,6 +101,8 @@ def launcher_operation_lock(lock_path: Path, operation: str) -> Iterator[None]:
|
||||
def launcher_play_lock(
|
||||
lock_path: Path,
|
||||
operation: str = "play",
|
||||
*,
|
||||
active_game_probe: ActiveGameProbe | None = None,
|
||||
) -> Iterator[LauncherPlayLease]:
|
||||
"""Serialize preparation, then allow multiple active game clients.
|
||||
|
||||
@@ -105,12 +130,24 @@ def launcher_play_lock(
|
||||
record_owned=False,
|
||||
)
|
||||
else:
|
||||
_write_owner(handle, f"{operation} preparation")
|
||||
lease = LauncherPlayLease(
|
||||
handle,
|
||||
preparation_allowed=True,
|
||||
record_owned=True,
|
||||
)
|
||||
if active_game_probe is not None and active_game_probe():
|
||||
# A game from an older launcher may have outlived its Python
|
||||
# supervisor and therefore its inherited lock. Rejoin that live
|
||||
# prefix without mutating its runtime view or waiting for it to exit.
|
||||
_write_owner(handle, operation)
|
||||
fcntl.flock(handle.fileno(), fcntl.LOCK_SH)
|
||||
lease = LauncherPlayLease(
|
||||
handle,
|
||||
preparation_allowed=False,
|
||||
record_owned=True,
|
||||
)
|
||||
else:
|
||||
_write_owner(handle, f"{operation} preparation")
|
||||
lease = LauncherPlayLease(
|
||||
handle,
|
||||
preparation_allowed=True,
|
||||
record_owned=True,
|
||||
)
|
||||
try:
|
||||
yield lease
|
||||
finally:
|
||||
@@ -123,6 +160,79 @@ def launcher_play_lock(
|
||||
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
||||
|
||||
|
||||
def managed_game_process_is_active(
|
||||
runtime_view: Path,
|
||||
wine_prefixes: Sequence[Path],
|
||||
*,
|
||||
process_root: Path = Path("/proc"),
|
||||
) -> bool:
|
||||
"""Detect a managed Wine game that outlived its launcher supervisor."""
|
||||
|
||||
prefixes = {_normalized_path(prefix) for prefix in wine_prefixes}
|
||||
prefixes.discard("")
|
||||
if not prefixes:
|
||||
return False
|
||||
runtime_marker = _normalized_path(runtime_view).casefold().replace("\\", "/")
|
||||
if not runtime_marker:
|
||||
return False
|
||||
runtime_marker = runtime_marker.rstrip("/") + "/"
|
||||
try:
|
||||
processes = tuple(process_root.iterdir())
|
||||
except OSError:
|
||||
return False
|
||||
for process in processes:
|
||||
if not process.name.isdecimal():
|
||||
continue
|
||||
try:
|
||||
name = (process / "comm").read_text(
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
).strip().casefold()
|
||||
except OSError:
|
||||
continue
|
||||
if not name.endswith(".exe"):
|
||||
continue
|
||||
environment = _read_bounded(process / "environ", MAX_PROCESS_ENVIRONMENT_BYTES)
|
||||
if environment is None:
|
||||
continue
|
||||
wine_prefix = next(
|
||||
(
|
||||
entry.removeprefix(b"WINEPREFIX=")
|
||||
for entry in environment.split(b"\0")
|
||||
if entry.startswith(b"WINEPREFIX=")
|
||||
),
|
||||
None,
|
||||
)
|
||||
if (
|
||||
wine_prefix is None
|
||||
or _normalized_path(os.fsdecode(wine_prefix)) not in prefixes
|
||||
):
|
||||
continue
|
||||
command = _read_bounded(process / "cmdline", MAX_PROCESS_COMMAND_BYTES)
|
||||
if command is None:
|
||||
continue
|
||||
normalized_command = os.fsdecode(command).casefold().replace("\\", "/")
|
||||
if runtime_marker in normalized_command:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _read_bounded(path: Path, maximum_bytes: int) -> bytes | None:
|
||||
try:
|
||||
with path.open("rb") as handle:
|
||||
value = handle.read(maximum_bytes + 1)
|
||||
except OSError:
|
||||
return None
|
||||
return value if len(value) <= maximum_bytes else None
|
||||
|
||||
|
||||
def _normalized_path(path: Path | str) -> str:
|
||||
value = str(path)
|
||||
if not value:
|
||||
return ""
|
||||
return str(Path(value).expanduser().resolve(strict=False))
|
||||
|
||||
|
||||
def _read_owner(handle: TextIO) -> str:
|
||||
handle.seek(0)
|
||||
return handle.read().strip() or "unknown operation"
|
||||
|
||||
Reference in New Issue
Block a user