fix: preserve existing mimeapps.list entries when installing web url handler - #1286
fix: preserve existing mimeapps.list entries when installing web url handler#1286crowecawcaw wants to merge 7 commits into
Conversation
…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>
| mimeapps.optionxform = str # type: ignore[assignment,method-assign] | ||
|
|
||
| if os.path.isfile(mimeapps_list_file_path): | ||
| mimeapps.read(mimeapps_list_file_path) |
There was a problem hiding this comment.
configparser.read() on an existing mimeapps.list can raise on any content that is not strictly valid INI — e.g. MissingSectionHeaderError (stray key before a section), DuplicateOptionError/DuplicateSectionError (default strict=True), or a UnicodeDecodeError. That exception now propagates uncaught out of install_deadline_web_url_handler, crashing the CLI with a raw traceback.
This is a behavior change from the old truncate-and-write path, which tolerated any prior content. Every other failure mode in this function is surfaced as a DeadlineOperationError; consider wrapping the read() in try/except and raising DeadlineOperationError for consistency (and to avoid an install failing on a slightly-malformed but otherwise usable mimeapps.list).
There was a problem hiding this comment.
Addressed in 0dc7a86: the read() is now wrapped in try/except (configparser.Error, UnicodeDecodeError) and surfaced as a DeadlineOperationError, consistent with the other failure modes in this function. Covered by test_linux_install_raises_on_malformed_mimeapps.
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>
|
|
||
| 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) |
There was a problem hiding this comment.
Note: configparser round-tripping preserves all associations (the important data this fix targets), but it does not preserve comments (# ... lines) or blank-line formatting in the original mimeapps.list. Those are silently dropped on rewrite. This is almost certainly acceptable in practice — comments in mimeapps.list are rare — but it does mean the "preserve everything" framing in the comment/PR isn't strictly complete. No change required if that trade-off is intended; flagging it since the change is explicitly about avoiding data loss.
There was a problem hiding this comment.
Acknowledged — dropping comments/blank lines on rewrite is an intended trade-off. The goal of this change is preserving the association data; comments in mimeapps.list are rare and other desktop tools (e.g. GLib's key-file writer) drop them too. Keeping configparser avoids hand-rolling an INI round-tripper for this edge case.
| # 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) |
There was a problem hiding this comment.
Consider passing strict=False to ConfigParser(...). With the default strict=True, configparser raises DuplicateOptionError/DuplicateSectionError on any repeated key or section — and real-world mimeapps.list files written by other desktop tools have historically contained duplicate keys (especially in [Added Associations]). On such a file, the current code converts what used to be an "install always succeeds" path into a hard DeadlineOperationError, so the handler can't be installed at all until the user hand-edits their file.
Since this change is specifically about being robust to whatever is already in mimeapps.list, strict=False (last-value-wins) would tolerate these files while still preserving unrelated associations, which seems closer to the intent. Genuinely malformed files (no section header, bad encoding) would still be caught by the surrounding try/except.
There was a problem hiding this comment.
Addressed in dbade12: ConfigParser is now constructed with strict=False so duplicate keys/sections in an existing mimeapps.list are tolerated (last value wins). Covered by test_linux_install_tolerates_duplicate_mimeapps_entries.
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>
…' into review-fix/web-url-mimeapps Signed-off-by: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com>
| f"{DEADLINE_URL_SCHEME_NAME}.desktop;", | ||
| ) | ||
|
|
||
| with open(mimeapps_list_file_path, "w") as mimeapps_list_file: |
There was a problem hiding this comment.
This rewrite is not atomic: open(mimeapps_list_file_path, "w") truncates the existing file before mimeapps.write() runs. If the process is interrupted between truncation and a successful write (crash, kill, disk-full, or an exception raised inside write()), the file is left empty or partially written — losing precisely the unrelated default-application associations this change was written to protect.
Consider writing to a temp file in the same directory and os.replace()-ing it into place so the original is only replaced once the new content is fully and successfully written:
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:
mimeapps.write(tmp, space_around_delimiters=False)
os.replace(tmp_path, mimeapps_list_file_path)
except BaseException:
os.unlink(tmp_path)
raiseThere was a problem hiding this comment.
Addressed in 58febaa: the rewrite now renders to a mkstemp temp file in the same directory and os.replace()s it into place, with the temp file unlinked on failure. Covered by test_linux_install_preserves_mimeapps_when_write_fails.
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>
| import tempfile | ||
|
|
||
| dir_name = os.path.dirname(mimeapps_list_file_path) | ||
| fd, tmp_path = tempfile.mkstemp(dir=dir_name, prefix=".mimeapps.list.", suffix=".tmp") |
There was a problem hiding this comment.
The atomic-write here does not preserve the destination file's permissions. tempfile.mkstemp() creates the temp file with mode 0600 (owner read/write only), and os.replace() keeps that mode on the final mimeapps.list.
For the all_users case (/usr/share/applications/mimeapps.list, written as root) this is a functional regression: the system-wide file needs to be world-readable so every user's desktop environment can resolve associations, but after this change it becomes root-only (0600) — and it silently drops the permissions on a pre-existing file that was previously e.g. 0644.
Consider stat-ing the existing file first (if present) and applying its mode to the temp file before os.replace(), or applying a sane default honoring umask (e.g. 0644) when the file is new. mkstemp's hardcoded 0600 is rarely what you want for a shared config file.
There was a problem hiding this comment.
Addressed in 08ec80f: the existing file's mode is stat()ed before the rewrite and applied to the temp file via os.chmod() before os.replace(); a newly created mimeapps.list defaults to 0644. Covered by test_linux_install_preserves_mimeapps_permissions and test_linux_install_creates_mimeapps_with_default_permissions.
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>
Fixes:
What was the problem/requirement? (What/Why)
deadline handle-web-url --installopened the user'smimeapps.listin"w"mode, which truncates the file. Installing the Deadline URL handler therefore wiped every unrelated default-application association (browser, PDF viewer, mailto handler, etc.) — permanent data loss for the user.What was the solution? (How)
Read and parse the existing
mimeapps.list(an INI-style file) withconfigparser, add/update only thex-scheme-handler/deadlineentry in[Default Applications], and write the file back preserving all other entries. The missing-file case is handled (a fresh file with just the deadline entry).configparseris created withinterpolation=Noneandoptionxform=strso mime-type/scheme keys keep their case and%characters don't break parsing; entries are written in thekey=value.desktop;freedesktop format.What is the impact of this change?
--installno longer destroys existing default-app associations. The deadline-owned.desktopfile write path is unchanged.How was this change tested?
Added a red-green regression test that pre-populates
mimeapps.listwith unrelated associations, runs the install path, and asserts the unrelated entries survive while the deadline handler is added, plus a test for the missing-file case.test/unit/deadline_client/cli/test_cli_handle_web_url.py(73 passed, 2 platform-gated skips).Was this change documented?
Does this PR introduce new dependencies?
configparseris stdlib.)Is this a breaking change?
No.
Does this change impact security?
No new threat surface; it prevents destruction of user-owned files (data-loss fix).
Testing
Automated
Unit tests in
test/unit/deadline_client/cli/test_cli_handle_web_url.pycover every changed behavior and its failure mode (78 passed, 2 platform-gated skips on this run):test_linux_install_preserves_existing_mimeapps_entries— pre-existing associations survive; deadline handler addedtest_linux_install_creates_mimeapps_when_missing— fresh file created with the handler entrytest_linux_install_raises_on_malformed_mimeapps— non-INI file surfaces asDeadlineOperationError, not a raw tracebacktest_linux_install_tolerates_duplicate_mimeapps_entries— duplicate keys/sections tolerated (strict=False, last value wins)test_linux_install_preserves_mimeapps_when_write_fails— atomic write: on mid-write failure the original file is untouched and no temp files are left behindtest_linux_install_preserves_mimeapps_permissions/test_linux_install_creates_mimeapps_with_default_permissions— existing file mode preserved across the temp-file rewrite; new file gets 0644 (not mkstemp's 0600)Manual end-to-end verification
Ran the real
install_deadline_web_url_handler()code path in an isolated throwaway$HOME(never touching the real~/.config), with a stubdeadline/update-desktop-databaseonPATHsoshutil.whichandsubprocess.runexecuted for real. Host was macOS withsys.platformpatched tolinux; the filesystem behavior (configparser round-trip,mkstemp,chmod,os.replace) is identical, but this has not been exercised on a physical Linux desktop session.Before (
~/.config/mimeapps.list, deliberately chmod'ed to0640):After install:
Observed results (19/19 checks passed):
[Default Applications]associations and the[Added Associations]section/entry survived; the deadline handler was appended.0640before and after); a freshly created file gets0644.DeadlineOperationErrorwith a clear message and leaves the original file byte-for-byte untouched..mimeapps.list.*.tmpfiles left behind in~/.config.# ...) and blank-line formatting are not preserved by the configparser round-trip — associations are, which is the data this fix protects.Note: this verification ran on macOS with the platform patched; a smoke test on a real Linux desktop (checking the DE actually resolves
deadline://URLs after install and that existing browser/PDF defaults still work) would be a worthwhile final check.