Skip to content
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

Support symlink of current unix username to steamuser #21

Merged
merged 3 commits into from
Feb 26, 2024
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
40 changes: 40 additions & 0 deletions ulwgl_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down
142 changes: 126 additions & 16 deletions ulwgl_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import ulwgl_plugins
import ulwgl_dl_util
import tarfile
import ulwgl_util


class TestGameLauncher(unittest.TestCase):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand Down
33 changes: 33 additions & 0 deletions ulwgl_util.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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