Skip to content

fix(plugin-manager): treat a dangling plugin symlink as not installed#2696

Open
elibosley wants to merge 1 commit into
masterfrom
fix/plugin-manager-dangling-install-symlink
Open

fix(plugin-manager): treat a dangling plugin symlink as not installed#2696
elibosley wants to merge 1 commit into
masterfrom
fix/plugin-manager-dangling-install-symlink

Conversation

@elibosley

@elibosley elibosley commented Jul 15, 2026

Copy link
Copy Markdown
Member

Summary

A stale plugin tracking symlink makes a plugin permanently un-installable via the plugin manager, failing with a misleading error:

plugin: XML file doesn't exist or xml parse error
sh: -c: line 1: unexpected EOF while looking for matching `'`

This has been observed with legacy plugins whose .plg was removed or renamed on the flash outside the manager (leaving the /var/log/plugins/<name>.plg tracking symlink dangling). The only workaround today is deleting the stale symlink by hand.

Root cause

The manager records an installed plugin as a symlink /var/log/plugins/<name>.plg/boot/config/plugins/<name>.plg, and uses @readlink($symlink) !== false as "is this plugin installed?".

readlink() still returns the target path for a dangling symlink (target file gone), so the manager takes the re-install path and then tries to read the version/pluginURL from the missing target:

$installed_version = plugin('version', $installed_plugin_file, $error);
if ($installed_version === false) { write("plugin: $error\n"); ... done(1); }

plugin() can't open the missing file, sets the generic "XML file doesn't exist or xml parse error", and the install aborts. (check and update hit the same latent flaw.) The secondary sh: -c: ... matching ' noise is downstream: the error string itself contains an apostrophe ("doesn't"), which breaks the hook invocation run("$hook plugin $method $plugin $error").

Fix

Add a small installed_plugin() helper that resolves the tracking symlink, treats a dangling link as not installed, and removes the stale link so the next operation starts from a clean state:

function installed_plugin($symlink) {
  $target = @readlink($symlink);
  if ($target === false) return false;
  if (!is_file($target)) { @unlink($symlink); return false; }
  return $target;
}

Used in the install, check, and update handlers in place of the bare @readlink(). remove already unlinks the stale symlink before acting, so it self-heals and is left unchanged.

Verification

On a live system, with a dangling tracking symlink:

Before After
plugin check <name> XML file doesn't exist or xml parse error, stale link remains plugin: not installed, stale link cleaned
plugin install <url> aborts on parse error proceeds with a normal fresh install

php -l clean.

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of removed or renamed plugins during installation, checking, and updates.
    • Automatically cleans up stale plugin links instead of treating missing plugins as installed.
    • Prevents plugin operations from targeting nonexistent plugin files.

The plugin manager tracks installed plugins with a symlink under
/var/log/plugins/<name>.plg pointing at the real /boot/config/plugins/<name>.plg.
It used `@readlink()` alone to decide whether a plugin is installed, so a
*dangling* symlink — one whose target .plg was removed or renamed on the flash
out-of-band — was still read as "installed". The manager then tried to parse the
missing target and aborted with:

    plugin: XML file doesn't exist or xml parse error

For `install` this made the plugin permanently un-installable until the stale
symlink was removed by hand; `check` and `update` failed the same way.

Add an `installed_plugin()` helper that resolves the tracking symlink to its
target, treats a dangling link as not installed, and removes the stale link so a
subsequent install/update/check starts clean. Use it in the install, check, and
update handlers in place of the bare `@readlink()`. (`remove` already unlinks the
stale symlink before acting, so it self-heals.)

Verified on a live box: with a dangling tracking symlink, `check` now reports
"plugin: not installed" and clears the stale link, and a fresh install proceeds
normally instead of aborting on the parse error.
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The plugin script adds validated symlink resolution and stale-link removal. Install, check, and update commands now use this helper, treating missing plugin targets as not installed.

Changes

Plugin symlink cleanup

Layer / File(s) Summary
Validated plugin resolution
emhttp/plugins/dynamix.plugin.manager/scripts/plugin
Adds installed_plugin() to resolve plugin symlinks, detect missing targets, and remove stale links.
Lifecycle command integration
emhttp/plugins/dynamix.plugin.manager/scripts/plugin
Install, check, and update paths use installed_plugin() instead of direct symlink reads.

