feat(tray): add Wine tools submenu #281
No files matched your search
@@ -1572,6 +1572,7 @@ pw_kill_autostart () {
|
||||
export -f pw_kill_autostart
|
||||
|
||||
stop_portproton () {
|
||||
pw_remove_session_env
|
||||
[[ "$int_xneur" == "1" ]] && xneur &
|
||||
|
||||
if [[ "$PW_USE_US_LAYOUT" == "1" ]] \
|
||||
@@ -3845,6 +3846,7 @@ start_portproton () {
|
||||
pw_mangohud_check
|
||||
pw_vkbasalt_check
|
||||
pw_lsfg_vk_check
|
||||
pw_save_session_env || print_warning "Could not save Wine session environment"
|
||||
}
|
||||
|
||||
pw_run () {
|
||||
@@ -3996,6 +3998,80 @@ pw_run () {
|
||||
}
|
||||
export -f pw_run
|
||||
|
||||
pw_save_session_env () {
|
||||
local session_dir="${PORT_DATA_PATH}/data/tmp/wine-session-$(id -u)"
|
||||
local session_tmp
|
||||
|
||||
( umask 077; mkdir -p "$session_dir" ) || return 1
|
||||
[[ ! -L "$session_dir" && -O "$session_dir" ]] || return 1
|
||||
[[ "$(stat -c %a "$session_dir")" == 700 ]] || return 1
|
||||
|
||||
session_tmp="$(mktemp "$session_dir/env.XXXXXX")" || return 1
|
||||
if ( export PW_TERM; export -p > "$session_tmp" ) \
|
||||
&& mv -fT "$session_tmp" "$session_dir/session.env" ; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -f "$session_tmp"
|
||||
return 1
|
||||
}
|
||||
|
||||
pw_remove_session_env () {
|
||||
local session_dir="${PORT_DATA_PATH}/data/tmp/wine-session-$(id -u)"
|
||||
[[ ! -L "$session_dir" && -O "$session_dir" ]] || return 0
|
||||
local session_env="$session_dir/session.env"
|
||||
[[ ! -L "$session_env" && -O "$session_env" ]] || return 0
|
||||
if grep -qsx "declare -x PW_START_PID=\"${PW_START_PID}\"" "$session_env" ; then
|
||||
rm -f "$session_env"
|
||||
fi
|
||||
}
|
||||
|
||||
pw_session_tool () {
|
||||
local tool="$1" session_pid session_env
|
||||
local session_dir="${PORT_DATA_PATH}/data/tmp/wine-session-$(id -u)"
|
||||
local session_wait_seconds="${2:-30}" wait_left
|
||||
wait_left="$session_wait_seconds"
|
||||
|
||||
case "$tool" in
|
||||
winefile|taskmgr|winecfg|regedit|cmd|uninstaller) ;;
|
||||
*) print_error "Unsupported Wine tool: $tool" ; return 1 ;;
|
||||
esac
|
||||
|
||||
session_env="$session_dir/session.env"
|
||||
while (( wait_left > 0 )) ; do
|
||||
if [[ ! -L "$session_dir" && -O "$session_dir" ]] \
|
||||
&& [[ "$(stat -c %a "$session_dir")" == 700 ]] \
|
||||
&& [[ ! -L "$session_env" && -O "$session_env" ]] \
|
||||
&& [[ "$(stat -c %a "$session_env")" == 600 ]] \
|
||||
&& session_pid="$(sed -n 's/^declare -x PW_START_PID="\([0-9]*\)"$/\1/p' "$session_env")" \
|
||||
&& [[ -n "$session_pid" ]] \
|
||||
&& kill -0 "$session_pid" 2>/dev/null ; then
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
(( --wait_left ))
|
||||
done
|
||||
|
||||
if (( wait_left == 0 )) ; then
|
||||
print_error "Wine session unavailable after ${session_wait_seconds} seconds"
|
||||
return 2
|
||||
fi
|
||||
|
||||
source "$session_env" || return 1
|
||||
PATH_TO_GAME="${WINEPREFIX}/drive_c"
|
||||
[[ -d "$PATH_TO_GAME" ]] || return 1
|
||||
|
||||
unset LAUNCH_PARAMETERS PW_RUN_AFTER_EXE PW_RUN_GAMESCOPE PW_USE_TERMINAL
|
||||
PW_TMPFS_PATH="$(mktemp -d "$session_dir/tool.XXXXXX")" || return 1
|
||||
export PW_TMPFS_PATH
|
||||
export PW_LOG_FILE="$PW_TMPFS_PATH/wine.log"
|
||||
|
||||
[[ "$tool" == cmd ]] && PW_USE_TERMINAL=1
|
||||
[[ "$tool" == winecfg || "$tool" == regedit ]] && export GST_PLUGIN_SYSTEM_PATH_1_0=""
|
||||
pw_run "$tool" || return 1
|
||||
}
|
||||
|
||||
pw_launch_wrapper () {
|
||||
if [[ -z "${PW_RUN_AFTER_EXE:-}" ]] ; then
|
||||
"$@"
|
||||
|
||||
@@ -67,6 +67,21 @@ export PW_LOG_FILE="${PORT_DATA_PATH}/PortProton.log"
|
||||
# shellcheck source=/dev/null
|
||||
source "$PORT_SCRIPTS_PATH/functions_helper"
|
||||
|
||||
if [[ "${1:-}" == "cli" && "${2:-}" == "--wine-session-tool" ]] ; then
|
||||
session_unavailable_status=2
|
||||
session_wait_seconds=30
|
||||
[[ -n "${4:-}" && -n "${5:-}" ]] && session_wait_seconds=1
|
||||
if pw_session_tool "${3:-}" "$session_wait_seconds" ; then
|
||||
exit 0
|
||||
else
|
||||
session_tool_status=$?
|
||||
fi
|
||||
if [[ "$session_tool_status" != "$session_unavailable_status" || -z "${4:-}" || -z "${5:-}" ]] ; then
|
||||
python_module dbus_tools notify -i error "Wine" "Failed to start Wine tool: ${3:-}" &>/dev/null
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
export PORT_WINE_TMP_PATH="${PORT_DATA_PATH}/data/tmp"
|
||||
create_new_dir "$PORT_WINE_TMP_PATH"
|
||||
rm -f "$PORT_WINE_TMP_PATH"/*.{exe,msi,tar}*
|
||||
@@ -280,11 +295,7 @@ case "$1" in
|
||||
--show-ppdb Show the content of .ppdb file for specified .exe file
|
||||
--backup-prefix Backup specified prefix to a file
|
||||
--restore-prefix Restore prefix from backup file
|
||||
--winefile Open wine file explorer, requires WINE version and prefix name
|
||||
--winecfg Open wine configuration, requires WINE version and prefix name
|
||||
--winecmd Open wine command prompt, requires WINE version and prefix name
|
||||
--winereg Open wine registry editor, requires WINE version and prefix name
|
||||
--wine_uninstaller Open wine uninstaller, requires WINE version and prefix name
|
||||
--wine-session-tool Open a Wine tool in the current session, or use optional WINE and prefix
|
||||
--winetricks-list-all List winetricks DLLs, fonts, and settings
|
||||
--winetricks-install Install winetricks items, requires WINE version and prefix name
|
||||
--clear_pfx Clear specified prefix, requires WINE version and prefix name
|
||||
@@ -300,7 +311,7 @@ Usage examples:
|
||||
portproton cli --del-user-conf PW_MANGOHUD
|
||||
portproton cli --backup-prefix DEFAULT /path/to/backup/directory
|
||||
portproton cli --restore-prefix /path/to/backup/file.ppack
|
||||
portproton cli --winecfg WINE_LG DEFAULT
|
||||
portproton cli --wine-session-tool winecfg WINE_LG DEFAULT
|
||||
portproton cli --winetricks-list-all WINE_LG DEFAULT
|
||||
portproton cli --winetricks-install WINE_LG DEFAULT --no-force corefonts
|
||||
portproton cli --mangohud-preview \"fps,frametime,cpu_temp,gpu_temp\"
|
||||
@@ -462,37 +473,21 @@ Usage examples:
|
||||
pw_unpack_prefix "$2"
|
||||
exit $?
|
||||
;;
|
||||
--winefile)
|
||||
get_wine_and_pfx "$2" "$3"
|
||||
--wine-session-tool)
|
||||
tool="$2"
|
||||
case "$tool" in
|
||||
winefile|taskmgr|winecfg|regedit|cmd|uninstaller) ;;
|
||||
*) print_error "Unsupported Wine tool: $tool" ; exit 1 ;;
|
||||
esac
|
||||
get_wine_and_pfx "$3" "$4"
|
||||
start_portproton
|
||||
pw_run winefile
|
||||
stop_portproton
|
||||
;;
|
||||
--winecfg)
|
||||
get_wine_and_pfx "$2" "$3"
|
||||
start_portproton
|
||||
export GST_PLUGIN_SYSTEM_PATH_1_0=""
|
||||
pw_run winecfg
|
||||
stop_portproton
|
||||
;;
|
||||
--winecmd)
|
||||
get_wine_and_pfx "$2" "$3"
|
||||
start_portproton
|
||||
cd "${PORT_DATA_PATH}/data/prefixes/${PW_PREFIX_NAME}/drive_c" || fatal "Prefix drive_c directory not found"
|
||||
PW_USE_TERMINAL=1 pw_run cmd
|
||||
stop_portproton
|
||||
;;
|
||||
--winereg)
|
||||
get_wine_and_pfx "$2" "$3"
|
||||
start_portproton
|
||||
export GST_PLUGIN_SYSTEM_PATH_1_0=""
|
||||
pw_run regedit
|
||||
stop_portproton
|
||||
;;
|
||||
--wine_uninstaller)
|
||||
get_wine_and_pfx "$2" "$3"
|
||||
start_portproton
|
||||
pw_run uninstaller
|
||||
[[ "$tool" == winecfg || "$tool" == regedit ]] && export GST_PLUGIN_SYSTEM_PATH_1_0=""
|
||||
if [[ "$tool" == cmd ]] ; then
|
||||
cd "${PORT_DATA_PATH}/data/prefixes/${PW_PREFIX_NAME}/drive_c" || fatal "Prefix drive_c directory not found"
|
||||
PW_USE_TERMINAL=1 pw_run "$tool"
|
||||
else
|
||||
pw_run "$tool"
|
||||
fi
|
||||
stop_portproton
|
||||
;;
|
||||
--winetricks-list-all)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generate shell completion files for bash, zsh, and fish
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
@@ -160,11 +160,12 @@ class MainWindowWineTabMixin(_MainWindowTypingBase):
|
||||
tools_grid.setSpacing(6)
|
||||
|
||||
tools = [
|
||||
("--winecfg", _("Wine Configuration")),
|
||||
("--winereg", _("Registry Editor")),
|
||||
("--winefile", _("File Explorer")),
|
||||
("--winecmd", _("Command Prompt")),
|
||||
("--wine_uninstaller", _("Uninstaller")),
|
||||
("winecfg", _("Wine Configuration")),
|
||||
("regedit", _("Registry Editor")),
|
||||
("winefile", _("File Explorer")),
|
||||
("cmd", _("Command Prompt")),
|
||||
("uninstaller", _("Uninstaller")),
|
||||
("taskmgr", _("Task Manager")),
|
||||
]
|
||||
|
||||
for i, (tool_cmd, tool_name) in enumerate(tools):
|
||||
@@ -237,7 +238,7 @@ class MainWindowWineTabMixin(_MainWindowTypingBase):
|
||||
env_vars = os.environ.copy()
|
||||
self._check_missing_prefix_by_name_before_launch(prefix, env_vars)
|
||||
start_sh = self.start_sh
|
||||
cmd = start_sh + ["cli", cli_arg, wine, prefix]
|
||||
cmd = start_sh + ["cli", "--wine-session-tool", cli_arg, wine, prefix]
|
||||
|
||||
# Show progress bar before launch
|
||||
self.wine_progress_bar.setVisible(True)
|
||||
@@ -269,16 +270,7 @@ class MainWindowWineTabMixin(_MainWindowTypingBase):
|
||||
|
||||
def _check_wine_process(self, cli_arg):
|
||||
"""Check if target .exe process started."""
|
||||
exe_map = {
|
||||
"--winecfg": "winecfg.exe",
|
||||
"--winereg": "regedit.exe",
|
||||
"--winefile": "winefile.exe",
|
||||
"--winecmd": "cmd.exe",
|
||||
"--wine_uninstaller": "uninstaller.exe",
|
||||
}
|
||||
target_exe = exe_map.get(cli_arg, "")
|
||||
if not target_exe:
|
||||
return
|
||||
target_exe = f"{cli_arg}.exe"
|
||||
|
||||
# Check processes via psutil
|
||||
for proc in psutil.process_iter(attrs=["name"]):
|
||||
|
||||
@@ -3,6 +3,7 @@ import subprocess
|
||||
import signal
|
||||
import psutil
|
||||
import os
|
||||
from PySide6.QtCore import QProcess
|
||||
from PySide6.QtWidgets import QSystemTrayIcon, QMenu, QApplication, QMessageBox
|
||||
from PySide6.QtGui import QIcon, QAction
|
||||
from portprotonqt.logger import get_logger
|
||||
@@ -45,6 +46,39 @@ def _restart_args(args: list[str]) -> list[str]:
|
||||
return restart_args
|
||||
|
||||
|
||||
def create_wine_tools_menu(parent: QMenu, start_sh: list[str]) -> QMenu:
|
||||
"""Create tools that join the running game's Wine session."""
|
||||
menu = QMenu("Wine", parent)
|
||||
tools = (
|
||||
("winefile", _("File Explorer")),
|
||||
("taskmgr", _("Task Manager")),
|
||||
("winecfg", _("Wine Configuration")),
|
||||
("regedit", _("Registry Editor")),
|
||||
("cmd", _("Command Prompt")),
|
||||
("uninstaller", _("Uninstaller")),
|
||||
)
|
||||
for tool, title in tools:
|
||||
action = menu.addAction(title)
|
||||
action.triggered.connect(
|
||||
lambda checked=False, name=tool: launch_session_wine_tool(start_sh, name)
|
||||
)
|
||||
return menu
|
||||
|
||||
|
||||
def launch_session_wine_tool(start_sh: list[str], tool: str) -> None:
|
||||
"""Start a Wine tool without running the game's startup or cleanup."""
|
||||
if not start_sh:
|
||||
logger.error("Cannot launch Wine tool: PortProton command is missing")
|
||||
QMessageBox.warning(None, _("Error"), _("Failed to start process."))
|
||||
return
|
||||
started, _pid = QProcess.startDetached(
|
||||
start_sh[0], start_sh[1:] + ["cli", "--wine-session-tool", tool]
|
||||
)
|
||||
if not started:
|
||||
logger.error("Failed to start Wine tool %s", tool)
|
||||
QMessageBox.warning(None, _("Error"), _("Failed to start process."))
|
||||
|
||||
|
||||
class TrayManager:
|
||||
"""Tray management module for PortProtonQt.
|
||||
|
||||
@@ -83,8 +117,12 @@ class TrayManager:
|
||||
self.pause_game_action.setEnabled(False)
|
||||
self.pause_game_action.triggered.connect(self.toggle_game_pause)
|
||||
|
||||
self.wine_tools_menu = create_wine_tools_menu(self.tray_menu, self.main_window.start_sh)
|
||||
self.wine_tools_menu.setEnabled(False)
|
||||
|
||||
self.tray_menu.addAction(self.pause_game_action)
|
||||
self.tray_menu.addAction(self.stop_game_action)
|
||||
self.tray_menu.addMenu(self.wine_tools_menu)
|
||||
self.tray_menu.addSeparator()
|
||||
self.tray_menu.addSeparator()
|
||||
|
||||
@@ -103,6 +141,7 @@ class TrayManager:
|
||||
|
||||
self.tray_menu.addAction(self.pause_game_action)
|
||||
self.tray_menu.addAction(self.stop_game_action)
|
||||
self.tray_menu.addMenu(self.wine_tools_menu)
|
||||
self.update_game_actions()
|
||||
if self.minimal_mode:
|
||||
return
|
||||
@@ -151,6 +190,7 @@ class TrayManager:
|
||||
target_exe = getattr(self.main_window, "target_exe", None)
|
||||
has_running_game = bool(game_processes or target_exe)
|
||||
self.stop_game_action.setEnabled(has_running_game)
|
||||
self.wine_tools_menu.setEnabled(has_running_game)
|
||||
self.pause_game_action.setEnabled(bool(processes))
|
||||
paused = self._are_processes_paused(processes)
|
||||
self.pause_game_action.setText(_("Resume Game") if paused else _("Pause Game"))
|
||||
|
||||
+101
-4
@@ -42,7 +42,7 @@ from portprotonqt.game_library_manager import FullLibraryTile, GameLibraryManage
|
||||
from portprotonqt.input_manager import InputManager
|
||||
from portprotonqt.main_window import MainWindow
|
||||
from portprotonqt.search_utils import SearchOptimizer, search_index
|
||||
from portprotonqt.tray_manager import TrayManager
|
||||
from portprotonqt.tray_manager import TrayManager, create_wine_tools_menu, launch_session_wine_tool
|
||||
from portprotonqt.themes.standart.styles.constants import GAME_CARD_ANIMATION
|
||||
from portprotonqt.portproton_api import remove_empty_custom_data_dirs
|
||||
import portprotonqt.tabs.autoinstall_tab as autoinstall_tab_module
|
||||
@@ -374,18 +374,71 @@ def test_minimal_tray_contains_game_actions() -> None:
|
||||
_application = QApplication.instance() or QApplication([])
|
||||
manager = TrayManager.__new__(TrayManager)
|
||||
manager.tray_menu = QMenu()
|
||||
manager.pause_game_action = QAction("Pause Game", manager.tray_menu)
|
||||
manager.stop_game_action = QAction("Stop Game", manager.tray_menu)
|
||||
owner = QWidget()
|
||||
manager.pause_game_action = QAction("Pause Game", owner)
|
||||
manager.stop_game_action = QAction("Stop Game", owner)
|
||||
manager.wine_tools_menu = create_wine_tools_menu(manager.tray_menu, ["portproton"])
|
||||
manager.minimal_mode = True
|
||||
manager.update_game_actions = MagicMock()
|
||||
|
||||
manager.refresh_tray_menu()
|
||||
manager.refresh_tray_menu()
|
||||
|
||||
assert manager.tray_menu.actions() == [
|
||||
manager.pause_game_action,
|
||||
manager.stop_game_action,
|
||||
manager.wine_tools_menu.menuAction(),
|
||||
]
|
||||
manager.update_game_actions.assert_called_once_with()
|
||||
assert manager.update_game_actions.call_count == 2
|
||||
|
||||
|
||||
@mark.parametrize("tool_index,tool", list(enumerate(
|
||||
("winefile", "taskmgr", "winecfg", "regedit", "cmd", "uninstaller")
|
||||
)))
|
||||
def test_wine_tools_menu_launches_session_tool(
|
||||
monkeypatch: MonkeyPatch, tool_index: int, tool: str,
|
||||
) -> None:
|
||||
_application = QApplication.instance() or QApplication([])
|
||||
parent = QMenu()
|
||||
start = MagicMock(return_value=(True, 123))
|
||||
monkeypatch.setattr("portprotonqt.tray_manager.QProcess.startDetached", start)
|
||||
menu = create_wine_tools_menu(parent, ["bash", "/path with spaces/start.sh"])
|
||||
|
||||
assert len(menu.actions()) == 6
|
||||
menu.actions()[tool_index].trigger()
|
||||
|
||||
start.assert_called_once_with(
|
||||
"bash", ["/path with spaces/start.sh", "cli", "--wine-session-tool", tool]
|
||||
)
|
||||
|
||||
|
||||
def test_wine_tool_start_failure_is_reported(monkeypatch: MonkeyPatch) -> None:
|
||||
_application = QApplication.instance() or QApplication([])
|
||||
warning = MagicMock()
|
||||
monkeypatch.setattr("portprotonqt.tray_manager.QMessageBox.warning", warning)
|
||||
monkeypatch.setattr(
|
||||
"portprotonqt.tray_manager.QProcess.startDetached", lambda *args: (False, 0)
|
||||
)
|
||||
|
||||
launch_session_wine_tool(["portproton"], "winefile")
|
||||
|
||||
warning.assert_called_once()
|
||||
|
||||
|
||||
@mark.parametrize("running", [False, True])
|
||||
def test_wine_tools_menu_requires_running_game(running: bool) -> None:
|
||||
_application = QApplication.instance() or QApplication([])
|
||||
manager = TrayManager.__new__(TrayManager)
|
||||
manager.main_window = SimpleNamespace(game_processes=[], target_exe="game.exe" if running else None)
|
||||
manager.stop_game_action = QAction()
|
||||
manager.pause_game_action = QAction()
|
||||
manager.wine_tools_menu = QMenu()
|
||||
manager._game_process_tree = MagicMock(return_value=[])
|
||||
|
||||
manager.update_game_actions()
|
||||
|
||||
assert manager.wine_tools_menu.isEnabled() is running
|
||||
|
||||
|
||||
def test_tray_pauses_game_process_tree(monkeypatch: MonkeyPatch) -> None:
|
||||
child = MagicMock(pid=12)
|
||||
@@ -3378,3 +3431,47 @@ def test_custom_combo_opens_readonly_field_on_mouse_release(readonly: bool) -> N
|
||||
assert combo.view().isVisible() is readonly
|
||||
combo.hidePopup()
|
||||
combo.close()
|
||||
|
||||
|
||||
@mark.parametrize("tool", ["winecfg", "regedit", "winefile", "cmd", "uninstaller", "taskmgr"])
|
||||
@mark.parametrize("running", [False, True])
|
||||
def test_wine_settings_tools_use_unified_command(
|
||||
monkeypatch: MonkeyPatch, tool: str, running: bool,
|
||||
) -> None:
|
||||
process = MagicMock()
|
||||
monkeypatch.setattr("portprotonqt.tabs.wine_tab.QProcess", MagicMock(return_value=process))
|
||||
window = MagicMock()
|
||||
window.game_processes = [MagicMock()] if running else []
|
||||
window.target_exe = "game.exe" if running else None
|
||||
window.start_sh = ["bash", "/path with spaces/start.sh"]
|
||||
window.wineCombo.currentText.return_value = "WINE_LG"
|
||||
window.prefixCombo.currentText.return_value = "my prefix"
|
||||
|
||||
WineMixin.launch_generic_tool(window, tool)
|
||||
|
||||
process.start.assert_called_once_with(
|
||||
"bash", ["/path with spaces/start.sh", "cli", "--wine-session-tool", tool, "WINE_LG", "MY_PREFIX"],
|
||||
)
|
||||
window._start_wine_process_monitor.assert_called_once_with(tool)
|
||||
|
||||
|
||||
|
||||
def test_silent_tray_includes_wine_tools_menu(monkeypatch: MonkeyPatch) -> None:
|
||||
_application = QApplication.instance() or QApplication([])
|
||||
manager = TrayManager.__new__(TrayManager)
|
||||
manager.tray_menu = QMenu()
|
||||
owner = QWidget()
|
||||
manager.pause_game_action = QAction("Pause Game", owner)
|
||||
manager.stop_game_action = QAction("Stop Game", owner)
|
||||
manager.wine_tools_menu = create_wine_tools_menu(manager.tray_menu, ["portproton"])
|
||||
manager.update_game_actions = MagicMock()
|
||||
start = MagicMock(return_value=(True, 123))
|
||||
monkeypatch.setattr("portprotonqt.tray_manager.QProcess.startDetached", start)
|
||||
|
||||
manager.set_minimal_mode()
|
||||
|
||||
tools = manager.tray_menu.actions()[2].menu()
|
||||
assert isinstance(tools, QMenu)
|
||||
assert len(tools.actions()) == 6
|
||||
tools.actions()[1].trigger()
|
||||
start.assert_called_once_with("portproton", ["cli", "--wine-session-tool", "taskmgr"])
|
||||
@@ -232,3 +232,141 @@ def test_executable_metadata_uses_pefile_and_preserves_lnk_fallback(
|
||||
if expected_calls == ["pe"]:
|
||||
assert "File Description: Description" in metadata
|
||||
assert "Company Name: Sony" in metadata
|
||||
|
||||
|
||||
def test_wine_session_missing_returns_distinct_status(tmp_path: Path) -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text()
|
||||
definition = helper.split("pw_session_tool () {", 1)[1].split("pw_launch_wrapper () {", 1)[0]
|
||||
script = "pw_session_tool () {" + definition + '''
|
||||
print_error () { echo "$@" >&2; }
|
||||
sleep () { return 0; }
|
||||
pw_session_tool taskmgr 1
|
||||
'''
|
||||
result = subprocess.run(
|
||||
["bash", "-c", script], cwd=tmp_path, capture_output=True, text=True, check=False,
|
||||
env={"PATH": os.defpath, "PORT_DATA_PATH": str(tmp_path)},
|
||||
)
|
||||
|
||||
assert result.returncode == 2
|
||||
|
||||
|
||||
@mark.parametrize("tool", ["winecfg", "regedit", "winefile", "cmd", "uninstaller", "taskmgr"])
|
||||
def test_wine_session_tool_standalone_uses_selected_wine_and_prefix(tmp_path: Path, tool: str) -> None:
|
||||
start = Path("build-aux/share/portproton/scripts/start.sh").read_text()
|
||||
block = start.split(" --wine-session-tool)\n", 1)[1].split(" --winetricks-list-all)", 1)[0]
|
||||
script = '''
|
||||
get_wine_and_pfx () { printf '%s\\n' "$@"; }
|
||||
start_portproton () { mkdir -p "$PORT_DATA_PATH/data/prefixes/$PW_PREFIX_NAME/drive_c"; echo start; }
|
||||
pw_run () { printf '%s\\n' "$@"; }
|
||||
stop_portproton () { echo stop; }
|
||||
''' + 'case "$1" in --wine-session-tool)\n' + block + '\nesac\n'
|
||||
result = subprocess.run(
|
||||
["bash", "-c", script, "bash", "--wine-session-tool", tool, "WINE_LG", "MY_PREFIX"],
|
||||
cwd=tmp_path, capture_output=True, text=True, check=False,
|
||||
env={"PATH": os.defpath, "PORT_DATA_PATH": str(tmp_path), "PW_PREFIX_NAME": "MY_PREFIX"},
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert result.stdout.splitlines() == ["WINE_LG", "MY_PREFIX", "start", tool, "stop"]
|
||||
|
||||
|
||||
@mark.parametrize("tool", ["winefile", "taskmgr", "winecfg", "regedit", "cmd", "uninstaller"])
|
||||
@mark.parametrize("tool_exit_code", [0, 2])
|
||||
def test_wine_session_tools_reuse_prefix_without_game_options(
|
||||
tmp_path: Path, tool: str, tool_exit_code: int,
|
||||
) -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text()
|
||||
definitions = helper.split("pw_save_session_env () {", 1)[1].split("pw_launch_wrapper () {", 1)[0]
|
||||
script = "pw_save_session_env () {" + definitions + '''
|
||||
print_error () { echo "$@" >&2; }
|
||||
pw_run () {
|
||||
printf '%s\\n' "$1" "$WINEPREFIX" "${PW_USE_TERMINAL:-0}" "${LAUNCH_PARAMETERS:-}" \\
|
||||
"${PW_RUN_AFTER_EXE:-}" "${PW_RUN_GAMESCOPE:-}" "$PW_TMPFS_PATH"
|
||||
return "$TOOL_EXIT_CODE"
|
||||
}
|
||||
PW_TERM="test-terminal"
|
||||
export PW_START_PID="$$" WINEPREFIX="$PORT_DATA_PATH/prefix with spaces"
|
||||
export LAUNCH_PARAMETERS="--game-only" PW_RUN_AFTER_EXE=trainer.exe PW_RUN_GAMESCOPE=gamescope
|
||||
pw_save_session_env || exit 2
|
||||
pw_session_tool "$1"
|
||||
'''
|
||||
(tmp_path / "prefix with spaces/drive_c").mkdir(parents=True)
|
||||
result = subprocess.run(
|
||||
["bash", "-c", script, "bash", tool], capture_output=True, text=True,
|
||||
env={"PATH": os.defpath, "PORT_DATA_PATH": str(tmp_path),
|
||||
"TOOL_EXIT_CODE": str(tool_exit_code)}, check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == int(tool_exit_code != 0), result.stderr
|
||||
lines = result.stdout.splitlines()
|
||||
assert lines[:6] == [tool, str(tmp_path / "prefix with spaces"), "1" if tool == "cmd" else "0", "", "", ""]
|
||||
assert Path(lines[6]).is_dir()
|
||||
assert Path(lines[6]).stat().st_mode & 0o777 == 0o700
|
||||
session = next((tmp_path / "data/tmp").glob("wine-session-*/session.env"))
|
||||
assert session.stat().st_mode & 0o777 == 0o600
|
||||
assert 'declare -x PW_TERM="test-terminal"' in session.read_text()
|
||||
|
||||
|
||||
@mark.parametrize("unsafe_directory", [False, True])
|
||||
def test_wine_session_save_does_not_follow_symlinks(tmp_path: Path, unsafe_directory: bool) -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text()
|
||||
definition = helper.split("pw_save_session_env () {", 1)[1].split("pw_remove_session_env () {", 1)[0]
|
||||
session_dir = tmp_path / "data/tmp" / f"wine-session-{os.getuid()}"
|
||||
session_dir.parent.mkdir(parents=True)
|
||||
victim = tmp_path / "victim"
|
||||
victim.write_text("keep this")
|
||||
if unsafe_directory:
|
||||
target = tmp_path / "other-directory"
|
||||
target.mkdir(mode=0o700)
|
||||
session_dir.symlink_to(target, target_is_directory=True)
|
||||
else:
|
||||
session_dir.mkdir(mode=0o700)
|
||||
(session_dir / "session.env").symlink_to(victim)
|
||||
result = subprocess.run(
|
||||
["bash", "-c", "pw_save_session_env () {" + definition + "\npw_save_session_env"],
|
||||
capture_output=True, text=True,
|
||||
env={"PATH": os.defpath, "PORT_DATA_PATH": str(tmp_path)}, check=False,
|
||||
)
|
||||
|
||||
assert victim.read_text() == "keep this"
|
||||
assert result.returncode == (1 if unsafe_directory else 0), result.stderr
|
||||
if not unsafe_directory:
|
||||
assert not (session_dir / "session.env").is_symlink()
|
||||
|
||||
|
||||
def test_wine_session_tool_rejects_unknown_tool_before_reading_environment(tmp_path: Path) -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text()
|
||||
definition = helper.split("pw_session_tool () {", 1)[1].split("pw_launch_wrapper () {", 1)[0]
|
||||
script = "pw_session_tool () {" + definition + '''
|
||||
print_error () { echo "$@" >&2; }
|
||||
sleep () { exit 99; }
|
||||
pw_session_tool '../../other-command'
|
||||
'''
|
||||
result = subprocess.run(
|
||||
["bash", "-c", script], capture_output=True, text=True,
|
||||
env={"PATH": os.defpath, "PORT_DATA_PATH": str(tmp_path)}, check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 1
|
||||
assert "Unsupported Wine tool" in result.stderr
|
||||
|
||||
|
||||
def test_wine_session_cleanup_preserves_newer_session(tmp_path: Path) -> None:
|
||||
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text()
|
||||
definitions = helper.split("pw_save_session_env () {", 1)[1].split("pw_session_tool () {", 1)[0]
|
||||
script = "pw_save_session_env () {" + definitions + '''
|
||||
export PW_START_PID="$$"
|
||||
pw_save_session_env || exit 2
|
||||
PW_START_PID=99999999
|
||||
pw_remove_session_env
|
||||
[[ -f "$PORT_DATA_PATH/data/tmp/wine-session-$(id -u)/session.env" ]] || exit 3
|
||||
PW_START_PID="$$"
|
||||
pw_remove_session_env
|
||||
[[ ! -e "$PORT_DATA_PATH/data/tmp/wine-session-$(id -u)/session.env" ]]
|
||||
'''
|
||||
result = subprocess.run(
|
||||
["bash", "-c", script], capture_output=True, text=True,
|
||||
env={"PATH": os.defpath, "PORT_DATA_PATH": str(tmp_path)}, check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
Reference in new issue
Block a user