Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 52 additions & 17 deletions rare/commands/launcher/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
import os
import platform
import shlex
import signal
import subprocess
import time
import traceback
from argparse import Namespace
from logging import getLogger
from signal import SIGINT, SIGTERM, signal, strsignal
from typing import Optional

import shiboken6
from legendary.models.game import SaveGameStatus

# from PySide6.import sip
Expand Down Expand Up @@ -171,6 +172,16 @@ def __init__(self, args: InitParams):
if args.show_console:
self.console = ConsoleDialog(game.app_title)
self.console.show()
self.game_process.stateChanged.connect(
lambda s: self.console.kill_button.setEnabled(
self.game_process.state() == QProcess.ProcessState.Running
)
)
self.game_process.stateChanged.connect(
lambda s: self.console.terminate_button.setEnabled(
self.game_process.state() == QProcess.ProcessState.Running
)
)

self.sync_dialog: Optional[CloudSyncDialog] = None

Expand Down Expand Up @@ -207,11 +218,17 @@ def __proc_log_stderr(self):

@Slot()
def __proc_term(self):
self.game_process.terminate()
if platform.system() == "Windows":
self.game_process.terminate()
else:
os.kill(self.game_process.processId(), signal.SIGINT)

@Slot()
def __proc_kill(self):
self.game_process.kill()
if platform.system() == "Windows":
self.game_process.kill()
else:
os.kill(self.game_process.processId(), signal.SIGINT)

def new_server_connection(self):
if self.socket is not None:
Expand Down Expand Up @@ -345,11 +362,20 @@ def launch_game(self, params: LaunchParams):

if appid := os.environ.get("SteamGameId", False):
params.environment["SteamGameId"] = appid
elif params.environment.get("SteamGameId", False):
appid = params.environment["SteamGameId"]

self.game_process.setProgram(executable)
# TODO: Add "SteamLauch" and "AppId=xxxxxx" here for steamdeck/gamescope
try:
appid = int(appid) >> 32
except ValueError:
pass
self.game_process.setArguments(
[*arguments, "subreaper", "SteamLaunch", f"AppId={int(appid) >> 32}", "--", params.executable, *params.arguments]
[*arguments, "subreaper", "SteamLaunch", f"AppId={appid}", "--", params.executable, *params.arguments]
)
self.game_process.setUnixProcessParameters(
QProcess.UnixProcessFlag.ResetSignalHandlers | QProcess.UnixProcessFlag.CreateNewSession
)
else:
self.game_process.setProgram(params.executable)
Expand All @@ -360,14 +386,16 @@ def launch_game(self, params: LaunchParams):

if self.args.debug and self.console:
self.console.log(str(self.game_process.program()))
self.console.log(str(self.game_process.arguments()))
self.console.log(shlex.join(self.game_process.arguments()))

self.game_process.setProcessEnvironment(dict_to_qprocenv(params.environment))
# send start message after process started
self.game_process.started.connect(
lambda: self.send_message(
StateChangedModel(
action=Actions.state_update, app_name=self.rgame.app_name, new_state=StateChangedModel.States.started
action=Actions.state_update,
app_name=self.rgame.app_name,
new_state=StateChangedModel.States.started,
)
)
)
Expand Down Expand Up @@ -435,7 +463,7 @@ def start(self):
else:
self.start_prepare()

def stop(self):
def stop(self, sig: int = signal.SIGINT):
try:
if self.console:
self.game_process.readyReadStandardOutput.disconnect()
Expand All @@ -447,10 +475,17 @@ def stop(self):
except (TypeError, RuntimeError) as e:
self.logger.error("Failed to disconnect process signals: %s", e)

if self.game_process.state() != QProcess.ProcessState.NotRunning:
self.game_process.kill()
exit_code = self.game_process.exitCode()
self.game_process.deleteLater()
if shiboken6.isValid(self.game_process): # pylint: disable=E1101
if self.game_process.state() != QProcess.ProcessState.NotRunning:
if sig == signal.SIGTERM:
self.__proc_term()
elif sig == signal.SIGINT:
self.__proc_kill()
self.game_process.waitForFinished()
exit_code = self.game_process.exitCode()
self.game_process.deleteLater()
else:
exit_code = 0

self.logger.info("Stopping server %s", self.server.socketDescriptor())
try:
Expand All @@ -477,17 +512,17 @@ def launcher(args: Namespace) -> int:

# This prevents ghost QLocalSockets, which block the name, which makes it unable to start
# No handling for SIGKILL
def sighandler(s, frame):
app.logger.info("%s received. Stopping", strsignal(s))
app.stop()
def signal_handler(sig, frame):
app.logger.info("%s received. Stopping", signal.strsignal(sig))
app.stop(sig)
app.exit(1)
return 1

