From 7c18e1bdc9bf888fd9b74cf03fd9ac9db7550530 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:39:27 +0000 Subject: [PATCH 1/3] chore(deps): bump actions/setup-python from 4 to 5 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/unit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index f78d2ea..232f6cf 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -14,7 +14,7 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install dependencies From c8c480d91d77fbe46a91fee8bb665204e2acd218 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:39:30 +0000 Subject: [PATCH 2/3] chore(deps): bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/unit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index f78d2ea..809f25a 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: From e13e6ce36601f79bcad3ab3259b549197f5ef97a Mon Sep 17 00:00:00 2001 From: gerblesh <101901964+gerblesh@users.noreply.github.com> Date: Wed, 4 Sep 2024 21:11:37 -0700 Subject: [PATCH 3/3] feat: use systemd-run, fix topgrade (#130) --- .gitignore | 166 +++++++++++++++++++++++++++++++++++- src/ublue_update/cli.py | 40 ++++----- src/ublue_update/session.py | 31 +------ tests/unit/test_cli.py | 11 +-- tests/unit/test_session.py | 115 +++---------------------- 5 files changed, 199 insertions(+), 164 deletions(-) diff --git a/.gitignore b/.gitignore index e146e19..aab7aa9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,165 @@ -/output -/dist -*.egg-info *.**.pyc *.pyc + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/src/ublue_update/cli.py b/src/ublue_update/cli.py index c754fec..e692146 100644 --- a/src/ublue_update/cli.py +++ b/src/ublue_update/cli.py @@ -11,7 +11,7 @@ from ublue_update.update_inhibitors.hardware import check_hardware_inhibitors from ublue_update.update_inhibitors.custom import check_custom_inhibitors from ublue_update.config import cfg -from ublue_update.session import get_xdg_runtime_dir, get_active_sessions +from ublue_update.session import get_active_sessions from ublue_update.filelock import acquire_lock, release_lock @@ -37,17 +37,12 @@ def notify(title: str, body: str, actions: list = [], urgency: str = "normal"): except KeyError as e: log.error("failed to get active logind session info", e) for user in users: - try: - xdg_runtime_dir = get_xdg_runtime_dir(user["User"]) - except KeyError as e: - log.error(f"failed to get xdg_runtime_dir for user: {user['Name']}", e) - return user_args = [ - "/usr/bin/sudo", - "-u", - f"{user['Name']}", - "DISPLAY=:0", - f"DBUS_SESSION_BUS_ADDRESS=unix:path={xdg_runtime_dir}/bus", + "/usr/bin/systemd-run", + "--user", + "--machine", + f"{user['user']}@", + "--wait", ] user_args += args out = subprocess.run(user_args, capture_output=True) @@ -119,6 +114,8 @@ def run_updates(system, system_update_available): users = [] """System""" + # remove backwards compat warnings in topgrade (requires user confirmation without this env var) + os.environ["TOPGRADE_SKIP_BRKC_NOTIFY"] = "true" out = subprocess.run( [ "/usr/bin/topgrade", @@ -136,21 +133,15 @@ def run_updates(system, system_update_available): """Users""" for user in users: - try: - xdg_runtime_dir = get_xdg_runtime_dir(user["User"]) - except KeyError as e: - log.error(f"failed to get xdg_runtime_dir for user: {user['Name']}", e) - break - log.info(f"""Running update for user: '{user['Name']}'""") - + log.info(f"""Running update for user: '{user['user']}'""") out = subprocess.run( [ - "/usr/bin/sudo", - "-u", - f"{user['Name']}", - "DISPLAY=:0", - f"XDG_RUNTIME_DIR={xdg_runtime_dir}", - f"DBUS_SESSION_BUS_ADDRESS=unix:path={xdg_runtime_dir}/bus", + "/usr/bin/systemd-run", + "--setenv=TOPGRADE_SKIP_BRKC_NOTIFY=true", + "--user", + "--machine", + f"{user['user']}@", + "--wait", "/usr/bin/topgrade", "--config", "/usr/share/ublue-update/topgrade-user.toml", @@ -186,7 +177,6 @@ def run_updates(system, system_update_available): def main(): - # setup argparse parser = argparse.ArgumentParser() parser.add_argument( diff --git a/src/ublue_update/session.py b/src/ublue_update/session.py index f02001a..2371452 100644 --- a/src/ublue_update/session.py +++ b/src/ublue_update/session.py @@ -2,41 +2,14 @@ import json -def get_xdg_runtime_dir(uid): - out = subprocess.run( - ["/usr/bin/loginctl", "show-user", f"{uid}"], - capture_output=True, - ) - loginctl_output = { - line.split("=")[0]: line.split("=")[-1] - for line in out.stdout.decode("utf-8").splitlines() - } - return loginctl_output["RuntimePath"] - - def get_active_sessions(): out = subprocess.run( ["/usr/bin/loginctl", "list-sessions", "--output=json"], capture_output=True, ) sessions = json.loads(out.stdout.decode("utf-8")) - session_properties = [] active_sessions = [] for session in sessions: - args = [ - "/usr/bin/loginctl", - "show-session", - f"{session['session']}", - ] - out = subprocess.run(args, capture_output=True) - if out.returncode == 0: - loginctl_output = { - line.split("=")[0]: line.split("=")[-1] - for line in out.stdout.decode("utf-8").splitlines() - } - session_properties.append(loginctl_output) - for session_info in session_properties: - graphical = session_info["Type"] == "x11" or session_info["Type"] == "wayland" - if graphical and session_info["Active"] == "yes": - active_sessions.append(session_info) + if session.get("state") == "active": + active_sessions.append(session) return active_sessions diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 1b20d8a..913644d 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -1,7 +1,7 @@ import pytest import sys import os -from unittest.mock import patch, MagicMock, mock_open +from unittest.mock import patch, MagicMock # Add the src directory to the sys.path sys.path.insert( @@ -22,7 +22,7 @@ @patch("ublue_update.cli.subprocess.run") def test_notify_no_dbus_notify(mock_run, mock_log, mock_os, mock_cfg): mock_cfg.dbus_notify = False - assert notify("test_title", "test_body") == None + assert notify("test_title", "test_body") is None @patch("ublue_update.cli.cfg") @@ -42,7 +42,7 @@ def test_notify_uid_user(mock_run, mock_log, mock_os, mock_cfg): body, "--app-name=Universal Blue Updater", "--icon=software-update-available-symbolic", - f"--urgency=normal", + "--urgency=normal", ], capture_output=True, ) @@ -51,7 +51,7 @@ def test_notify_uid_user(mock_run, mock_log, mock_os, mock_cfg): @patch("ublue_update.cli.cfg") def test_ask_for_updates_no_dbus_notify(mock_cfg): mock_cfg.dbus_notify = False - assert ask_for_updates(True) == None + assert ask_for_updates(True) is None @patch("ublue_update.cli.cfg") @@ -59,7 +59,7 @@ def test_ask_for_updates_no_dbus_notify(mock_cfg): def test_ask_for_updates_notify_none(mock_notify, mock_cfg): mock_cfg.dbus_notify = True mock_notify.return_value = None - assert ask_for_updates(True) == None + assert ask_for_updates(True) is None mock_notify.assert_called_once_with( "System Updater", "Update available, but system checks failed. Update now?", @@ -210,6 +210,7 @@ def test_run_updates_system( ["universal-blue-update-reboot=Reboot Now"], ) + @patch("ublue_update.cli.os") @patch("ublue_update.cli.get_active_sessions") @patch("ublue_update.cli.acquire_lock") diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py index 156def5..551e42d 100644 --- a/tests/unit/test_session.py +++ b/tests/unit/test_session.py @@ -7,25 +7,7 @@ 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../src")) ) -from ublue_update.session import get_active_sessions, get_xdg_runtime_dir - -loginctl_output = b""" -UID=1001 -GID=1001 -Name=test -Timestamp=Thu 2024-08-15 18:18:08 UTC -TimestampMonotonic=293807858 -RuntimePath=/run/user/1001 -Service=user@1001.service -Slice=user-1001.slice -Display=c3 -State=active -Sessions=c4 c3 c1 3 -IdleHint=no -IdleSinceHint=0 -IdleSinceHintMonotonic=0 -Linger=yes -""" +from ublue_update.session import get_active_sessions loginctl_json_output = b""" [ @@ -37,6 +19,7 @@ "leader" : 6205, "class" : "manager", "tty" : null, + "state": "active", "idle" : false, "since" : null }, @@ -48,105 +31,33 @@ "leader" : 6230, "class" : "manager", "tty" : null, + "state": "inactive", "idle" : false, "since" : null } ] """ -session_info = [ - b""" -Id=3 -User=1001 -Name=test -Timestamp=Thu 2024-08-15 18:18:08 UTC -TimestampMonotonic=293993628 -VTNr=0 -Remote=no -Service=systemd-user -Leader=6205 -Audit=3 -Type=wayland -Class=manager -Active=yes -State=active -IdleHint=no -IdleSinceHint=0 -IdleSinceHintMonotonic=0 -LockedHint=no -""", - b""" -Id=c1 -User=1001 -Name=test -Timestamp=Thu 2024-08-15 18:18:09 UTC -TimestampMonotonic=295100128 -VTNr=0 -Remote=no -Service=systemd-user -Leader=6230 -Audit=3 -Type=unspecified -Class=manager -Active=yes -State=active -IdleHint=no -IdleSinceHint=0 -IdleSinceHintMonotonic=0 -LockedHint=no -""", -] - - -@patch("ublue_update.session.subprocess.run") -def test_get_xdg_runtime_dir(mock_run): - mock_run.return_value = MagicMock(stdout=loginctl_output) - assert get_xdg_runtime_dir(1001) == "/run/user/1001" - mock_run.assert_called_once_with( - ["/usr/bin/loginctl", "show-user", "1001"], capture_output=True - ) - @patch("ublue_update.session.subprocess.run") def test_get_active_sessions(mock_run): - mock_session1 = MagicMock(stdout=session_info[0]) - mock_session2 = MagicMock(stdout=session_info[1]) - mock_session1.returncode = 0 - mock_session2.returncode = 0 mock_run.side_effect = [ MagicMock(stdout=loginctl_json_output), - mock_session1, - mock_session2, ] assert get_active_sessions() == [ { - "": "", - "Id": "3", - "User": "1001", - "Name": "test", - "Timestamp": "Thu 2024-08-15 18:18:08 UTC", - "TimestampMonotonic": "293993628", - "VTNr": "0", - "Remote": "no", - "Service": "systemd-user", - "Leader": "6205", - "Audit": "3", - "Type": "wayland", - "Class": "manager", - "Active": "yes", - "State": "active", - "IdleHint": "no", - "IdleSinceHint": "0", - "IdleSinceHintMonotonic": "0", - "LockedHint": "no", + "session": "3", + "uid": 1001, + "user": "test", + "seat": None, + "leader": 6205, + "class": "manager", + "tty": None, + "state": "active", + "idle": False, + "since": None, } ] mock_run.assert_any_call( ["/usr/bin/loginctl", "list-sessions", "--output=json"], capture_output=True ) - mock_run.assert_any_call( - ["/usr/bin/loginctl", "show-session", "3"], capture_output=True - ) - mock_run.assert_any_call( - ["/usr/bin/loginctl", "show-session", "c1"], capture_output=True - )