plugins 2.0 update

This commit is contained in:
KingKDot
2026-06-22 10:52:27 -05:00
parent e1f5e4c800
commit 30dfdda3ed
55 changed files with 2534 additions and 128 deletions
+5
View File
@@ -0,0 +1,5 @@
# Sample WASM HostInfo Plugin
This Plugin 2.0 sample is written in Go for TinyGo. It demonstrates how a universal WASM module can read HostInfo and emit it back to the UI.
On Windows, install TinyGo 0.41.0 or newer, then run this sample's local `build.bat`. TinyGo 0.41.0 added Go 1.26 support. The builder runs `tinygo build -target=wasi`, writes `sample-wasm-hostinfo.wasm`, and creates `sample-wasm-hostinfo.zip`.
+55
View File
@@ -0,0 +1,55 @@
@echo off
setlocal enabledelayedexpansion
set "PLUGIN_DIR=%~dp0"
set "PLUGIN_NAME=sample-wasm-hostinfo"
set "NATIVE_DIR=%PLUGIN_DIR%\native"
set "WASM_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.wasm"
set "ZIP_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.zip"
if not exist "%PLUGIN_DIR%\config.json" (
echo [error] config.json not found in %PLUGIN_DIR%
exit /b 1
)
set "WASM_SOURCE=%NATIVE_DIR%\main.go"
if exist "%WASM_SOURCE%" (
where tinygo >nul 2>&1
if errorlevel 1 (
echo [error] tinygo was not found. Install TinyGo or provide %WASM_OUT% yourself.
exit /b 1
)
echo [build] tinygo build -target=wasi -opt=2 -no-debug -o "%WASM_OUT%" "%WASM_SOURCE%"
tinygo build -target=wasi -opt=2 -no-debug -o "%WASM_OUT%" "%WASM_SOURCE%"
if errorlevel 1 (
echo [error] WASM build failed
exit /b 1
)
)
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
pushd "%PLUGIN_DIR%"
powershell -NoProfile -ExecutionPolicy Bypass -Command "$zip = '%ZIP_OUT%'; $items = @(); foreach ($file in @('config.json', '%PLUGIN_NAME%.html', '%PLUGIN_NAME%.css', '%PLUGIN_NAME%.js', '%PLUGIN_NAME%.wasm', 'server.js')) { if (Test-Path -LiteralPath $file -PathType Leaf) { $items += $file } }; foreach ($dir in @('src', 'assets')) { if (Test-Path -LiteralPath $dir -PathType Container) { $items += $dir } }; if ($items.Count -eq 0) { throw 'No plugin files found to package' }; Compress-Archive -Path $items -DestinationPath $zip -Force"
set "ZIP_STATUS=%ERRORLEVEL%"
popd
if not "%ZIP_STATUS%"=="0" exit /b %ZIP_STATUS%
if errorlevel 1 (
echo [error] zip failed
exit /b 1
)
if defined PLUGIN_SIGN_KEY (
where bun >nul 2>&1
if not errorlevel 1 (
echo [sign] Signing plugin with key: %PLUGIN_SIGN_KEY%
bun run "%~dp0..\..\Overlord-Server\scripts\plugin-sign.ts" --key "%PLUGIN_SIGN_KEY%" "%ZIP_OUT%"
) else (
echo [warn] bun not found, skipping plugin signing
)
)
echo [ok] %ZIP_OUT%
@@ -0,0 +1,93 @@
package main
import "unsafe"
//go:wasmimport env overlord_emit
func overlordEmit(eventPtr, eventLen, payloadPtr, payloadLen uint32) int32
//go:wasmimport env overlord_host_info
func overlordHostInfo(outPtr, outLen uint32) int32
var heap = make([]byte, 64*1024)
var heapOffset uint32 = 8
var hostInfo [1024]byte
var hostInfoLen uint32
//go:export overlord_alloc
func overlordAlloc(size uint32) uint32 {
if size == 0 {
size = 1
}
ptr := (heapOffset + 7) &^ 7
next := ptr + size
if next >= uint32(len(heap)) {
return 0
}
heapOffset = next
return uint32(uintptr(unsafe.Pointer(&heap[ptr])))
}
//go:export overlord_free
func overlordFree(ptr, size uint32) {
_, _ = ptr, size
}
//go:export overlord_on_load
func overlordOnLoad(hostPtr, hostLen uint32) int32 {
n := hostLen
if n > uint32(len(hostInfo)) {
n = uint32(len(hostInfo))
}
copy(hostInfo[:n], bytesAt(hostPtr, n))
hostInfoLen = n
emit("ready", hostInfo[:hostInfoLen])
return 0
}
//go:export overlord_on_event
func overlordOnEvent(eventPtr, eventLen, payloadPtr, payloadLen uint32) int32 {
_, _ = payloadPtr, payloadLen
event := string(bytesAt(eventPtr, eventLen))
if event != "query_host" {
return 0
}
var buf [1024]byte
n := overlordHostInfo(uint32(uintptr(unsafe.Pointer(&buf[0]))), uint32(len(buf)))
if n < 0 {
emitString("host_info", `{"error":"host info buffer too small"}`)
return 0
}
emit("host_info", buf[:uint32(n)])
return 0
}
//go:export overlord_on_unload
func overlordOnUnload() {}
func emitString(event string, payload string) {
emit(event, []byte(payload))
}
func emit(event string, payload []byte) {
eventBytes := []byte(event)
var eventPtr uint32
var payloadPtr uint32
if len(eventBytes) > 0 {
eventPtr = uint32(uintptr(unsafe.Pointer(&eventBytes[0])))
}
if len(payload) > 0 {
payloadPtr = uint32(uintptr(unsafe.Pointer(&payload[0])))
}
overlordEmit(eventPtr, uint32(len(eventBytes)), payloadPtr, uint32(len(payload)))
}
func bytesAt(ptr, length uint32) []byte {
if length == 0 {
return nil
}
return unsafe.Slice((*byte)(unsafe.Pointer(uintptr(ptr))), length)
}
func main() {}
@@ -0,0 +1,22 @@
#sample-wasm-hostinfo-output {
min-height: 8rem;
}
.sample-wasm-hostinfo-field {
border: 1px solid rgba(8, 145, 178, 0.45);
background: rgba(8, 47, 73, 0.28);
padding: 0.75rem;
}
.sample-wasm-hostinfo-field span {
display: block;
color: rgb(148, 163, 184);
font-size: 0.75rem;
}
.sample-wasm-hostinfo-field strong {
display: block;
color: rgb(224, 242, 254);
font-size: 0.95rem;
overflow-wrap: anywhere;
}
@@ -0,0 +1,12 @@
<div class="space-y-3">
<h2 class="text-lg font-semibold text-slate-100">Sample WASM Host Info</h2>
<button id="sample-wasm-hostinfo-run" class="px-3 py-2 rounded-lg border border-cyan-700 bg-cyan-950/40 text-cyan-100">Query host info</button>
<div id="sample-wasm-hostinfo-status" class="text-xs text-slate-400">Waiting for host info</div>
<div class="grid gap-2 sm:grid-cols-2">
<div class="sample-wasm-hostinfo-field"><span>Client</span><strong id="sample-wasm-hostinfo-client">-</strong></div>
<div class="sample-wasm-hostinfo-field"><span>Version</span><strong id="sample-wasm-hostinfo-version">-</strong></div>
<div class="sample-wasm-hostinfo-field"><span>OS</span><strong id="sample-wasm-hostinfo-os">-</strong></div>
<div class="sample-wasm-hostinfo-field"><span>Arch</span><strong id="sample-wasm-hostinfo-arch">-</strong></div>
</div>
<pre id="sample-wasm-hostinfo-output" class="text-xs text-slate-300 whitespace-pre-wrap"></pre>
</div>
@@ -0,0 +1,93 @@
(() => {
const out = document.getElementById("sample-wasm-hostinfo-output");
const btn = document.getElementById("sample-wasm-hostinfo-run");
const status = document.getElementById("sample-wasm-hostinfo-status");
const fields = {
clientId: document.getElementById("sample-wasm-hostinfo-client"),
version: document.getElementById("sample-wasm-hostinfo-version"),
os: document.getElementById("sample-wasm-hostinfo-os"),
arch: document.getElementById("sample-wasm-hostinfo-arch"),
};
const clientId = new URLSearchParams(window.location.search).get("clientId");
const pluginId = "sample-wasm-hostinfo";
let polling = false;
function log(message) {
if (out) out.textContent = `${message}\n${out.textContent || ""}`;
}
function setStatus(message) {
if (status) status.textContent = message;
}
function parsePayload(payload) {
if (payload == null) return {};
if (typeof payload === "string") {
try { return JSON.parse(payload); } catch (_) { return { raw: payload }; }
}
return payload;
}
function renderHostInfo(payload) {
const info = parsePayload(payload);
if (info.error) {
setStatus(info.error);
log(JSON.stringify(info, null, 2));
return;
}
if (fields.clientId) fields.clientId.textContent = info.clientId || "-";
if (fields.version) fields.version.textContent = info.version || "-";
if (fields.os) fields.os.textContent = info.os || "-";
if (fields.arch) fields.arch.textContent = info.arch || "-";
setStatus("Host info received");
log(JSON.stringify(info, null, 2));
}
function handlePluginEvent(event, payload) {
if (event === "ready" || event === "host_info") renderHostInfo(payload);
}
window.addEventListener("message", (e) => {
if (!e.data || e.data.type !== "plugin_event") return;
if (e.data.pluginId && e.data.pluginId !== pluginId) return;
handlePluginEvent(e.data.event, e.data.payload);
});
async function pollEvents() {
if (!clientId || polling) return;
polling = true;
try {
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${pluginId}/events`, {
headers: { Accept: "application/json" },
});
if (res.ok) {
const data = await res.json();
if (data.events) data.events.forEach((e) => handlePluginEvent(e.event, e.payload));
}
} catch (_) {
// Polling is best-effort; the next interval can recover.
} finally {
polling = false;
}
}
btn?.addEventListener("click", async () => {
if (!clientId) {
setStatus("Open this plugin from a client row so clientId is present.");
return;
}
setStatus("Requesting host info");
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/sample-wasm-hostinfo/event`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ event: "query_host", payload: {} }),
});
if (!res.ok) setStatus(`Send failed: ${res.status}`);
setTimeout(pollEvents, 250);
});
if (clientId) {
setInterval(pollEvents, 1500);
setTimeout(pollEvents, 300);
}
})();