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:
parent
3c613f0e44
commit
1d20b833f2
Binary file not shown.
|
|
@ -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 -->
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue