From 23a2af05c3b73564f7196f2c72d85ab39aae2a08 Mon Sep 17 00:00:00 2001 From: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:28:03 -0700 Subject: [PATCH 1/5] fix: preserve existing mimeapps.list entries when installing web url handler The Linux install path opened mimeapps.list in "w" mode, truncating the entire file and destroying every unrelated default-application association (browser, PDF, mailto, etc.) -- permanent data loss. Now the existing mimeapps.list is read/parsed as an INI file, only the deadline x-scheme-handler entry in [Default Applications] is added/updated, and the file is written back preserving all other entries. Handles the case where the file does not exist yet. Signed-off-by: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> --- src/deadline/client/cli/_deadline_web_url.py | 27 +++++- .../cli/test_cli_handle_web_url.py | 89 +++++++++++++++++++ 2 files changed, 112 insertions(+), 4 deletions(-) diff --git a/src/deadline/client/cli/_deadline_web_url.py b/src/deadline/client/cli/_deadline_web_url.py index 919b94803..41d03a922 100644 --- a/src/deadline/client/cli/_deadline_web_url.py +++ b/src/deadline/client/cli/_deadline_web_url.py @@ -1,5 +1,6 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +import configparser import os import re import sys @@ -215,13 +216,31 @@ def install_deadline_web_url_handler(all_users: bool) -> None: MimeType=x-scheme-handler/{DEADLINE_URL_SCHEME_NAME} """ - mimeapps_file_content = f"""[Default Applications] -x-scheme-handler/{DEADLINE_URL_SCHEME_NAME}={DEADLINE_URL_SCHEME_NAME}.desktop; -""" with open(desktop_file_path, "w") as desktop_file: desktop_file.write(desktop_file_content) + + # Read/parse any existing mimeapps.list and add/update ONLY the deadline + # handler entry, preserving all other default-application associations. + # Opening in "w" mode would truncate the file and destroy unrelated + # associations (browser, PDF, mailto, etc.), causing permanent data loss. + # interpolation=None avoids treating "%" in values specially, and + # optionxform=str preserves the case of mime-type/scheme keys. + mimeapps = configparser.ConfigParser(interpolation=None) + mimeapps.optionxform = str # type: ignore[assignment,method-assign] + + if os.path.isfile(mimeapps_list_file_path): + mimeapps.read(mimeapps_list_file_path) + + if not mimeapps.has_section("Default Applications"): + mimeapps.add_section("Default Applications") + mimeapps.set( + "Default Applications", + f"x-scheme-handler/{DEADLINE_URL_SCHEME_NAME}", + f"{DEADLINE_URL_SCHEME_NAME}.desktop;", + ) + with open(mimeapps_list_file_path, "w") as mimeapps_list_file: - mimeapps_list_file.write(mimeapps_file_content) + mimeapps.write(mimeapps_list_file, space_around_delimiters=False) try: subprocess.run(["update-desktop-database", entry_dir], check=True) diff --git a/test/unit/deadline_client/cli/test_cli_handle_web_url.py b/test/unit/deadline_client/cli/test_cli_handle_web_url.py index 7902cca35..1b881a977 100644 --- a/test/unit/deadline_client/cli/test_cli_handle_web_url.py +++ b/test/unit/deadline_client/cli/test_cli_handle_web_url.py @@ -1006,6 +1006,95 @@ def test_linux_install_generates_valid_desktop_file(fresh_deadline_config, tmp_p ) +def test_linux_install_preserves_existing_mimeapps_entries(fresh_deadline_config, tmp_path): + """ + Regression test: installing the web URL handler on Linux must NOT wipe out + unrelated default-application associations already present in mimeapps.list. + + Previously the install opened mimeapps.list in "w" mode, truncating the whole + file and destroying every other association (browser, PDF, mailto, etc.). + """ + entry_dir = tmp_path / "applications" + entry_dir.mkdir() + config_dir = tmp_path / "config" + config_dir.mkdir() + + mimeapps_path = config_dir / "mimeapps.list" + # Pre-populate with unrelated default-application associations. + mimeapps_path.write_text( + "[Default Applications]\n" + "text/html=firefox.desktop\n" + "x-scheme-handler/http=firefox.desktop\n" + "x-scheme-handler/mailto=thunderbird.desktop\n" + "application/pdf=okular.desktop\n" + ) + + with ( + patch.object(sys, "platform", "linux"), + patch.object(sys, "argv", ["/usr/bin/deadline"]), + patch.object(shutil, "which", return_value="/usr/bin/deadline"), + patch.object( + os.path, + "expanduser", + side_effect=lambda p: p.replace("~/.local/share", str(tmp_path)).replace( + "~/.config", str(config_dir) + ), + ), + patch.object(subprocess, "run"), + patch.object(os, "makedirs"), + ): + from deadline.client.cli._deadline_web_url import install_deadline_web_url_handler + + install_deadline_web_url_handler(all_users=False) + + contents = mimeapps_path.read_text() + + # The pre-existing associations must survive. + assert "text/html=firefox.desktop" in contents + assert "x-scheme-handler/http=firefox.desktop" in contents + assert "x-scheme-handler/mailto=thunderbird.desktop" in contents + assert "application/pdf=okular.desktop" in contents + + # And the deadline handler must be added. + assert "x-scheme-handler/deadline=deadline.desktop" in contents + + +def test_linux_install_creates_mimeapps_when_missing(fresh_deadline_config, tmp_path): + """ + Tests that when mimeapps.list does not exist yet, installing creates it + with the deadline handler entry. + """ + entry_dir = tmp_path / "applications" + entry_dir.mkdir() + config_dir = tmp_path / "config" + config_dir.mkdir() + + mimeapps_path = config_dir / "mimeapps.list" + assert not mimeapps_path.exists() + + with ( + patch.object(sys, "platform", "linux"), + patch.object(sys, "argv", ["/usr/bin/deadline"]), + patch.object(shutil, "which", return_value="/usr/bin/deadline"), + patch.object( + os.path, + "expanduser", + side_effect=lambda p: p.replace("~/.local/share", str(tmp_path)).replace( + "~/.config", str(config_dir) + ), + ), + patch.object(subprocess, "run"), + patch.object(os, "makedirs"), + ): + from deadline.client.cli._deadline_web_url import install_deadline_web_url_handler + + install_deadline_web_url_handler(all_users=False) + + contents = mimeapps_path.read_text() + assert "[Default Applications]" in contents + assert "x-scheme-handler/deadline=deadline.desktop" in contents + + def test_linux_install_resolves_bare_command_via_shutil_which(fresh_deadline_config, tmp_path): """ Tests that on Linux, when sys.argv[0] is a bare command name (e.g. 'deadline'), From 0dc7a86574e5e9be988c4c0beb1dedbace46dc85 Mon Sep 17 00:00:00 2001 From: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:56:01 -0700 Subject: [PATCH 2/5] fix: surface malformed mimeapps.list as DeadlineOperationError A pre-existing mimeapps.list that is not strictly valid INI could make configparser.read() raise (MissingSectionHeaderError, DuplicateOptionError, UnicodeDecodeError, etc.), crashing the CLI with a raw traceback. Wrap the read in try/except and raise DeadlineOperationError for consistency. Signed-off-by: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> --- src/deadline/client/cli/_deadline_web_url.py | 13 ++++++- .../cli/test_cli_handle_web_url.py | 36 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/deadline/client/cli/_deadline_web_url.py b/src/deadline/client/cli/_deadline_web_url.py index 41d03a922..12bdf2963 100644 --- a/src/deadline/client/cli/_deadline_web_url.py +++ b/src/deadline/client/cli/_deadline_web_url.py @@ -229,7 +229,18 @@ def install_deadline_web_url_handler(all_users: bool) -> None: mimeapps.optionxform = str # type: ignore[assignment,method-assign] if os.path.isfile(mimeapps_list_file_path): - mimeapps.read(mimeapps_list_file_path) + # A pre-existing mimeapps.list that is not strictly valid INI can make + # configparser raise (MissingSectionHeaderError, DuplicateOptionError, + # DuplicateSectionError, UnicodeDecodeError, ...). Surface these as a + # DeadlineOperationError for consistency with the rest of this function + # rather than crashing the CLI with a raw traceback. + try: + mimeapps.read(mimeapps_list_file_path) + except (configparser.Error, UnicodeDecodeError) as e: + raise DeadlineOperationError( + f"Failed to install the handler for {DEADLINE_URL_SCHEME_NAME} URLs: " + f"could not parse existing {mimeapps_list_file_path}:\n{e}" + ) from e if not mimeapps.has_section("Default Applications"): mimeapps.add_section("Default Applications") diff --git a/test/unit/deadline_client/cli/test_cli_handle_web_url.py b/test/unit/deadline_client/cli/test_cli_handle_web_url.py index 1b881a977..e88e49088 100644 --- a/test/unit/deadline_client/cli/test_cli_handle_web_url.py +++ b/test/unit/deadline_client/cli/test_cli_handle_web_url.py @@ -1095,6 +1095,42 @@ def test_linux_install_creates_mimeapps_when_missing(fresh_deadline_config, tmp_ assert "x-scheme-handler/deadline=deadline.desktop" in contents +def test_linux_install_raises_on_malformed_mimeapps(fresh_deadline_config, tmp_path): + """ + A pre-existing mimeapps.list that is not valid INI (here, a stray key before + any section header) must surface as a DeadlineOperationError rather than + crashing the CLI with a raw configparser traceback. + """ + entry_dir = tmp_path / "applications" + entry_dir.mkdir() + config_dir = tmp_path / "config" + config_dir.mkdir() + + mimeapps_path = config_dir / "mimeapps.list" + # Not valid INI: a key/value pair before any [section] header. + mimeapps_path.write_text("this-is=not-valid-ini\n") + + with ( + patch.object(sys, "platform", "linux"), + patch.object(sys, "argv", ["/usr/bin/deadline"]), + patch.object(shutil, "which", return_value="/usr/bin/deadline"), + patch.object( + os.path, + "expanduser", + side_effect=lambda p: p.replace("~/.local/share", str(tmp_path)).replace( + "~/.config", str(config_dir) + ), + ), + patch.object(subprocess, "run"), + patch.object(os, "makedirs"), + ): + from deadline.client.cli._deadline_web_url import install_deadline_web_url_handler + from deadline.client.exceptions import DeadlineOperationError + + with pytest.raises(DeadlineOperationError, match="could not parse existing"): + install_deadline_web_url_handler(all_users=False) + + def test_linux_install_resolves_bare_command_via_shutil_which(fresh_deadline_config, tmp_path): """ Tests that on Linux, when sys.argv[0] is a bare command name (e.g. 'deadline'), From dbade125f317f5fe18475d9ff27eb4502a3e11f2 Mon Sep 17 00:00:00 2001 From: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> Date: Wed, 22 Jul 2026 10:17:29 -0700 Subject: [PATCH 3/5] fix: tolerate duplicate keys/sections in existing mimeapps.list Real-world mimeapps.list files written by other desktop tools have historically contained duplicate keys and sections. The default configparser strict=True raised DuplicateOptionError/DuplicateSectionError on these, turning a previously always-succeeding install into a hard DeadlineOperationError until the user hand-edited their file. Pass strict=False (last-value-wins) so these files parse while unrelated associations are still preserved. Genuinely malformed files (no section header, bad encoding) still surface as DeadlineOperationError via the surrounding try/except. Signed-off-by: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> --- src/deadline/client/cli/_deadline_web_url.py | 17 ++++-- .../cli/test_cli_handle_web_url.py | 58 +++++++++++++++++++ 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/src/deadline/client/cli/_deadline_web_url.py b/src/deadline/client/cli/_deadline_web_url.py index 12bdf2963..708dca5c8 100644 --- a/src/deadline/client/cli/_deadline_web_url.py +++ b/src/deadline/client/cli/_deadline_web_url.py @@ -225,15 +225,20 @@ def install_deadline_web_url_handler(all_users: bool) -> None: # associations (browser, PDF, mailto, etc.), causing permanent data loss. # interpolation=None avoids treating "%" in values specially, and # optionxform=str preserves the case of mime-type/scheme keys. - mimeapps = configparser.ConfigParser(interpolation=None) + # strict=False tolerates duplicate keys/sections (last value wins), + # which real-world mimeapps.list files written by other desktop tools + # have historically contained; the default strict=True would turn an + # otherwise-usable file into a hard install failure. + mimeapps = configparser.ConfigParser(interpolation=None, strict=False) mimeapps.optionxform = str # type: ignore[assignment,method-assign] if os.path.isfile(mimeapps_list_file_path): - # A pre-existing mimeapps.list that is not strictly valid INI can make - # configparser raise (MissingSectionHeaderError, DuplicateOptionError, - # DuplicateSectionError, UnicodeDecodeError, ...). Surface these as a - # DeadlineOperationError for consistency with the rest of this function - # rather than crashing the CLI with a raw traceback. + # A pre-existing mimeapps.list that is not valid INI can still make + # configparser raise even with strict=False (MissingSectionHeaderError + # for a key before any section header, UnicodeDecodeError for a + # non-text file, ...). Surface these as a DeadlineOperationError for + # consistency with the rest of this function rather than crashing the + # CLI with a raw traceback. try: mimeapps.read(mimeapps_list_file_path) except (configparser.Error, UnicodeDecodeError) as e: diff --git a/test/unit/deadline_client/cli/test_cli_handle_web_url.py b/test/unit/deadline_client/cli/test_cli_handle_web_url.py index e88e49088..bb43d87d4 100644 --- a/test/unit/deadline_client/cli/test_cli_handle_web_url.py +++ b/test/unit/deadline_client/cli/test_cli_handle_web_url.py @@ -1131,6 +1131,64 @@ def test_linux_install_raises_on_malformed_mimeapps(fresh_deadline_config, tmp_p install_deadline_web_url_handler(all_users=False) +def test_linux_install_tolerates_duplicate_mimeapps_entries(fresh_deadline_config, tmp_path): + """ + Regression test: real-world mimeapps.list files written by other desktop + tools have historically contained duplicate keys/sections. The default + configparser strict=True would raise DuplicateOptionError/DuplicateSectionError + on these, turning a previously always-succeeding install into a hard failure. + + With strict=False the install must succeed (last value wins) while still + preserving unrelated associations and adding the deadline handler. + """ + entry_dir = tmp_path / "applications" + entry_dir.mkdir() + config_dir = tmp_path / "config" + config_dir.mkdir() + + mimeapps_path = config_dir / "mimeapps.list" + # Duplicate key (x-scheme-handler/http appears twice) AND a duplicate section + # ([Default Applications] appears twice) — both illegal under strict=True. + mimeapps_path.write_text( + "[Default Applications]\n" + "x-scheme-handler/http=firefox.desktop\n" + "x-scheme-handler/http=chrome.desktop\n" + "application/pdf=okular.desktop\n" + "[Default Applications]\n" + "x-scheme-handler/mailto=thunderbird.desktop\n" + ) + + with ( + patch.object(sys, "platform", "linux"), + patch.object(sys, "argv", ["/usr/bin/deadline"]), + patch.object(shutil, "which", return_value="/usr/bin/deadline"), + patch.object( + os.path, + "expanduser", + side_effect=lambda p: p.replace("~/.local/share", str(tmp_path)).replace( + "~/.config", str(config_dir) + ), + ), + patch.object(subprocess, "run"), + patch.object(os, "makedirs"), + ): + from deadline.client.cli._deadline_web_url import install_deadline_web_url_handler + + # Must NOT raise despite the duplicate key/section. + install_deadline_web_url_handler(all_users=False) + + contents = mimeapps_path.read_text() + + # Unrelated associations survive; duplicate key resolves last-value-wins. + assert "x-scheme-handler/http=chrome.desktop" in contents + assert "x-scheme-handler/http=firefox.desktop" not in contents + assert "application/pdf=okular.desktop" in contents + assert "x-scheme-handler/mailto=thunderbird.desktop" in contents + + # And the deadline handler is added. + assert "x-scheme-handler/deadline=deadline.desktop" in contents + + def test_linux_install_resolves_bare_command_via_shutil_which(fresh_deadline_config, tmp_path): """ Tests that on Linux, when sys.argv[0] is a bare command name (e.g. 'deadline'), From 58febaae0bd2744621bb6d05aa4dd6915262ecf5 Mon Sep 17 00:00:00 2001 From: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> Date: Wed, 22 Jul 2026 11:21:07 -0700 Subject: [PATCH 4/5] fix: make mimeapps.list rewrite atomic Opening the destination in "w" mode truncated the existing mimeapps.list before configparser.write() ran, so a crash/kill/disk-full or an exception inside write() would leave the file empty or partially written -- losing exactly the unrelated default-application associations this change exists to protect. Render to a temp file in the same directory and os.replace() it into place so the original is only replaced once the new content is fully and successfully written; the temp file is cleaned up on failure. Signed-off-by: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> --- src/deadline/client/cli/_deadline_web_url.py | 22 +++++++- .../cli/test_cli_handle_web_url.py | 51 +++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/deadline/client/cli/_deadline_web_url.py b/src/deadline/client/cli/_deadline_web_url.py index 708dca5c8..2c7b2d431 100644 --- a/src/deadline/client/cli/_deadline_web_url.py +++ b/src/deadline/client/cli/_deadline_web_url.py @@ -255,8 +255,26 @@ def install_deadline_web_url_handler(all_users: bool) -> None: f"{DEADLINE_URL_SCHEME_NAME}.desktop;", ) - with open(mimeapps_list_file_path, "w") as mimeapps_list_file: - mimeapps.write(mimeapps_list_file, space_around_delimiters=False) + # Write atomically: rendering to a temp file in the same directory and + # os.replace()-ing it into place means the original mimeapps.list is only + # replaced once the new content is fully and successfully written. Opening + # the destination directly in "w" mode would truncate it before write() + # runs, so a crash/kill/disk-full mid-write would leave it empty or partial + # -- losing exactly the unrelated associations this change protects. + import tempfile + + dir_name = os.path.dirname(mimeapps_list_file_path) + fd, tmp_path = tempfile.mkstemp(dir=dir_name, prefix=".mimeapps.list.", suffix=".tmp") + try: + with os.fdopen(fd, "w") as tmp_file: + mimeapps.write(tmp_file, space_around_delimiters=False) + os.replace(tmp_path, mimeapps_list_file_path) + except BaseException: + try: + os.unlink(tmp_path) + except OSError: + pass + raise try: subprocess.run(["update-desktop-database", entry_dir], check=True) diff --git a/test/unit/deadline_client/cli/test_cli_handle_web_url.py b/test/unit/deadline_client/cli/test_cli_handle_web_url.py index bb43d87d4..0d3f19aef 100644 --- a/test/unit/deadline_client/cli/test_cli_handle_web_url.py +++ b/test/unit/deadline_client/cli/test_cli_handle_web_url.py @@ -1059,6 +1059,57 @@ def test_linux_install_preserves_existing_mimeapps_entries(fresh_deadline_config assert "x-scheme-handler/deadline=deadline.desktop" in contents +def test_linux_install_preserves_mimeapps_when_write_fails(fresh_deadline_config, tmp_path): + """ + Regression test: the mimeapps.list rewrite must be atomic. If writing the new + content fails partway through (crash, disk-full, exception inside write()), + the original file must be left intact rather than truncated/emptied -- losing + the unrelated associations this change exists to protect. + """ + entry_dir = tmp_path / "applications" + entry_dir.mkdir() + config_dir = tmp_path / "config" + config_dir.mkdir() + + mimeapps_path = config_dir / "mimeapps.list" + original = ( + "[Default Applications]\n" + "text/html=firefox.desktop\n" + "x-scheme-handler/http=firefox.desktop\n" + "application/pdf=okular.desktop\n" + ) + mimeapps_path.write_text(original) + + with ( + patch.object(sys, "platform", "linux"), + patch.object(sys, "argv", ["/usr/bin/deadline"]), + patch.object(shutil, "which", return_value="/usr/bin/deadline"), + patch.object( + os.path, + "expanduser", + side_effect=lambda p: p.replace("~/.local/share", str(tmp_path)).replace( + "~/.config", str(config_dir) + ), + ), + patch.object(subprocess, "run"), + patch.object(os, "makedirs"), + # Simulate a failure while rendering the new content to the temp file. + patch( + "configparser.ConfigParser.write", + side_effect=OSError("No space left on device"), + ), + ): + from deadline.client.cli._deadline_web_url import install_deadline_web_url_handler + + with pytest.raises(OSError, match="No space left on device"): + install_deadline_web_url_handler(all_users=False) + + # The original file must be untouched, and no temp files left behind. + assert mimeapps_path.read_text() == original + leftover = [p.name for p in config_dir.iterdir() if p.name != "mimeapps.list"] + assert leftover == [], f"temp files not cleaned up: {leftover}" + + def test_linux_install_creates_mimeapps_when_missing(fresh_deadline_config, tmp_path): """ Tests that when mimeapps.list does not exist yet, installing creates it From 08ec80f46b70d55985ee67216e4b4062dab591e1 Mon Sep 17 00:00:00 2001 From: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:54:37 -0700 Subject: [PATCH 5/5] fix: preserve mimeapps.list permissions in atomic rewrite The atomic temp-file rewrite used tempfile.mkstemp(), which creates the temp file with mode 0600, and os.replace() carried that mode onto the final mimeapps.list. That silently dropped the permissions of a pre-existing file and, for the all_users case (/usr/share/applications/mimeapps.list written as root), produced a root-only file that other users' desktop environments could not read. Preserve the existing file's mode, or default to 0644 for a new file, by chmod-ing the temp file before os.replace(). Also add an explanatory comment to the best-effort temp-file cleanup except clause flagged by CodeQL. Signed-off-by: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> --- src/deadline/client/cli/_deadline_web_url.py | 16 ++++ .../cli/test_cli_handle_web_url.py | 81 +++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/src/deadline/client/cli/_deadline_web_url.py b/src/deadline/client/cli/_deadline_web_url.py index 2c7b2d431..811334231 100644 --- a/src/deadline/client/cli/_deadline_web_url.py +++ b/src/deadline/client/cli/_deadline_web_url.py @@ -261,18 +261,34 @@ def install_deadline_web_url_handler(all_users: bool) -> None: # the destination directly in "w" mode would truncate it before write() # runs, so a crash/kill/disk-full mid-write would leave it empty or partial # -- losing exactly the unrelated associations this change protects. + import stat import tempfile + # tempfile.mkstemp() creates the temp file with mode 0600, and os.replace() + # keeps that mode on the final file. That would silently drop the + # permissions of a pre-existing mimeapps.list, and for the all_users case + # (/usr/share/applications/mimeapps.list, written as root) would leave a + # system-wide file that other users' desktop environments cannot read. + # Preserve the existing file's mode, or default to 0644 for a new file. + if os.path.isfile(mimeapps_list_file_path): + mimeapps_list_file_mode = stat.S_IMODE(os.stat(mimeapps_list_file_path).st_mode) + else: + mimeapps_list_file_mode = 0o644 + dir_name = os.path.dirname(mimeapps_list_file_path) fd, tmp_path = tempfile.mkstemp(dir=dir_name, prefix=".mimeapps.list.", suffix=".tmp") try: with os.fdopen(fd, "w") as tmp_file: mimeapps.write(tmp_file, space_around_delimiters=False) + os.chmod(tmp_path, mimeapps_list_file_mode) os.replace(tmp_path, mimeapps_list_file_path) except BaseException: try: os.unlink(tmp_path) except OSError: + # Best-effort temp file cleanup; the original exception below is + # the actual failure to surface, and a leftover temp file is + # harmless compared to masking it. pass raise diff --git a/test/unit/deadline_client/cli/test_cli_handle_web_url.py b/test/unit/deadline_client/cli/test_cli_handle_web_url.py index 0d3f19aef..a9d186836 100644 --- a/test/unit/deadline_client/cli/test_cli_handle_web_url.py +++ b/test/unit/deadline_client/cli/test_cli_handle_web_url.py @@ -6,6 +6,7 @@ import os import shutil +import stat import subprocess import sys from pathlib import Path @@ -1110,6 +1111,86 @@ def test_linux_install_preserves_mimeapps_when_write_fails(fresh_deadline_config assert leftover == [], f"temp files not cleaned up: {leftover}" +@pytest.mark.skipif( + sys.platform == "win32", reason="POSIX file modes are not meaningful on Windows" +) +def test_linux_install_preserves_mimeapps_permissions(fresh_deadline_config, tmp_path): + """ + Regression test: the atomic temp-file rewrite must not change the permissions + of an existing mimeapps.list. tempfile.mkstemp() creates the temp file with + mode 0600 and os.replace() would keep that mode -- e.g. turning a + world-readable system-wide /usr/share/applications/mimeapps.list into a + root-only file that other users' desktop environments cannot read. + """ + entry_dir = tmp_path / "applications" + entry_dir.mkdir() + config_dir = tmp_path / "config" + config_dir.mkdir() + + mimeapps_path = config_dir / "mimeapps.list" + mimeapps_path.write_text("[Default Applications]\ntext/html=firefox.desktop\n") + mimeapps_path.chmod(0o644) + + with ( + patch.object(sys, "platform", "linux"), + patch.object(sys, "argv", ["/usr/bin/deadline"]), + patch.object(shutil, "which", return_value="/usr/bin/deadline"), + patch.object( + os.path, + "expanduser", + side_effect=lambda p: p.replace("~/.local/share", str(tmp_path)).replace( + "~/.config", str(config_dir) + ), + ), + patch.object(subprocess, "run"), + patch.object(os, "makedirs"), + ): + from deadline.client.cli._deadline_web_url import install_deadline_web_url_handler + + install_deadline_web_url_handler(all_users=False) + + assert stat.S_IMODE(mimeapps_path.stat().st_mode) == 0o644 + # Sanity check that the rewrite happened. + assert "x-scheme-handler/deadline=deadline.desktop" in mimeapps_path.read_text() + + +@pytest.mark.skipif( + sys.platform == "win32", reason="POSIX file modes are not meaningful on Windows" +) +def test_linux_install_creates_mimeapps_with_default_permissions(fresh_deadline_config, tmp_path): + """ + When mimeapps.list does not exist yet, the new file must get a standard + world-readable config file mode (0644), not mkstemp's private 0600. + """ + entry_dir = tmp_path / "applications" + entry_dir.mkdir() + config_dir = tmp_path / "config" + config_dir.mkdir() + + mimeapps_path = config_dir / "mimeapps.list" + assert not mimeapps_path.exists() + + with ( + patch.object(sys, "platform", "linux"), + patch.object(sys, "argv", ["/usr/bin/deadline"]), + patch.object(shutil, "which", return_value="/usr/bin/deadline"), + patch.object( + os.path, + "expanduser", + side_effect=lambda p: p.replace("~/.local/share", str(tmp_path)).replace( + "~/.config", str(config_dir) + ), + ), + patch.object(subprocess, "run"), + patch.object(os, "makedirs"), + ): + from deadline.client.cli._deadline_web_url import install_deadline_web_url_handler + + install_deadline_web_url_handler(all_users=False) + + assert stat.S_IMODE(mimeapps_path.stat().st_mode) == 0o644 + + def test_linux_install_creates_mimeapps_when_missing(fresh_deadline_config, tmp_path): """ Tests that when mimeapps.list does not exist yet, installing creates it