plugins 2.0 update
This commit is contained in:
@@ -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`.
|
||||
@@ -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);
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user