signal(SIGINT, sighandler)
signal(SIGTERM, sighandler)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)

if not app.success:
app.stop()
app.stop(signal.SIGINT)
app.exit(1)
return 1

Expand Down
2 changes: 2 additions & 0 deletions rare/commands/launcher/console_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,13 @@ def __init__(self, app_title: str, parent=None):
# self.terminate_button.setVisible(platform.system() == "Windows")
button_layout.addWidget(self.terminate_button)
self.terminate_button.clicked.connect(lambda: self.term.emit())
self.terminate_button.setEnabled(False)

self.kill_button = QPushButton(self.tr("Kill"))
# self.kill_button.setVisible(platform.system() == "Windows")
button_layout.addWidget(self.kill_button)
self.kill_button.clicked.connect(lambda: self.kill.emit())
self.kill_button.setEnabled(False)

layout.addLayout(button_layout)

Expand Down
3 changes: 3 additions & 0 deletions rare/commands/launcher/lgd_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ def prepare_process(command: List[str], environment: Dict) -> Tuple[str, List[st
environ["MANGOHUD"] = "0"
# ensure shader compat dirs exist
if platform.system() in {"Linux", "FreeBSD"}:
environ["UMU_USE_STEAM"] = "1"
if "STEAM_COMPAT_CLIENT_INSTALL_PATH" not in environ:
environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = ""
if "WINEPREFIX" in environ and not os.path.isdir(environ["WINEPREFIX"]):
os.makedirs(environ["WINEPREFIX"], exist_ok=True)
if "STEAM_COMPAT_DATA_PATH" in environ:
Expand Down
44 changes: 43 additions & 1 deletion rare/commands/subreaper.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import logging
import os
import signal
import sys
from argparse import Namespace
from ctypes import CDLL, byref, c_int, create_string_buffer
from ctypes.util import find_library
from logging import getLogger
from typing import List
from pathlib import Path
from typing import Any, Generator, List

# Constant defined in prctl.h
# See prctl(2) for more details
Expand All @@ -20,6 +22,37 @@ def get_libc() -> str:
return find_library("c") or ""


def _get_pids() -> Generator[int, Any, None]:
yield from (int(pid.name) for pid in Path("/proc").glob("*") if pid.name.isdigit())


def get_pstree_from_pid(root_pid: int) -> set[int]:
"""Get descendent PIDs of a PID."""
descendants: set[int] = set()
pid_to_ppid: dict[int, int] = {}

for pid in _get_pids():
try:
path = Path(f"/proc/{pid}/status")
with path.open(mode="r", encoding="utf-8") as file:
st_ppid = next(line for line in file if line.startswith("PPid:"))
st_ppid = st_ppid.removeprefix("PPid:").strip()
pid_to_ppid[pid] = int(st_ppid)
except (FileNotFoundError, ProcessLookupError, ValueError):
continue

current_pid: list[int] = [root_pid]
while current_pid:
current = current_pid.pop()
# Ignore. mypy flags [arg-type] due to the reuse of pid variable
for pid, ppid in pid_to_ppid.items(): # type: ignore
if ppid == current and pid not in descendants:
descendants.add(pid) # type: ignore
current_pid.append(pid) # type: ignore

return descendants


def subreaper(args: Namespace, other: List[str]) -> int:
logger = getLogger("subreaper")
logging.basicConfig(
Expand All @@ -31,6 +64,12 @@ def subreaper(args: Namespace, other: List[str]) -> int:
logger.debug("command: %s", args)
logger.debug("arguments: %s", other)

def signal_handler(sig, frame):
logger.info("Caught '%s' signal.", signal.strsignal(sig))
pstree = get_pstree_from_pid(os.getpid())
for p in pstree:
os.kill(p, sig)

command: List[str] = [args.command, *other]
workdir: str = args.workdir
child_status: int = 0
Expand Down Expand Up @@ -64,6 +103,9 @@ def subreaper(args: Namespace, other: List[str]) -> int:
sys.stderr.flush()
os.chdir(workdir)
os.execvp(command[0], command) # noqa: S606
else:
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)

while True:
try:
Expand Down
3 changes: 2 additions & 1 deletion rare/components/tabs/settings/widgets/proton.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def __init__(self, rcore: RareCore, parent=None):
folder_layout.addWidget(self.compat_combo)
folder_layout.addWidget(self.compat_edit)
layout.addRow(self.tr("Compat folder"), folder_layout)
layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
layout.setFormAlignment(Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignTop)
# layout.addRow(button_layout)
Expand Down Expand Up @@ -152,6 +152,7 @@ def __on_tool_changed(self, index):
library_paths = (
":".join([library_paths, os.path.dirname(install_path)]) if library_paths else os.path.dirname(install_path)
)
# https://gitlab.steamos.cloud/steamrt/steam-runtime-tools/-/blob/main/docs/steam-compat-tool-interface.md#non-steam-games
steam_environ["STEAM_COMPAT_INSTALL_PATH"] = install_path
steam_environ["STEAM_COMPAT_LIBRARY_PATHS"] = library_paths
for key, value in steam_environ.items():
Expand Down
3 changes: 2 additions & 1 deletion rare/models/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from dataclasses import dataclass, field
from datetime import datetime, timezone
from logging import getLogger
from secrets import token_hex
from threading import Lock
from typing import Dict, List, Optional, Set, Tuple

Expand Down Expand Up @@ -465,7 +466,7 @@ def steam_appid(self) -> Optional[str]:
def steam_appid(self, appid: str) -> None:
config.adjust_envvar(self.app_name, "SteamAppId", appid)
config.adjust_envvar(self.app_name, "SteamGameId", appid)
config.adjust_envvar(self.app_name, "STEAM_COMPAT_APP_ID", appid)
config.adjust_envvar(self.app_name, "STEAM_COMPAT_APP_ID", f"rare_{self.app_name}")
config.adjust_envvar(self.app_name, "UMU_ID", f"umu-{appid}" if appid else "umu-default")
self.metadata.steam_appid = appid
self.__save_metadata()
Expand Down
2 changes: 1 addition & 1 deletion rare/models/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class Settings(Namespace):
window_width = Setting(key="window_width", default=1280, dtype=int)
window_height = Setting(key="window_height", default=720, dtype=int)
notification = Setting(key="notification", default=True, dtype=bool)
log_games = Setting(key="show_console", default=False, dtype=bool)
log_games = Setting(key="show_console", default=pf.system() != "Windows", dtype=bool)

color_scheme = Setting(key="color_scheme", default="", dtype=str)
style_sheet = Setting(key="style_sheet", default="RareStyle", dtype=str)
Expand Down
2 changes: 2 additions & 0 deletions rare/utils/compat/steam.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ def get_steam_environment(
environ["STORE"] = ""
environ["STEAM_COMPAT_DATA_PATH"] = ""
environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = ""
environ["STEAM_COMPAT_LAUNCHER_SERVICE"] = ""
environ["STEAM_COMPAT_INSTALL_PATH"] = ""
environ["STEAM_COMPAT_LIBRARY_PATHS"] = ""
environ["STEAM_COMPAT_MOUNTS"] = ""
Expand All @@ -349,6 +350,7 @@ def get_steam_environment(

environ["STORE"] = "egs"
environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = tool.steam_path
environ["STEAM_COMPAT_LAUNCHER_SERVICE"] = tool.layer
if isinstance(tool, ProtonTool):
environ["STEAM_COMPAT_LIBRARY_PATHS"] = tool.steam_library
if tool.runtime is not None:
Expand Down
6 changes: 3 additions & 3 deletions rare/utils/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,14 @@ def setup_compat_shaders_dir(path: str) -> Dict:
"__GL_SHADER_DISK_CACHE_PATH": os.path.join(path, "nvidiav1"),
"__GL_SHADER_DISK_CACHE_READ_ONLY_APP_NAME": "steam_shader_cache;steamapp_merged_shader_cache",
"__GL_SHADER_DISK_CACHE_SIZE": "10737418240", # 10GiB
"__GL_SHADER_DISK_CACHE_SKIP_CLEANUP": "1",
#"__GL_SHADER_DISK_CACHE_SKIP_CLEANUP": "1",
# Mesa
"MESA_DISK_CACHE_READ_ONLY_FOZ_DBS": "steam_cache,steam_precompiled",
"MESA_DISK_CACHE_SINGLE_FILE": "1",
"MESA_GLSL_CACHE_DIR": path,
"MESA_GLSL_CACHE_MAX_SIZE": "5G",
"MESA_GLSL_CACHE_MAX_SIZE": "10G",
"MESA_SHADER_CACHE_DIR": path,
"MESA_SHADER_CACHE_MAX_SIZE": "5G",
"MESA_SHADER_CACHE_MAX_SIZE": "10G",
# AMD VK
"AMD_VK_PIPELINE_CACHE_FILENAME": shader_cache_name,
"AMD_VK_PIPELINE_CACHE_PATH": os.path.join(path, "AMDv1"),
Expand Down
2 changes: 1 addition & 1 deletion rare/utils/workarounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def __screen_width() -> int:
"ff1d9bf6b1304cb9a12b8754afa78ae5": {
"options": {
"override_exe": {
"value": "EternalThreads.exe",
"value": "EternalThreadsBuild/EternalThreads.exe",
"os": __os_compat,
},
},
Expand Down
Loading