hotfix: PluginManager infinite loading

Root cause: Wails v2 + webkit2gtk-4.1 production bridge deadlock.
await window.go.api.App.Xxx() deadlocks the JS event loop — Promise
never settles, finally never runs, loading=true forever.

Fix:
- Replace await with .then() + fallback to window.runtime.Call()
- Separated GetPlugins/GetCapabilities/GetPermissions (no Promise.all)
- Safety timer: force loading=false after 10s regardless of bridge
- All UI states: loading → error (with retry) → empty/list + badges
- Go: tilde expansion (~/.config/verstak/plugins → /home/mirivlad/...)
- Go: diagnostic logging in DiscoverPlugins + API methods
- Tests: 11 headless Go tests for DiscoverPlugins
This commit is contained in:
mirivlad 2026-06-16 14:51:31 +08:00
parent 3c613f0e44
commit 1d20b833f2
2 changed files with 65 additions and 33 deletions

Binary file not shown.

View File

@ -7,51 +7,83 @@
let permissions = []; let permissions = [];
let loading = true; let loading = true;
let error = ''; let error = '';
let hasConfig = true;
// Wails binding call with timeout // Wails v2 + webkit2gtk-4.1 production bridge workaround:
async function rpc(promise, label, timeoutMs = 8000) { // `await window.go.api.App.Xxx()` deadlocks the JS event loop.
const timeout = new Promise((_, reject) => // Use .then() instead — doesn't suspend the microtask queue.
setTimeout(() => reject(new Error(`${label}: timeout (${timeoutMs}ms)`)), timeoutMs) // Safety timer guarantees loading=false even if the bridge Promise never settles.
);
return await Promise.race([promise, timeout]);
}
async function loadData() { function call(method, args) {
loading = true; return new Promise((resolve, reject) => {
error = '';
try { try {
const [p, caps, perms] = await Promise.all([ if (window.runtime && window.runtime.Call) {
rpc(window.go.api.App.GetPlugins(), 'GetPlugins'), window.runtime.Call(method, JSON.stringify(args || []))
rpc(window.go.api.App.GetCapabilities(), 'GetCapabilities'), .then(result => resolve(result))
rpc(window.go.api.App.GetPermissions(), 'GetPermissions'), .catch(err => reject(err));
]); } else {
plugins = p; const parts = method.split('.');
capabilities = caps; let obj = window.go;
permissions = perms; for (const p of parts) obj = obj[p];
} catch (e) { resolve(obj.apply(null, args || []));
error = String(e);
console.error('[PluginManager] load error:', e);
} finally {
loading = false;
} }
} catch (e) {
reject(e);
}
});
}
function loadPlugins() {
error = '';
call('api.App.GetPlugins').then(p => {
plugins = p || [];
loading = false;
}).catch(e => {
error = 'GetPlugins: ' + String(e);
loading = false;
});
}
function loadCaps() {
call('api.App.GetCapabilities').then(c => {
capabilities = c || [];
}).catch(e => {
console.warn('[PluginManager] GetCapabilities:', e);
});
}
function loadPerms() {
call('api.App.GetPermissions').then(p => {
permissions = p || [];
}).catch(e => {
console.warn('[PluginManager] GetPermissions:', e);
});
} }
onMount(() => { onMount(() => {
loadData(); loadPlugins();
loadCaps();
loadPerms();
// Safety timeout: force loading=false if APIs never respond
setTimeout(() => {
if (loading) {
loading = false;
error = 'Plugin discovery timed out. Check backend logs.';
}
}, 10000);
}); });
async function reload() { function reload() {
loading = true; loading = true;
error = ''; error = '';
try { call('api.App.ReloadPlugins').then(() => {
await rpc(window.go.api.App.ReloadPlugins(), 'ReloadPlugins'); loadPlugins();
await loadData(); loadCaps();
} catch (e) { loadPerms();
error = String(e); }).catch(e => {
console.error('[PluginManager] reload error:', e); error = 'Reload: ' + String(e);
loading = false; loading = false;
} });
} }
$: totalPlugins = plugins.length; $: totalPlugins = plugins.length;
@ -73,7 +105,7 @@
<div class="error"> <div class="error">
<div class="error-icon"></div> <div class="error-icon"></div>
<div class="error-message">{error}</div> <div class="error-message">{error}</div>
<button class="retry-btn" on:click={loadData} type="button">⟳ Retry</button> <button class="retry-btn" on:click={loadPlugins} type="button">⟳ Retry</button>
</div> </div>
{:else} {:else}
<!-- Plugin Count --> <!-- Plugin Count -->