Estimated code review effort: 2 (Simple) | ~10 minutes

Poem

A rabbit hops past a symlink thread,
Finds the plugin file is gone instead.
It tidies the link with a gentle sweep,
So install and checks know what to keep.
Hop, hop—stale paths now sleep!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: handling dangling plugin symlinks as not installed.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/plugin-manager-dangling-install-symlink

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

🔧 PR Test Plugin Available

A test plugin has been generated for this PR that includes the modified files.

Version: 2026.07.15.1649
Build: View Workflow Run

📥 Installation Instructions:

Install via Unraid Web UI:

  1. Go to Plugins → Install Plugin
  2. Copy and paste this URL:
https://preview.dl.unraid.net/pr-plugins/pr-2696/webgui-pr-2696.plg
  1. Click Install

Alternative: Direct Download

⚠️ Important Notes:

  • Testing only: This plugin is for testing PR changes
  • Backup included: Original files are automatically backed up
  • Easy removal: Files are restored when plugin is removed
  • Conflicts: Remove this plugin before installing production updates
  • Post-merge behavior: This preview stays available after merge until preview storage expires or it is manually cleaned up

📝 Modified Files:

Click to expand file list
emhttp/plugins/dynamix.plugin.manager/scripts/plugin

🔄 To Remove:

Navigate to Plugins → Installed Plugins and remove webgui-pr-2696, or run:

plugin remove webgui-pr-2696

🤖 This comment is automatically generated and will be updated with each new push to this PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@emhttp/plugins/dynamix.plugin.manager/scripts/plugin`:
- Around line 521-535: Update the checkall, updateall, and checkos bulk loops to
call installed_plugin() when resolving tracking symlinks instead of bypassing it
with direct checks. Preserve each loop’s existing handling for valid installed
plugins while ensuring dangling symlinks are removed and treated as not
installed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 615588bd-ed09-4084-9a2f-057611ff1eae

📥 Commits

Reviewing files that changed from the base of the PR and between 369f0b2 and dcb9acb.

📒 Files selected for processing (1)
  • emhttp/plugins/dynamix.plugin.manager/scripts/plugin

Comment on lines +521 to +535
// Resolve the installed plugin file that a tracking symlink points at, or FALSE
// if the plugin is not installed. A dangling symlink - one whose target .plg has
// been removed or renamed on the flash out-of-band - is treated as not installed
// and the stale link is cleaned up, so a subsequent install/update/check starts
// from a clean state instead of failing to parse the missing target.
function installed_plugin($symlink) {
$target = @readlink($symlink);
if ($target === false) return false;
if (!is_file($target)) {
@unlink($symlink);
return false;
}
return $target;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '500,680p' emhttp/plugins/dynamix.plugin.manager/scripts/plugin

Repository: unraid/webgui

Length of output: 6097


🏁 Script executed:

rg -n "^function plugin\\(|plugin\\('pluginURL'|plugin\\('version'" emhttp/plugins/dynamix.plugin.manager/scripts/plugin

Repository: unraid/webgui

Length of output: 904


🏁 Script executed:

sed -n '1,220p' emhttp/plugins/dynamix.plugin.manager/scripts/plugin

Repository: unraid/webgui

Length of output: 9648


🏁 Script executed:

sed -n '303,430p' emhttp/plugins/dynamix.plugin.manager/scripts/plugin

Repository: unraid/webgui

Length of output: 4524


Use installed_plugin() in the bulk loops (emhttp/plugins/dynamix.plugin.manager/scripts/plugin:597,620,646)
checkall, updateall, and checkos still bypass the cleanup helper, so dangling tracking symlinks are silently skipped and left behind. Reusing installed_plugin() here keeps /var/log/plugins consistent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@emhttp/plugins/dynamix.plugin.manager/scripts/plugin` around lines 521 - 535,
Update the checkall, updateall, and checkos bulk loops to call
installed_plugin() when resolving tracking symlinks instead of bypassing it with
direct checks. Preserve each loop’s existing handling for valid installed
plugins while ensuring dangling symlinks are removed and treated as not
installed.

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.

1 participant