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]); function call(method, args) {
return new Promise((resolve, reject) => {
try {
if (window.runtime && window.runtime.Call) {
window.runtime.Call(method, JSON.stringify(args || []))
.then(result => resolve(result))
.catch(err => reject(err));
} else {
const parts = method.split('.');
let obj = window.go;
for (const p of parts) obj = obj[p];
resolve(obj.apply(null, args || []));
}
} catch (e) {
reject(e);
}
});
} }
async function loadData() { function loadPlugins() {
loading = true;
error = ''; error = '';
try { call('api.App.GetPlugins').then(p => {
const [p, caps, perms] = await Promise.all([ plugins = p || [];
rpc(window.go.api.App.GetPlugins(), 'GetPlugins'),
rpc(window.go.api.App.GetCapabilities(), 'GetCapabilities'),
rpc(window.go.api.App.GetPermissions(), 'GetPermissions'),
]);
plugins = p;
capabilities = caps;
permissions = perms;
} catch (e) {
error = String(e);
console.error('[PluginManager] load error:', e);
} finally {
loading = false; 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 -->