plugins 2.0 update
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# Sample WASM Platform Note Plugin
|
||||
|
||||
This Plugin 2.0 sample is written in Rust. It reads HostInfo, branches on `os` and `arch`, and writes a small note into the approved `pluginData` bucket.
|
||||
|
||||
On Windows, install the Rust WASI target first:
|
||||
|
||||
```bat
|
||||
rustup target add wasm32-wasip1
|
||||
```
|
||||
|
||||
Then run this sample's local `build.bat`. The builder runs Cargo, writes `sample-wasm-platform-note.wasm`, and creates `sample-wasm-platform-note.zip`.
|
||||
@@ -0,0 +1,76 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set "PLUGIN_DIR=%~dp0"
|
||||
set "PLUGIN_NAME=sample-wasm-platform-note"
|
||||
|
||||
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 "RUST_MANIFEST=%NATIVE_DIR%\Cargo.toml"
|
||||
|
||||
if exist "%RUST_MANIFEST%" (
|
||||
where cargo >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [error] cargo was not found. Install Rust or provide %WASM_OUT% yourself.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
where rustup >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [error] rustup was not found. Install the Rust target manually: rustup target add wasm32-wasip1
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
rustup target list --installed | findstr /x "wasm32-wasip1" >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [setup] Installing Rust target wasm32-wasip1
|
||||
rustup target add wasm32-wasip1
|
||||
if errorlevel 1 (
|
||||
echo [error] Could not install wasm32-wasip1. Run this manually: rustup target add wasm32-wasip1
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo [build] cargo build --manifest-path "%RUST_MANIFEST%" --release --target wasm32-wasip1
|
||||
cargo build --manifest-path "%RUST_MANIFEST%" --release --target wasm32-wasip1
|
||||
if errorlevel 1 (
|
||||
echo [error] WASM build failed
|
||||
exit /b 1
|
||||
)
|
||||
copy /Y "%NATIVE_DIR%\target\wasm32-wasip1\release\sample_wasm_platform_note.wasm" "%WASM_OUT%" >nul
|
||||
if errorlevel 1 (
|
||||
echo [error] could not copy built WASM to %WASM_OUT%
|
||||
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,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "sample_wasm_platform_note"
|
||||
version = "0.1.0"
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "sample_wasm_platform_note"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
path = "src/lib.rs"
|
||||
@@ -0,0 +1,131 @@
|
||||
use std::alloc::{alloc, dealloc, Layout};
|
||||
use std::ptr;
|
||||
use std::slice;
|
||||
use std::str;
|
||||
|
||||
extern "C" {
|
||||
fn overlord_emit(event: *const u8, event_len: i32, payload: *const u8, payload_len: i32) -> i32;
|
||||
fn overlord_host_info(out: *mut u8, out_len: i32) -> i32;
|
||||
fn overlord_fs_mkdir(bucket: *const u8, bucket_len: i32, path: *const u8, path_len: i32) -> i32;
|
||||
fn overlord_fs_write(
|
||||
bucket: *const u8,
|
||||
bucket_len: i32,
|
||||
path: *const u8,
|
||||
path_len: i32,
|
||||
data: *const u8,
|
||||
data_len: i32,
|
||||
) -> i32;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn overlord_alloc(size: u32) -> *mut u8 {
|
||||
let size = size.max(1) as usize;
|
||||
let layout = Layout::from_size_align(size, 8).unwrap();
|
||||
unsafe { alloc(layout) }
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn overlord_free(ptr: *mut u8, size: u32) {
|
||||
if ptr.is_null() {
|
||||
return;
|
||||
}
|
||||
let layout = Layout::from_size_align((size.max(1)) as usize, 8).unwrap();
|
||||
unsafe { dealloc(ptr, layout) };
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn overlord_on_load(_host: *const u8, _host_len: u32) -> i32 {
|
||||
emit("ready", br#"{"sample":"platform-note"}"#);
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn overlord_on_event(
|
||||
event: *const u8,
|
||||
event_len: u32,
|
||||
_payload: *const u8,
|
||||
_payload_len: u32,
|
||||
) -> i32 {
|
||||
let event = unsafe { slice::from_raw_parts(event, event_len as usize) };
|
||||
if event != b"write_note" {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut host = [0u8; 1024];
|
||||
let host_len = unsafe { overlord_host_info(host.as_mut_ptr(), host.len() as i32) };
|
||||
let host = if host_len > 0 {
|
||||
str::from_utf8(&host[..host_len as usize]).unwrap_or("")
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let os = if host.contains(r#""os":"windows""#) {
|
||||
"windows"
|
||||
} else if host.contains(r#""os":"darwin""#) {
|
||||
"darwin"
|
||||
} else if host.contains(r#""os":"linux""#) {
|
||||
"linux"
|
||||
} else {
|
||||
"unknown"
|
||||
};
|
||||
|
||||
let arch = if host.contains(r#""arch":"arm64""#) {
|
||||
"arm64"
|
||||
} else if host.contains(r#""arch":"amd64""#) {
|
||||
"amd64"
|
||||
} else if host.contains(r#""arch":"386""#) {
|
||||
"386"
|
||||
} else {
|
||||
"unknown"
|
||||
};
|
||||
|
||||
let bucket = "pluginData";
|
||||
let dir = "platform";
|
||||
let file = format!("platform/{os}-{arch}.txt");
|
||||
let note = format!("single WASM plugin running on os={os} arch={arch}\n");
|
||||
|
||||
let mkdir_result = unsafe {
|
||||
overlord_fs_mkdir(
|
||||
bucket.as_ptr(),
|
||||
bucket.len() as i32,
|
||||
dir.as_ptr(),
|
||||
dir.len() as i32,
|
||||
)
|
||||
};
|
||||
let write_result = unsafe {
|
||||
overlord_fs_write(
|
||||
bucket.as_ptr(),
|
||||
bucket.len() as i32,
|
||||
file.as_ptr(),
|
||||
file.len() as i32,
|
||||
note.as_ptr(),
|
||||
note.len() as i32,
|
||||
)
|
||||
};
|
||||
|
||||
let response = format!(
|
||||
r#"{{"os":"{os}","arch":"{arch}","mkdir":{mkdir_result},"write":{write_result}}}"#
|
||||
);
|
||||
emit("platform_note", response.as_bytes());
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn overlord_on_unload() {}
|
||||
|
||||
fn emit(event: &str, payload: &[u8]) {
|
||||
unsafe {
|
||||
overlord_emit(
|
||||
event.as_ptr(),
|
||||
event.len() as i32,
|
||||
payload.as_ptr(),
|
||||
payload.len() as i32,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn memset(dest: *mut u8, value: i32, len: usize) -> *mut u8 {
|
||||
unsafe { ptr::write_bytes(dest, value as u8, len) };
|
||||
dest
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc_fingerprint":2568544898921049753,"outputs":{"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\n___.lib\n___.dll\nC:\\Users\\this1\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\npacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"msvc\"\ntarget_family=\"windows\"\ntarget_feature=\"cmpxchg16b\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"sse3\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"windows\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"pc\"\nwindows\n","stderr":""},"5946400966674136012":{"success":true,"status":"","code":0,"stdout":"___.wasm\nlib___.rlib\n___.wasm\nlib___.a\nC:\\Users\\this1\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\noff\n___\ndebug_assertions\npanic=\"abort\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"wasm32\"\ntarget_endian=\"little\"\ntarget_env=\"p1\"\ntarget_family=\"wasm\"\ntarget_feature=\"bulk-memory\"\ntarget_feature=\"multivalue\"\ntarget_feature=\"mutable-globals\"\ntarget_feature=\"nontrapping-fptoint\"\ntarget_feature=\"reference-types\"\ntarget_feature=\"sign-ext\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"wasi\"\ntarget_pointer_width=\"32\"\ntarget_vendor=\"unknown\"\n","stderr":"warning: dropping unsupported crate type `dylib` for target `wasm32-wasip1`\n\nwarning: dropping unsupported crate type `proc-macro` for target `wasm32-wasip1`\n\nwarning: 2 warnings emitted\n\n"},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.94.0 (4a4ef493e 2026-03-02)\nbinary: rustc\ncommit-hash: 4a4ef493e3a1488c6e321570238084b38948f6db\ncommit-date: 2026-03-02\nhost: x86_64-pc-windows-msvc\nrelease: 1.94.0\nLLVM version: 21.1.8\n","stderr":""}},"successes":{}}
|
||||
@@ -0,0 +1,3 @@
|
||||
Signature: 8a477f597d28d172789f06886806bc55
|
||||
# This file is a cache directory tag created by cargo.
|
||||
# For information about cache directory tags see https://bford.info/cachedir/
|
||||
@@ -0,0 +1,3 @@
|
||||
Signature: 8a477f597d28d172789f06886806bc55
|
||||
# This file is a cache directory tag created by cargo.
|
||||
# For information about cache directory tags see https://bford.info/cachedir/
|
||||
@@ -0,0 +1,22 @@
|
||||
#sample-wasm-platform-note-output {
|
||||
min-height: 8rem;
|
||||
}
|
||||
|
||||
.sample-wasm-platform-note-field {
|
||||
border: 1px solid rgba(5, 150, 105, 0.45);
|
||||
background: rgba(6, 78, 59, 0.25);
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.sample-wasm-platform-note-field span {
|
||||
display: block;
|
||||
color: rgb(148, 163, 184);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.sample-wasm-platform-note-field strong {
|
||||
display: block;
|
||||
color: rgb(209, 250, 229);
|
||||
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 Platform Note</h2>
|
||||
<button id="sample-wasm-platform-note-run" class="px-3 py-2 rounded-lg border border-emerald-700 bg-emerald-950/40 text-emerald-100">Write platform note</button>
|
||||
<div id="sample-wasm-platform-note-status" class="text-xs text-slate-400">Waiting to write platform note</div>
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
<div class="sample-wasm-platform-note-field"><span>OS</span><strong id="sample-wasm-platform-note-os">-</strong></div>
|
||||
<div class="sample-wasm-platform-note-field"><span>Arch</span><strong id="sample-wasm-platform-note-arch">-</strong></div>
|
||||
<div class="sample-wasm-platform-note-field"><span>mkdir</span><strong id="sample-wasm-platform-note-mkdir">-</strong></div>
|
||||
<div class="sample-wasm-platform-note-field"><span>write</span><strong id="sample-wasm-platform-note-write">-</strong></div>
|
||||
</div>
|
||||
<pre id="sample-wasm-platform-note-output" class="text-xs text-slate-300 whitespace-pre-wrap"></pre>
|
||||
</div>
|
||||
@@ -0,0 +1,93 @@
|
||||
(() => {
|
||||
const out = document.getElementById("sample-wasm-platform-note-output");
|
||||
const btn = document.getElementById("sample-wasm-platform-note-run");
|
||||
const status = document.getElementById("sample-wasm-platform-note-status");
|
||||
const fields = {
|
||||
os: document.getElementById("sample-wasm-platform-note-os"),
|
||||
arch: document.getElementById("sample-wasm-platform-note-arch"),
|
||||
mkdir: document.getElementById("sample-wasm-platform-note-mkdir"),
|
||||
write: document.getElementById("sample-wasm-platform-note-write"),
|
||||
};
|
||||
const clientId = new URLSearchParams(window.location.search).get("clientId");
|
||||
const pluginId = "sample-wasm-platform-note";
|
||||
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 renderPlatformNote(payload) {
|
||||
const info = parsePayload(payload);
|
||||
if (fields.os) fields.os.textContent = info.os || "-";
|
||||
if (fields.arch) fields.arch.textContent = info.arch || "-";
|
||||
if (fields.mkdir) fields.mkdir.textContent = String(info.mkdir ?? "-");
|
||||
if (fields.write) fields.write.textContent = String(info.write ?? "-");
|
||||
setStatus(info.write === 0 ? "Platform note written" : "Platform note result received");
|
||||
log(JSON.stringify(info, null, 2));
|
||||
}
|
||||
|
||||
function handlePluginEvent(event, payload) {
|
||||
if (event === "ready") {
|
||||
setStatus("Plugin loaded");
|
||||
log(JSON.stringify(parsePayload(payload), null, 2));
|
||||
return;
|
||||
}
|
||||
if (event === "platform_note") renderPlatformNote(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("Writing platform note");
|
||||
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/sample-wasm-platform-note/event`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ event: "write_note", 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