Added custom plugins manifest URL option for self hosting - #4585
Added custom plugins manifest URL option for self hosting#4585mrmetrix4 wants to merge 1 commit into
Conversation
📝 WalkthroughWalkthroughChangesPlugins manifest URL configuration
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant SettingsUI
participant PublicAPIInstance
participant PluginsManifest
participant PluginStores
SettingsUI->>PublicAPIInstance: set PluginsManifestUrl
PublicAPIInstance->>PluginsManifest: UpdateManifestAsync(_settings, ...)
PluginsManifest->>PluginStores: construct configured or default stores
PluginsManifest->>PluginStores: fetch and update manifest
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs (1)
15-41: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftMake manifest state source-aware and synchronize store selection.
mainPluginStoreis process-wide, but Lines 24-41 overwrite it from each call’s settings before acquiringmanifestUpdateLock. Concurrent callers can therefore fetch through another caller’s store. Also,UserPluginsandlastFetchedAtare not keyed bycustomUrl, so changing the setting within the two-minute window can leave the previous manifest in use. Keep the selected store local or initialize it under the lock, and invalidate/key the cache by URL.🤖 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 `@Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs` around lines 15 - 41, Make UpdateManifestAsync source-aware by deriving the CommunityPluginStore from the current customUrl inside the manifestUpdateLock or keeping it local, rather than overwriting shared mainPluginStore before synchronization. Key UserPlugins and lastFetchedAt by the effective manifest URL, and invalidate or bypass the cached manifest when customUrl changes so each settings source fetches and uses its own data.
🤖 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 `@Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs`:
- Around line 26-37: Validate PluginsManifestUrl before constructing
mainPluginStore: when customUrl is non-empty, require it to be a supported
absolute HTTPS URL and handle invalid input without allowing the constructor to
escape UpdateManifestAsync’s error path. Alternatively, move the new
plugin-store construction into the existing guarded section, preserving the
default mainUrls behavior.
---
Outside diff comments:
In `@Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs`:
- Around line 15-41: Make UpdateManifestAsync source-aware by deriving the
CommunityPluginStore from the current customUrl inside the manifestUpdateLock or
keeping it local, rather than overwriting shared mainPluginStore before
synchronization. Key UserPlugins and lastFetchedAt by the effective manifest
URL, and invalidate or bypass the cached manifest when customUrl changes so each
settings source fetches and uses its own data.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: d33357ed-31da-4f3e-8fa9-7a24de1f7e99
📒 Files selected for processing (6)
Flow.Launcher.Core/ExternalPlugins/PluginsManifest.csFlow.Launcher.Infrastructure/Constant.csFlow.Launcher.Infrastructure/UserSettings/PluginSettings.csFlow.Launcher/Languages/en.xamlFlow.Launcher/PublicAPIInstance.csFlow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml
| string customUrl = settings.PluginSettings.PluginsManifestUrl; | ||
|
|
||
| string[] mainUrls = [ | ||
| "https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/main/plugins.json", | ||
| "https://fastly.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@main/plugins.json", | ||
| "https://gcore.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@main/plugins.json", | ||
| "https://cdn.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@main/plugins.json" | ||
| ]; | ||
|
|
||
| if (!string.IsNullOrWhiteSpace(customUrl)) | ||
| { | ||
| mainPluginStore = new(customUrl, mainUrls); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate relevant files =="
fd -i 'PluginsManifest.cs|IPluginStore|CommunityPluginStore|.*Store.*\.cs' . | sed -n '1,120p'
echo
echo "== outline PluginsManifest.cs =="
ast-grep outline Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs --view expanded || true
echo
echo "== relevant PluginsManifest.cs section =="
cat -n Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs | sed -n '1,120p'
echo
echo "== search CommunityPluginStore references =="
rg -n "class CommunityPluginStore|CommunityPluginStore|new\\(customUrl|PluginsManifestUrl|try\\s*\\{" -S . | sed -n '1,220p'Repository: Flow-Launcher/Flow.Launcher
Length of output: 6085
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== CommunityPluginStore.cs =="
cat -n Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs | sed -n '1,240p'
echo
echo "== PluginSettings.cs section =="
cat -n Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs | sed -n '1,120p'
echo
echo "== SettingsBindingViewModel references for validation =="
rg -n "PluginsManifestUrl|plugin Store|Plugin Store|Validate|Uri\\(" Flow.Launcher/Flow.Launcher.Core -S | sed -n '1,240p'
echo
echo "== C# Uri constructor behavior probe =="
python3 - <<'PY'
# Deterministic check of inputs relative to current method ordering and comment premise.
snippet = """
string customUrl = settings.PluginSettings.PluginsManifestUrl;
string[] mainUrls = [...];
if (!string.IsNullOrWhiteSpace(customUrl))
{
mainPluginStore = new(customUrl, mainUrls);
} else {
mainPluginStore = new(mainUrls[0], mainUrls[1..]);
}
try
{
await manifestUpdateLock.WaitAsync(token)...
}
catch (Exception e)
{
PublicApi.Instance.LogException(ClassName, "Http request failed", e);
}
"""
checks = {
"new_before_try": "new(customUrl, mainUrls)" in snippet and snippet.index("(customUrl, mainUrls)") < snippet.index("try\n {"),
"catch_catches_Construction": snippet.index("catch (Exception e)") > snippet.index("(customUrl, mainUrls)"),
"operation_canceled_catches_Construction": "catch (Exception e)" in snippet.split("catch (OperationCanceledException)", 1)[1],
}
print(checks)
PYRepository: Flow-Launcher/Flow.Launcher
Length of output: 8469
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== plugin source and URI handling =="
fd -i 'CommunityPluginSource.cs|.*Source.*\.cs' . | sed -n '1,120p'
echo
echo "== locate source files =="
rg -n "class CommunityPluginSource|record CommunityPluginSource|new Uri\\(" -S . --glob '*.cs' | sed -n '1,220p'
echo
echo "== relevant source snippets =="
for f in $(rg -l "class CommunityPluginSource|record CommunityPluginSource|new Uri\\(" . --glob '*.cs' | sed -n '1,20p'); do
echo "--- $f"
cat -n "$f" | sed -n '1,220p'
done
echo
echo "== UI binding validators around PluginsManifestUrl =="
rg -n "PluginsManifestUrl|ValidationRule|IValidator|NotifyDataError|Error\\(|Url|URL|Manifest" Flow.Launcher/Flow.Launcher.Core Flow.Launcher -S --glob '*.cs' | sed -n '1,260p'
echo
echo "== static behavioral probe of constructor side-effects (file shape) =="
python3 - <<'PY'
from pathlib import Path
for path in [
Path('Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs'),
Path('Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs'),
]:
text = path.read_text()
print(path)
terms = {
"new Uri(": text.count('new Uri('),
"Uri": text.count("Uri"),
"AbsoluteUri": text.count("AbsoluteUri"),
"TryParse": text.count("TryParse"),
"HttpWebRequest/HttpClient": any(k in text for k in ["HttpClient", "HttpWebRequest"]),
"Constructor/Initializer side effect": ".Select(url => new CommunityPluginSource(url))" in text or "new Uri(" in text,
}
for k,v in terms.items():
print(f"{k}: {v}")
PYRepository: Flow-Launcher/Flow.Launcher
Length of output: 50384
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Http.SendAsync implementation =="
cat -n Flow.Launcher.Infrastructure/Http/Http.cs | sed -n '1,230p'
echo
echo "== HttpRequestMessage(string, string) usage/tests =="
rg -n "new HttpRequestMessage\\(HttpMethod\\.Get, .*Invalid|invalid.*url|bad.*url|ManifestFileUrl|CommunityPluginSource" Flow.Launcher Flow.Launcher.Test -S --glob '*.cs' | sed -n '1,200p'
echo
echo "== focused docs check if dotnet doc XML exists =="
rg -n "class HttpRequestMessage|HttpRequestMessage\\(" . --glob '*.cs' | head -50Repository: Flow-Launcher/Flow.Launcher
Length of output: 11762
🌐 Web query:
Microsoft documentation HttpRequestMessage constructor UriFormatException invalid URI address
💡 Result:
The HttpRequestMessage constructor in.NET does not explicitly throw a UriFormatException for an "invalid" URI string in the way commonly associated with validation errors; rather, it typically accepts a string and attempts to create a Uri object [1][2]. However, developers frequently encounter errors related to URIs when using HttpClient or HttpRequestMessage that are often confused with constructor-level validation [3][4]. Key distinctions and common causes for URI-related exceptions include: 1. InvalidOperationException: This is the most common exception encountered when using HttpClient, often accompanied by the message: "An invalid request URI was provided. The request URI must either be an absolute URI or BaseAddress must be set" [3][4][5]. This occurs during the request execution process (e.g., SendAsync) when the HttpClient cannot resolve a full, absolute URI because the provided RequestUri is relative and the HttpClient.BaseAddress is null [4][6]. 2. UriFormatException: This exception is thrown by the System.Uri constructor if the string provided is malformed or cannot be parsed into a valid URI format [7]. If you manually create a Uri object using new Uri(string) before passing it to the HttpRequestMessage constructor, the Uri constructor will throw this exception if the input string is invalid [8][7]. 3. Common Resolution Strategies: - Ensure the URI is absolute: If you are not using a BaseAddress, provide a full, absolute URI (e.g., https://example.com/api) [3]. - Configure BaseAddress: If using relative paths, ensure the HttpClient instance has its BaseAddress property correctly set [4][6]. - Use UriKind: When manually creating Uri objects for relative paths, explicitly specify UriKind.Relative to avoid parsing errors [8]. In summary, if you are receiving an exception related to an "invalid URI" when using HttpRequestMessage, it is usually an InvalidOperationException indicating a missing base address or an incorrectly formatted string passed to the Uri constructor [4]. Always verify that your URI strings are properly formatted and that the HttpClient state aligns with your chosen URI structure [3][6].
Citations:
- 1: https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httprequestmessage.-ctor?view=net-10.0
- 2: https://github.com/microsoft/referencesource/blob/5697c29004a34d80acdaf5742d7e699022c64ecd/System/net/System/Net/Http/HttpRequestMessage.cs
- 3: https://learn.microsoft.com/en-us/answers/questions/437731/how-to-fix-an-invalid-uri-when-calling-client-send
- 4: HttpClient throws InvalidOperationException for two very different reasons dotnet/runtime#39758
- 5: https://stackoverflow.com/questions/70467086/why-is-this-httprequestmessages-request-uri-invalid
- 6: https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClient.cs
- 7: https://github.com/duplicati/duplicati/blob/master/Duplicati/Library/Backend/OneDrive/MicrosoftGraphBackend.cs
- 8: https://stackoverflow.com/questions/70161001/why-is-passing-requesturi-as-a-param-different-than-setting-it-in-httprequestmes
Validate PluginsManifestUrl before creating the plugin store.
UpdateManifestAsync passes the editable value directly to new(customUrl, mainUrls), while the method’s try/catch starts after that construction. Accept only supported absolute https URLs before this path, or move/create the store inside the guarded section; otherwise malformed URL input can fail before FetchAsync.
🤖 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 `@Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs` around lines 26 - 37,
Validate PluginsManifestUrl before constructing mainPluginStore: when customUrl
is non-empty, require it to be a supported absolute HTTPS URL and handle invalid
input without allowing the constructor to escape UpdateManifestAsync’s error
path. Alternatively, move the new plugin-store construction into the existing
guarded section, preserving the default mainUrls behavior.
There was a problem hiding this comment.
4 issues found across 6 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs">
<violation number="1" location="Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs:26">
P2: Concurrent manifest updates can replace the shared `mainPluginStore` before the lock is acquired, so one request may fetch using another request's manifest URL. Build or select the store under `manifestUpdateLock` (or use a local immutable store) so each update uses a consistent URL configuration.</violation>
<violation number="2" location="Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs:37">
P1: A custom manifest is not reliably used: built-in manifests race the configured URL and usually win, so self-hosted plugins can be silently replaced by the public manifest. Create the custom store without these unrelated secondary URLs.</violation>
<violation number="3" location="Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs:39">
P3: Scheduled and manual refreshes now redownload the full manifest instead of using the existing conditional-request cache, because each call discards `CommunityPluginSource.latestEtag`. Retain the store while its configured URL is unchanged, and recreate/reset it only when that configuration changes.</violation>
</file>
<file name="Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml">
<violation number="1" location="Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml:512">
P2: The custom URL setting is not represented by the added `Constant.PluginsManifestURL` field at runtime: manifest loading reads `Settings.PluginSettings.PluginsManifestUrl` directly, and repository references show no consumer of `Constant.PluginsManifestURL`. Remove the unused global assignment/field or route the manifest-loading code through one consistent source to avoid a misleading configuration path.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
|
|
||
| if (!string.IsNullOrWhiteSpace(customUrl)) | ||
| { | ||
| mainPluginStore = new(customUrl, mainUrls); |
There was a problem hiding this comment.
P1: A custom manifest is not reliably used: built-in manifests race the configured URL and usually win, so self-hosted plugins can be silently replaced by the public manifest. Create the custom store without these unrelated secondary URLs.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs, line 37:
<comment>A custom manifest is not reliably used: built-in manifests race the configured URL and usually win, so self-hosted plugins can be silently replaced by the public manifest. Create the custom store without these unrelated secondary URLs.</comment>
<file context>
@@ -24,8 +21,24 @@ public static class PluginsManifest
+
+ if (!string.IsNullOrWhiteSpace(customUrl))
+ {
+ mainPluginStore = new(customUrl, mainUrls);
+ } else {
+ mainPluginStore = new(mainUrls[0], mainUrls[1..]);
</file context>
| mainPluginStore = new(customUrl, mainUrls); | |
| mainPluginStore = new(customUrl); |
| <TextBox | ||
| Width="370" | ||
| Height="34" | ||
| Text="{Binding Settings.PluginSettings.PluginsManifestUrl,TargetNullValue='None'}" /> |
There was a problem hiding this comment.
P2: The custom URL setting is not represented by the added Constant.PluginsManifestURL field at runtime: manifest loading reads Settings.PluginSettings.PluginsManifestUrl directly, and repository references show no consumer of Constant.PluginsManifestURL. Remove the unused global assignment/field or route the manifest-loading code through one consistent source to avoid a misleading configuration path.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml, line 512:
<comment>The custom URL setting is not represented by the added `Constant.PluginsManifestURL` field at runtime: manifest loading reads `Settings.PluginSettings.PluginsManifestUrl` directly, and repository references show no consumer of `Constant.PluginsManifestURL`. Remove the unused global assignment/field or route the manifest-loading code through one consistent source to avoid a misleading configuration path.</comment>
<file context>
@@ -504,6 +504,15 @@
+ <TextBox
+ Width="370"
+ Height="34"
+ Text="{Binding Settings.PluginSettings.PluginsManifestUrl,TargetNullValue='None'}" />
+ </StackPanel>
+ </ui:SettingsCard>
</file context>
| public static async Task<bool> UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default) | ||
| public static async Task<bool> UpdateManifestAsync(Settings settings, bool usePrimaryUrlOnly = false, CancellationToken token = default) | ||
| { | ||
| string customUrl = settings.PluginSettings.PluginsManifestUrl; |
There was a problem hiding this comment.
P2: Concurrent manifest updates can replace the shared mainPluginStore before the lock is acquired, so one request may fetch using another request's manifest URL. Build or select the store under manifestUpdateLock (or use a local immutable store) so each update uses a consistent URL configuration.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs, line 26:
<comment>Concurrent manifest updates can replace the shared `mainPluginStore` before the lock is acquired, so one request may fetch using another request's manifest URL. Build or select the store under `manifestUpdateLock` (or use a local immutable store) so each update uses a consistent URL configuration.</comment>
<file context>
@@ -24,8 +21,24 @@ public static class PluginsManifest
- public static async Task<bool> UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default)
+ public static async Task<bool> UpdateManifestAsync(Settings settings, bool usePrimaryUrlOnly = false, CancellationToken token = default)
{
+ string customUrl = settings.PluginSettings.PluginsManifestUrl;
+
+ string[] mainUrls = [
</file context>
| { | ||
| mainPluginStore = new(customUrl, mainUrls); | ||
| } else { | ||
| mainPluginStore = new(mainUrls[0], mainUrls[1..]); |
There was a problem hiding this comment.
P3: Scheduled and manual refreshes now redownload the full manifest instead of using the existing conditional-request cache, because each call discards CommunityPluginSource.latestEtag. Retain the store while its configured URL is unchanged, and recreate/reset it only when that configuration changes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs, line 39:
<comment>Scheduled and manual refreshes now redownload the full manifest instead of using the existing conditional-request cache, because each call discards `CommunityPluginSource.latestEtag`. Retain the store while its configured URL is unchanged, and recreate/reset it only when that configuration changes.</comment>
<file context>
@@ -24,8 +21,24 @@ public static class PluginsManifest
+ {
+ mainPluginStore = new(customUrl, mainUrls);
+ } else {
+ mainPluginStore = new(mainUrls[0], mainUrls[1..]);
+ }
+
</file context>
As talked in a lot of issues
Summary by cubic
Adds support for a custom plugins manifest URL for self‑hosting, with a new field in General settings. Defaults remain unchanged when the field is empty and fallbacks are still used.
Summary of changes
PluginsManifest.UpdateManifestAsyncnow takesSettingsand buildsCommunityPluginStoreat call time, usingSettings.PluginSettings.PluginsManifestUrlas primary when set, otherwise the existing default URLs. UpdatedPublicAPIInstance.UpdatePluginManifestAsyncto pass settings. Replaced the previous static, hard-codedmainPluginStore.PluginSettings.PluginsManifestUrl(also mirrored toConstant.PluginsManifestURL). New General settings field to enter the URL. AddedpluginManifestUrlstring inen.xaml.mainPluginStorewith only the GitHub/CDN URLs.Release Note
You can now set a custom plugin catalog URL in Settings to use a self‑hosted plugins list.
Written for commit 46122c6. Summary will update on new commits.