Skip to content

Skip a non-executable PATH match when looking up a helper [patch] - #35

Merged
matt-edmondson merged 4 commits into
mainfrom
fix/34-skip-non-executable-path-entries
Nov 4, 2026
Merged

matt-edmondson merged 4 commits into
mainfrom
fix/34-skip-non-executable-path-entries

Conversation

@matt-edmondson

Copy link
Copy Markdown
Collaborator

Fixes #34

What was wrong

ExecutableLocator.Find returned the first file on PATH whose name matched, using only File.Exists. The shell, command -v and execvp all skip a match that has no execute bit. So a stray, non-executable systemd-inhibit earlier on PATH hid the real one. --status then reported "supported", and every Acquire failed with Permission denied.

Change

  • Find now skips a candidate unless it is a regular file and, off Windows, has at least one of UserExecute, GroupExecute or OtherExecute set. A directory with the command's name was already skipped by File.Exists, and the new test covers it.
  • The lookup moved into an internal Find(fileName, path) overload that the public Find calls with the PATH variable. The test passes its own search path, so it doesn't change the process environment.

Tests

PlatformIntegrationTests.Find_SkipsANonExecutableMatchEarlierOnThePath sets up three directories in this order: one with a mode-644 file named like the command, one with a directory named like it, and one with an executable file. It checks that Find returns the executable, and returns null when only the first two are on the path. On Windows the test is inconclusive.

With the ExecutableLocator change reverted, the new test fails. With it, the full suite passes (30/30) on Linux.


Generated by Claude Code

ExecutableLocator.Find returned the first file on PATH with the right name,
so a stray non-executable systemd-inhibit earlier on PATH hid the real one:
--status reported support and every Acquire failed with Permission denied.
Skip a match with no execute bit (off Windows), as the shell and execvp do.

Fixes #34

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW2EzJMFdK7ZQBB2keC
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW2EzJMFdK7ZQBB2keC

Copy link
Copy Markdown
Collaborator Author

The SonarCloud gate fails on 66.7% new-code coverage, and I'm leaving it as is. The new lines it reports as uncovered are the ones the Linux test run can't reach:

  • IsExecutableFile's OperatingSystem.IsWindows() early return. The coverage run is on Linux, and Windows has no execute bit to check.
  • The IOException / UnauthorizedAccessException catches around File.GetUnixFileMode. They guard a file that is deleted or made unreadable between File.Exists and the mode read. Without them, that race would throw out of LinuxSleepBlocker's constructor. A deterministic test would need a filesystem fake that this code doesn't use, which is more machinery than the guard is worth.

All the reachable paths are tested: an executable match, a non-executable match, a directory with the command's name, and no match at all. The gate is not a required check, so it doesn't block merging.


Generated by Claude Code

SonarCloud measures coverage from the Windows run, where the Unix-only
branch never executes, so new-code coverage sat at 66.7%. The decision
now takes its mode reader as a parameter and is tested directly,
including the unreadable-mode cases, and a cross-platform test drives
Find over a given path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW5jpiuVuBRXSgitSY1
@sonarqubecloud

sonarqubecloud Bot commented Nov 4, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A non-executable systemd-inhibit earlier on PATH hides the real one: --status says "supported" but every Acquire fails with Permission denied

2 participants