diff --git a/ulwgl_run.py b/ulwgl_run.py index 713a98cbd..91c17fec2 100755 --- a/ulwgl_run.py +++ b/ulwgl_run.py @@ -13,6 +13,7 @@ from ulwgl_consts import Level from ulwgl_util import msg from ulwgl_log import log, console_handler, debug_formatter +from ulwgl_util import UnixUser verbs: Set[str] = { "waitforexitandrun", @@ -86,6 +87,11 @@ def set_log() -> None: def setup_pfx(path: str) -> None: """Create a symlink to the WINE prefix and tracked_files file.""" pfx: Path = Path(path).joinpath("pfx").expanduser() + steam: Path = Path(path).expanduser().joinpath("drive_c/users/steamuser") + user: UnixUser = UnixUser() + wineuser: Path = ( + Path(path).expanduser().joinpath(f"drive_c/users/{user.get_user()}") + ) if pfx.is_symlink(): pfx.unlink() @@ -95,6 +101,40 @@ def setup_pfx(path: str) -> None: Path(path).joinpath("tracked_files").expanduser().touch() + # Create a symlink of the current user to the steamuser dir or vice versa + # Default for a new prefix is: unixuser -> steamuser + if ( + not wineuser.is_dir() + and not steam.is_dir() + and not (wineuser.is_symlink() or steam.is_symlink()) + ): + # For new prefixes with our Proton: user -> steamuser + steam.mkdir(parents=True) + wineuser.unlink(missing_ok=True) + wineuser.symlink_to("steamuser") + elif wineuser.is_dir() and not steam.is_dir() and not steam.is_symlink(): + # When there's a user dir: steamuser -> user + # Be sure it's relative + steam.unlink(missing_ok=True) + steam.symlink_to(user.get_user()) + elif not wineuser.exists() and not wineuser.is_symlink() and steam.is_dir(): + wineuser.unlink(missing_ok=True) + wineuser.symlink_to("steamuser") + else: + paths: List[str] = [steam.as_posix(), wineuser.as_posix()] + log.warning( + msg( + f"Skipping link creation for prefix: {pfx}", + Level.WARNING, + ) + ) + log.warning( + msg( + f"Following paths already exist: {paths}", + Level.WARNING, + ) + ) + def check_env( env: Dict[str, str], toml: Dict[str, Any] = None diff --git a/ulwgl_test.py b/ulwgl_test.py index 930f6ea08..1d0166926 100644 --- a/ulwgl_test.py +++ b/ulwgl_test.py @@ -10,6 +10,7 @@ import ulwgl_plugins import ulwgl_dl_util import tarfile +import ulwgl_util class TestGameLauncher(unittest.TestCase): @@ -845,35 +846,132 @@ def test_setup_pfx_mv(self): Path(self.test_file + "/pfx").is_symlink(), "Expected pfx to be a symlink" ) - # Check if the symlink is in its unexpanded form - self.assertEqual( - Path(self.test_file + "/pfx").readlink().as_posix(), - Path(unexpanded_path).expanduser().as_posix(), + def test_setup_pfx_symlinks_else(self): + """Test setup_pfx in the case both steamuser and unixuser exist in some form. + + Tests the case when they are symlinks + An error should not be raised and we should just do nothing + """ + result = None + pattern = r"^/home/[\w\d]+" + user = ulwgl_util.UnixUser() + unexpanded_path = re.sub( + pattern, + "~", + Path( + Path(self.test_file).cwd().as_posix() + "/" + self.test_file + ).as_posix(), + ) + + # Create only the dir + Path(unexpanded_path).joinpath("drive_c/users").expanduser().mkdir( + parents=True, exist_ok=True ) - old_link = Path(self.test_file + "/pfx").resolve() + # Create the symlink to the test file itself + Path(unexpanded_path).joinpath("drive_c/users").joinpath( + user.get_user() + ).expanduser().symlink_to(Path(self.test_file).absolute()) + Path(unexpanded_path).joinpath("drive_c/users").joinpath( + "steamuser" + ).expanduser().symlink_to(Path(self.test_file).absolute()) + + result = ulwgl_run.setup_pfx(unexpanded_path) - # Rename the dir and replicate passing a new WINEPREFIX - new_dir = Path(unexpanded_path).expanduser().rename("foo") - new_unexpanded_path = re.sub( + self.assertIsNone( + result, + "Expected None when calling setup_pfx", + ) + + def test_setup_pfx_symlinks_unixuser(self): + """Test setup_pfx for symbolic link to steamuser. + + Tests the case when the steamuser dir does not exist and user dir exists + In this case, create: steamuser -> user + """ + result = None + pattern = r"^/home/[\w\d]+" + user = ulwgl_util.UnixUser() + unexpanded_path = re.sub( pattern, "~", - new_dir.cwd().joinpath("foo").as_posix(), + Path( + Path(self.test_file).cwd().as_posix() + "/" + self.test_file + ).as_posix(), ) - ulwgl_run.setup_pfx(new_unexpanded_path) + # Create only the user dir + Path(unexpanded_path).joinpath("drive_c/users").joinpath( + user.get_user() + ).expanduser().mkdir(parents=True, exist_ok=True) + + result = ulwgl_run.setup_pfx(unexpanded_path) + + self.assertIsNone( + result, + "Expected None when creating symbolic link to WINE prefix and tracked_files file", + ) - new_link = Path("foo/pfx").resolve() + # Verify steamuser -> unix user self.assertTrue( - old_link is not new_link, - "Expected the symbolic link to change after moving the WINEPREFIX", + Path(self.test_file).joinpath("drive_c/users/steamuser").is_symlink(), + "Expected steamuser to be a symbolic link", ) + self.assertEqual( + Path(self.test_file).joinpath("drive_c/users/steamuser").readlink(), + Path(user.get_user()), + "Expected steamuser -> user", + ) + + def test_setup_pfx_symlinks_steamuser(self): + """Test setup_pfx for symbolic link to wine. + + Tests the case when only steamuser exist and the user dir does not exist + """ + result = None + user = ulwgl_util.UnixUser() + pattern = r"^/home/[\w\d]+" + unexpanded_path = re.sub( + pattern, + "~", + Path( + Path(self.test_file).cwd().as_posix() + "/" + self.test_file + ).as_posix(), + ) + + # Create the steamuser dir + Path(unexpanded_path + "/drive_c/users/steamuser").expanduser().mkdir( + parents=True, exist_ok=True + ) + + result = ulwgl_run.setup_pfx(unexpanded_path) - if new_link.exists(): - rmtree(new_link.as_posix()) + self.assertIsNone( + result, + "Expected None when creating symbolic link to WINE prefix and tracked_files file", + ) + + # Verify unixuser -> steamuser + self.assertTrue( + Path(self.test_file + "/drive_c/users/steamuser").is_dir(), + "Expected steamuser to be created", + ) + self.assertTrue( + Path(unexpanded_path + "/drive_c/users/" + user.get_user()) + .expanduser() + .is_symlink(), + "Expected symbolic link for unixuser", + ) + self.assertEqual( + Path(self.test_file) + .joinpath(f"drive_c/users/{user.get_user()}") + .readlink(), + Path("steamuser"), + "Expected unixuser -> steamuser", + ) def test_setup_pfx_symlinks(self): - """Test _setup_pfx for valid symlinks. + """Test setup_pfx for valid symlinks. Ensure that symbolic links to the WINE prefix (pfx) are always in expanded form when passed an unexpanded path. For example: @@ -946,6 +1044,7 @@ def test_setup_pfx_paths(self): def test_setup_pfx(self): """Test setup_pfx.""" result = None + user = ulwgl_util.UnixUser() result = ulwgl_run.setup_pfx(self.test_file) self.assertIsNone( result, @@ -958,6 +1057,17 @@ def test_setup_pfx(self): Path(self.test_file + "/tracked_files").is_file(), "Expected tracked_files to be a file", ) + # For new prefixes, steamuser should exist and a user symlink + self.assertTrue( + Path(self.test_file + "/drive_c/users/steamuser").is_dir(), + "Expected steamuser to be created", + ) + self.assertTrue( + Path(self.test_file + "/drive_c/users/" + user.get_user()) + .expanduser() + .is_symlink(), + "Expected symlink of username -> steamuser", + ) def test_parse_args(self): """Test parse_args with no options. diff --git a/ulwgl_util.py b/ulwgl_util.py index b0960598b..54c28255e 100644 --- a/ulwgl_util.py +++ b/ulwgl_util.py @@ -1,5 +1,8 @@ from ulwgl_consts import Color, Level from typing import Any +from os import getuid +from pathlib import Path +from pwd import struct_passwd, getpwuid def msg(msg: Any, level: Level): @@ -18,3 +21,33 @@ def msg(msg: Any, level: Level): log = f"{Color.BOLD.value}{Color.DEBUG.value}{msg}{Color.RESET.value}" return log + + +class UnixUser: + """Represents the User of the system as determined by the password database rather than environment variables or file system paths.""" + + def __init__(self): + """Immutable properties of the user determined by the password database that's derived from the real user id.""" + uid: int = getuid() + entry: struct_passwd = getpwuid(uid) + # Immutable properties, hence no setters + self.name: str = entry.pw_name + self.puid: str = entry.pw_uid # Should be equivalent to the value from getuid + self.dir: str = entry.pw_dir + self.is_user: bool = self.puid == uid + + def get_home_dir(self) -> Path: + """User home directory as determined by the password database that's derived from the current process's real user id.""" + return Path(self.dir).as_posix() + + def get_user(self) -> str: + """User (login name) as determined by the password database that's derived from the current process's real user id.""" + return self.name + + def get_puid(self) -> int: + """Numerical user ID as determined by the password database that's derived from the current process's real user id.""" + return self.puid + + def is_user(self, uid: int) -> bool: + """Compare the UID passed in to this instance.""" + return uid == self.puid