organize repo
This commit is contained in:
@@ -0,0 +1,882 @@
|
||||
# Overlord Plugins
|
||||
|
||||
This document explains how to build Overlord plugins using the **native plugin system**. Plugins run as native shared libraries (`.so` on Linux, `.dylib` on macOS, in-memory DLL on Windows), giving them full access to system APIs.
|
||||
|
||||
Plugins can be written in **any language** that can produce a C-ABI shared library: **Go**, **C**, **C++**, **Rust**, or anything else that compiles to native code.
|
||||
|
||||
> TL;DR: A plugin is a zip with platform-specific binaries (`.so`/`.dll`/`.dylib`) plus `<id>.html`, `<id>.css`, `<id>.js`. Upload it in the Plugins page or drop it in Overlord-Server/plugins.
|
||||
|
||||
## 1) How plugins are structured
|
||||
|
||||
### Required bundle format
|
||||
|
||||
A plugin bundle is a zip file named after the plugin ID:
|
||||
|
||||
```
|
||||
<pluginId>.zip
|
||||
```
|
||||
|
||||
Inside the zip (root level), you need:
|
||||
|
||||
- **Platform-specific binaries** named `<pluginId>-<os>-<arch>.<ext>`
|
||||
- **Web assets**: `<pluginId>.html`, `<pluginId>.css`, `<pluginId>.js`
|
||||
|
||||
Example for plugin ID `sample`:
|
||||
|
||||
```
|
||||
sample.zip
|
||||
├─ sample-linux-amd64.so
|
||||
├─ sample-linux-arm64.so
|
||||
├─ sample-darwin-arm64.dylib
|
||||
├─ sample-windows-amd64.dll
|
||||
├─ sample.html
|
||||
├─ sample.css
|
||||
└─ sample.js
|
||||
```
|
||||
|
||||
When the server extracts the zip:
|
||||
|
||||
```
|
||||
Overlord-Server/plugins/sample/
|
||||
├─ sample-linux-amd64.so
|
||||
├─ sample-linux-arm64.so
|
||||
├─ sample-darwin-arm64.dylib
|
||||
├─ sample-windows-amd64.dll
|
||||
├─ manifest.json (auto-generated)
|
||||
└─ assets/
|
||||
├─ sample.html
|
||||
├─ sample.css
|
||||
└─ sample.js
|
||||
```
|
||||
|
||||
### Architecture validation
|
||||
|
||||
The server matches binaries to clients by OS and architecture. Binary filenames **must** follow the naming convention `<pluginId>-<os>-<arch>.<ext>`:
|
||||
|
||||
| OS | Arch | Example filename |
|
||||
|----------|---------|------------------------------|
|
||||
| windows | amd64 | `sample-windows-amd64.dll` |
|
||||
| windows | arm64 | `sample-windows-arm64.dll` |
|
||||
| linux | amd64 | `sample-linux-amd64.so` |
|
||||
| linux | arm64 | `sample-linux-arm64.so` |
|
||||
| darwin | amd64 | `sample-darwin-amd64.dylib` |
|
||||
| darwin | arm64 | `sample-darwin-arm64.dylib` |
|
||||
|
||||
The server will **never** send an x64 binary to an ARM client (or vice versa). If no binary matches the client's platform, loading is skipped with an error.
|
||||
|
||||
### Manifest fields
|
||||
|
||||
The auto-generated manifest:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "sample",
|
||||
"name": "sample",
|
||||
"version": "1.0.0",
|
||||
"binaries": {
|
||||
"linux-amd64": "sample-linux-amd64.so",
|
||||
"linux-arm64": "sample-linux-arm64.so",
|
||||
"darwin-arm64": "sample-darwin-arm64.dylib",
|
||||
"windows-amd64": "sample-windows-amd64.dll"
|
||||
},
|
||||
"entry": "sample.html",
|
||||
"assets": {
|
||||
"html": "sample.html",
|
||||
"css": "sample.css",
|
||||
"js": "sample.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2) Plugin ABI (all languages)
|
||||
|
||||
Every plugin — regardless of language — must export **C-callable functions** with specific signatures. The ABI differs slightly between Windows and Unix.
|
||||
|
||||
### Required exports
|
||||
|
||||
| Export | Signature (Windows) | Signature (Linux / macOS) | Required |
|
||||
|--------------------|-----------------------------------------------------------------------|-----------------------------------------------------------------------------------|----------|
|
||||
| `PluginOnLoad` | `int PluginOnLoad(char* hostInfo, int hostInfoLen, uint64 callback)` | `int PluginOnLoad(char* hostInfo, int hostInfoLen, uintptr callback, uintptr ctx)` | Yes |
|
||||
| `PluginOnEvent` | `int PluginOnEvent(char* event, int eventLen, char* payload, int payloadLen)` | same | Yes |
|
||||
| `PluginOnUnload` | `void PluginOnUnload()` | same | Yes |
|
||||
| `PluginSetCallback`| `void PluginSetCallback(uint64 callback)` | — | Windows only |
|
||||
| `PluginGetRuntime` | `const char* PluginGetRuntime()` | same | Recommended |
|
||||
|
||||
### Host callback
|
||||
|
||||
The host passes a callback function pointer during `PluginOnLoad`. The plugin uses it to send events back to the agent.
|
||||
|
||||
**Windows** — The callback is a `__stdcall` function:
|
||||
|
||||
```c
|
||||
void __stdcall callback(const char *event, uintptr_t eventLen,
|
||||
const char *payload, uintptr_t payloadLen);
|
||||
```
|
||||
|
||||
**Linux / macOS** — The callback is a standard C function with an opaque context pointer:
|
||||
|
||||
```c
|
||||
void callback(uintptr_t ctx,
|
||||
const char *event, int eventLen,
|
||||
const char *payload, int payloadLen);
|
||||
```
|
||||
|
||||
The `ctx` value received in `PluginOnLoad` must be passed back as the first argument to every callback invocation.
|
||||
|
||||
### PluginGetRuntime (runtime detection)
|
||||
|
||||
If a plugin exports `PluginGetRuntime`, the host calls it after loading. It must return a pointer to a static, null-terminated string identifying the runtime:
|
||||
|
||||
| Return value | Meaning |
|
||||
|-------------|---------------------------------------|
|
||||
| `"c"` | Pure C plugin — fully unloadable |
|
||||
| `"cpp"` | C++ plugin — fully unloadable |
|
||||
| `"rust"` | Rust plugin — fully unloadable |
|
||||
| *(absent)* | Defaults to `"go"` — **not** freed |
|
||||
|
||||
This matters because **Go plugins cannot be fully unloaded** due to [golang/go#11100](https://github.com/golang/go/issues/11100) — Go's runtime spawns threads that cannot be stopped, so calling `FreeLibrary`/`dlclose` on a Go shared library causes a crash. The host uses this information to skip freeing Go plugins while properly reclaiming memory for C/C++/Rust plugins.
|
||||
|
||||
The runtime is logged on load and unload:
|
||||
```
|
||||
[plugins] loaded plugin "sample-c" (runtime=c, freeable=true)
|
||||
[plugins] unloaded plugin "sample-c" — memory freed
|
||||
[plugins] unloaded plugin "sample-go" — memory leaked (go runtime)
|
||||
```
|
||||
|
||||
### HostInfo JSON
|
||||
|
||||
The `hostInfo` buffer passed to `PluginOnLoad` is JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"clientId": "abc123",
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
### Loading mechanism
|
||||
|
||||
- **Windows** — DLLs are loaded **entirely in memory** via a custom PE loader. No files are written to disk. This is why Rust's `std::sync::Mutex` (backed by `SRWLOCK`) may not initialize correctly — see the Rust section below.
|
||||
- **Linux** — Shared libraries are loaded in memory via `memfd_create` + `dlopen` on `/proc/self/fd/`. No files touch disk.
|
||||
- **macOS** — Shared libraries are written to a temp file, `dlopen`'d, then the temp file is deleted.
|
||||
|
||||
## 3) Sample plugins by language
|
||||
|
||||
### Go (`sample-go/`)
|
||||
|
||||
Go plugins use `-buildmode=c-shared` with platform-specific export wrappers.
|
||||
|
||||
**Project structure:**
|
||||
```
|
||||
sample-go/native/
|
||||
├─ main.go (shared core logic)
|
||||
├─ exports_unix.go (exports for Linux/macOS)
|
||||
├─ exports_windows.go (exports for Windows)
|
||||
└─ go.mod
|
||||
```
|
||||
|
||||
**Linux / macOS exports:**
|
||||
```go
|
||||
//export PluginOnLoad
|
||||
func PluginOnLoad(hostInfo *C.char, hostInfoLen C.int, cb C.uintptr_t, ctx C.uintptr_t) C.int
|
||||
|
||||
//export PluginOnEvent
|
||||
func PluginOnEvent(event *C.char, eventLen C.int, payload *C.char, payloadLen C.int) C.int
|
||||
|
||||
//export PluginOnUnload
|
||||
func PluginOnUnload()
|
||||
```
|
||||
|
||||
**Windows exports:**
|
||||
```go
|
||||
//export PluginOnLoad
|
||||
func PluginOnLoad(hostInfo *C.char, hostInfoLen C.int, callbackPtr C.ulonglong) C.int
|
||||
|
||||
//export PluginOnEvent
|
||||
func PluginOnEvent(event *C.char, eventLen C.int, payload *C.char, payloadLen C.int) C.int
|
||||
|
||||
//export PluginOnUnload
|
||||
func PluginOnUnload()
|
||||
|
||||
//export PluginSetCallback
|
||||
func PluginSetCallback(callbackPtr C.ulonglong)
|
||||
```
|
||||
|
||||
**Build:**
|
||||
```bash
|
||||
CGO_ENABLED=1 go build -buildmode=c-shared -o sample-windows-amd64.dll ./native
|
||||
```
|
||||
|
||||
> **Note:** Go plugins do **not** export `PluginGetRuntime` and default to the `"go"` runtime. They are intentionally never freed on unload to prevent crashes from orphaned Go runtime threads.
|
||||
|
||||
---
|
||||
|
||||
### C (`sample-c/`)
|
||||
|
||||
Pure C plugins are the simplest — no runtime, no GC, fully unloadable.
|
||||
|
||||
**Project structure:**
|
||||
```
|
||||
sample-c/
|
||||
├─ native/plugin.c
|
||||
├─ sample-c.html
|
||||
├─ sample-c.css
|
||||
└─ sample-c.js
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- Uses `__declspec(dllexport)` on Windows, `__attribute__((visibility("default")))` on Unix
|
||||
- Has a `DllMain` entry point on Windows (receives `DLL_PROCESS_ATTACH` / `DLL_PROCESS_DETACH`)
|
||||
- Exports `PluginGetRuntime` → returns `"c"`
|
||||
- No heap allocations required — can use stack buffers for all responses
|
||||
|
||||
**Minimal example (Windows callback):**
|
||||
```c
|
||||
#ifdef _WIN32
|
||||
typedef void (__stdcall *host_callback_t)(
|
||||
const char *event, uintptr_t eventLen,
|
||||
const char *payload, uintptr_t payloadLen);
|
||||
#else
|
||||
typedef void (*host_callback_t)(
|
||||
uintptr_t ctx,
|
||||
const char *event, int eventLen,
|
||||
const char *payload, int payloadLen);
|
||||
#endif
|
||||
|
||||
static host_callback_t g_callback;
|
||||
|
||||
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, ...) {
|
||||
// Parse hostInfo JSON, store callback, return 0 on success
|
||||
}
|
||||
|
||||
EXPORT int PluginOnEvent(const char *event, int eventLen,
|
||||
const char *payload, int payloadLen) {
|
||||
// Handle events, call g_callback() to respond
|
||||
return 0;
|
||||
}
|
||||
|
||||
EXPORT void PluginOnUnload(void) {
|
||||
// Cleanup
|
||||
}
|
||||
|
||||
EXPORT const char *PluginGetRuntime(void) { return "c"; }
|
||||
```
|
||||
|
||||
**Build:**
|
||||
```bash
|
||||
# MSVC
|
||||
cl /LD /O2 plugin.c /Fe:sample-c-windows-amd64.dll
|
||||
|
||||
# MinGW / gcc
|
||||
gcc -shared -O2 -o sample-c-windows-amd64.dll plugin.c
|
||||
|
||||
# Linux
|
||||
gcc -shared -fPIC -O2 -o sample-c-linux-amd64.so plugin.c
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### C++ (`sample-cpp/`)
|
||||
|
||||
C++ plugins can use the full standard library (STL containers, `std::mutex`, `std::string`, etc.) while still being fully unloadable.
|
||||
|
||||
**Project structure:**
|
||||
```
|
||||
sample-cpp/
|
||||
├─ native/plugin.cpp
|
||||
├─ sample-cpp.html
|
||||
├─ sample-cpp.css
|
||||
└─ sample-cpp.js
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- All exports wrapped in `extern "C"` to prevent name mangling
|
||||
- Can freely use `std::string`, `std::unordered_map`, `std::mutex`, etc.
|
||||
- Exports `PluginGetRuntime` → returns `"cpp"`
|
||||
|
||||
**Build:**
|
||||
```bash
|
||||
# MSVC
|
||||
cl /LD /EHsc /O2 plugin.cpp /Fe:sample-cpp-windows-amd64.dll
|
||||
|
||||
# MinGW / g++
|
||||
g++ -shared -O2 -o sample-cpp-windows-amd64.dll plugin.cpp
|
||||
|
||||
# Linux
|
||||
g++ -shared -fPIC -O2 -o sample-cpp-linux-amd64.so plugin.cpp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Rust (`sample-rust/`)
|
||||
|
||||
Rust plugins compile as `cdylib` crates. Exports use `#[no_mangle]` and `extern "C"`.
|
||||
|
||||
**Project structure:**
|
||||
```
|
||||
sample-rust/
|
||||
├─ native/
|
||||
│ ├─ Cargo.toml
|
||||
│ └─ src/lib.rs
|
||||
├─ sample-rust.html
|
||||
├─ sample-rust.css
|
||||
└─ sample-rust.js
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- `crate-type = ["cdylib"]` in Cargo.toml
|
||||
- Uses `extern "stdcall"` for the callback type on Windows, `extern "C"` on Unix
|
||||
- Exports `PluginGetRuntime` → returns `"rust"`
|
||||
|
||||
**⚠️ Important: avoid `std::sync::Mutex` on Windows.** Because the DLL is loaded via an in-memory PE loader (not `LoadLibrary`), Rust's standard `Mutex` (backed by Windows `SRWLOCK`) may not initialize correctly, causing an access violation. Use C-style `static mut` globals instead — this is safe because the Go host serializes all calls through its own mutex.
|
||||
|
||||
**Cargo.toml:**
|
||||
```toml
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
strip = true
|
||||
```
|
||||
|
||||
**Build:**
|
||||
```bash
|
||||
# Native
|
||||
cargo build --release
|
||||
|
||||
# Cross-compile
|
||||
cargo build --release --target=x86_64-pc-windows-msvc
|
||||
cargo build --release --target=aarch64-unknown-linux-gnu
|
||||
```
|
||||
|
||||
## 4) Build scripts & cross-compilation
|
||||
|
||||
Each language has `.bat` (Windows) and `.sh` (Unix) build scripts. All scripts support the `BUILD_TARGETS` environment variable to compile for multiple platforms in one invocation.
|
||||
|
||||
### Available build scripts
|
||||
|
||||
| Language | Windows | Unix |
|
||||
|----------|--------------------------|-------------------------|
|
||||
| Go | `build-plugin.bat` | `build-plugin.sh` |
|
||||
| C | `build-plugin-c.bat` | `build-plugin-c.sh` |
|
||||
| C++ | `build-plugin-cpp.bat` | `build-plugin-cpp.sh` |
|
||||
| Rust | `build-plugin-rust.bat` | `build-plugin-rust.sh` |
|
||||
|
||||
### Usage
|
||||
|
||||
All scripts default to building for the host platform only:
|
||||
|
||||
```bash
|
||||
# Build for current platform
|
||||
./build-plugin-c.sh
|
||||
|
||||
# Build for multiple targets
|
||||
BUILD_TARGETS="linux-amd64 linux-arm64 windows-amd64" ./build-plugin-c.sh
|
||||
|
||||
# Custom plugin directory
|
||||
./build-plugin-c.sh /path/to/my-plugin
|
||||
```
|
||||
|
||||
```bat
|
||||
REM Build for current platform
|
||||
build-plugin-c.bat
|
||||
|
||||
REM Build for multiple targets
|
||||
set BUILD_TARGETS=windows-amd64 windows-arm64
|
||||
build-plugin-c.bat
|
||||
|
||||
REM Custom plugin directory
|
||||
build-plugin-c.bat C:\path\to\my-plugin
|
||||
```
|
||||
|
||||
### Cross-compilation
|
||||
|
||||
**Go** — Cross-compilation is built-in via `GOOS` / `GOARCH` env vars. No extra toolchains needed (unless CGo calls platform-specific APIs).
|
||||
|
||||
**Rust** — Cross-compilation uses `--target=` with Rust target triples. Install targets with `rustup target add <triple>`. The build scripts map `os-arch` pairs to triples automatically:
|
||||
|
||||
| Target | Rust triple (`.bat` / MSVC) | Rust triple (`.sh` / GNU) |
|
||||
|-----------------|-------------------------------------|---------------------------------------|
|
||||
| windows-amd64 | `x86_64-pc-windows-msvc` | `x86_64-pc-windows-gnu` |
|
||||
| windows-arm64 | `aarch64-pc-windows-msvc` | `aarch64-pc-windows-gnullvm` |
|
||||
| linux-amd64 | `x86_64-unknown-linux-gnu` | `x86_64-unknown-linux-gnu` |
|
||||
| linux-arm64 | `aarch64-unknown-linux-gnu` | `aarch64-unknown-linux-gnu` |
|
||||
| darwin-amd64 | `x86_64-apple-darwin` | `x86_64-apple-darwin` |
|
||||
| darwin-arm64 | `aarch64-apple-darwin` | `aarch64-apple-darwin` |
|
||||
|
||||
**C / C++** — Cross-compilation requires the appropriate cross-compiler toolchain to be installed:
|
||||
|
||||
| Target | C compiler | C++ compiler |
|
||||
|-----------------|-------------------------------|--------------------------------|
|
||||
| linux-amd64 | `x86_64-linux-gnu-gcc` | `x86_64-linux-gnu-g++` |
|
||||
| linux-arm64 | `aarch64-linux-gnu-gcc` | `aarch64-linux-gnu-g++` |
|
||||
| linux-arm | `arm-linux-gnueabihf-gcc` | `arm-linux-gnueabihf-g++` |
|
||||
| windows-amd64 | `x86_64-w64-mingw32-gcc` | `x86_64-w64-mingw32-g++` |
|
||||
| windows-arm64 | `aarch64-w64-mingw32-gcc` | `aarch64-w64-mingw32-g++` |
|
||||
|
||||
On Windows `.bat` scripts, MSVC (`cl`) is tried first with the appropriate `/link /machine:` flag, falling back to MinGW if `cl` fails.
|
||||
|
||||
On Unix `.sh` scripts, override the compiler with `CC=<compiler>` (C) or `CXX=<compiler>` (C++):
|
||||
|
||||
```bash
|
||||
CC=zig-cc BUILD_TARGETS="linux-arm64" ./build-plugin-c.sh
|
||||
CXX=zig-c++ BUILD_TARGETS="linux-arm64" ./build-plugin-cpp.sh
|
||||
```
|
||||
|
||||
### Installing cross-compiler toolchains
|
||||
|
||||
**Debian/Ubuntu:**
|
||||
```bash
|
||||
# ARM64 Linux cross-compilers
|
||||
sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
|
||||
|
||||
# Windows cross-compilers (MinGW)
|
||||
sudo apt install gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64
|
||||
|
||||
# Rust targets
|
||||
rustup target add aarch64-unknown-linux-gnu x86_64-pc-windows-gnu
|
||||
```
|
||||
|
||||
**macOS (Homebrew):**
|
||||
```bash
|
||||
# Linux cross-compilers via musl-cross
|
||||
brew install FiloSottile/musl-cross/musl-cross
|
||||
|
||||
# Rust targets
|
||||
rustup target add aarch64-apple-darwin x86_64-apple-darwin
|
||||
```
|
||||
|
||||
## 5) Install & open a plugin
|
||||
|
||||
### Install / upload
|
||||
|
||||
- Use the UI at `/plugins` to upload the zip
|
||||
- Or drop `<pluginId>.zip` into `Overlord-Server/plugins` and restart
|
||||
|
||||
### Open the UI
|
||||
|
||||
Plugin UI is served from:
|
||||
|
||||
```
|
||||
/plugins/<pluginId>?clientId=<CLIENT_ID>
|
||||
```
|
||||
|
||||
Your HTML loads its JS/CSS from `/plugins/<pluginId>/assets/`.
|
||||
|
||||
## 6) Runtime: how events flow
|
||||
|
||||
Overlord plugins have **two parts**:
|
||||
|
||||
1. **UI (HTML/CSS/JS)** — Runs in the browser, calls server APIs.
|
||||
2. **Native module** — Runs in the agent (client) process as a loaded shared library.
|
||||
|
||||
### UI → agent (plugin event)
|
||||
|
||||
From your UI JS:
|
||||
|
||||
```
|
||||
POST /api/clients/<clientId>/plugins/<pluginId>/event
|
||||
{
|
||||
"event": "ui_message",
|
||||
"payload": { "message": "hello" }
|
||||
}
|
||||
```
|
||||
|
||||
If the plugin is not loaded yet, the server will load it on the client, queue the event, and deliver it once ready.
|
||||
|
||||
### Agent → plugin (direct function call)
|
||||
|
||||
The agent calls your `OnEvent(event, payload)` function directly with JSON-encoded data. No stdin/stdout pipes, no msgpack — just a direct function call.
|
||||
|
||||
### Plugin → agent (callback)
|
||||
|
||||
Your plugin sends events back to the host using the callback received during `OnLoad`:
|
||||
|
||||
**Go:**
|
||||
```go
|
||||
send("echo", []byte(`{"message":"hello back"}`))
|
||||
```
|
||||
|
||||
**C / C++:**
|
||||
```c
|
||||
g_callback("echo", 4, "{\"message\":\"hello back\"}", 24);
|
||||
```
|
||||
|
||||
**Rust:**
|
||||
```rust
|
||||
let event = b"echo";
|
||||
let payload = b"{\"message\":\"hello back\"}";
|
||||
(g_callback)(event.as_ptr(), event.len(), payload.as_ptr(), payload.len());
|
||||
```
|
||||
|
||||
On Windows, the callback uses `__stdcall` calling convention. On Unix, it's a standard C call with the context pointer as the first argument.
|
||||
|
||||
### Plugin lifecycle events
|
||||
|
||||
The agent sends these events to the server:
|
||||
|
||||
- `loaded` on successful load
|
||||
- `unloaded` when unloaded
|
||||
- `error` if load or runtime fails
|
||||
|
||||
## 7) What can plugins do?
|
||||
|
||||
Since plugins run as native code, they can:
|
||||
|
||||
- Call any system API (file I/O, network, processes, etc.)
|
||||
- Use any library available to their language
|
||||
- Spawn threads / goroutines
|
||||
- Access hardware
|
||||
- Do anything a normal native program can do
|
||||
|
||||
Plugins have the same capabilities as the agent itself.
|
||||
|
||||
| Language | Can be fully unloaded? | Runtime overhead |
|
||||
|----------|----------------------|------------------|
|
||||
| C | Yes | None |
|
||||
| C++ | Yes | Minimal (STL) |
|
||||
| Rust | Yes | Minimal |
|
||||
| Go | **No** ([#11100](https://github.com/golang/go/issues/11100)) | Go runtime + GC |
|
||||
|
||||
### UI Security constraints
|
||||
|
||||
Plugin UI pages are still served with a tight CSP:
|
||||
|
||||
- Scripts must be same-origin
|
||||
- No third-party JS/CDN
|
||||
- WebSocket and fetch are allowed to same origin
|
||||
|
||||
Plugin UIs run in a **sandboxed iframe** with a fetch bridge.
|
||||
|
||||
## 8) API surface
|
||||
|
||||
### Plugin management
|
||||
|
||||
- `GET /api/plugins` — list installed plugins (includes `autoLoad` and `autoStartEvents` per plugin)
|
||||
- `POST /api/plugins/upload` — upload zip
|
||||
- `POST /api/plugins/<id>/enable` — enable/disable
|
||||
- `POST /api/plugins/<id>/autoload` — configure auto-load on client connect
|
||||
- `DELETE /api/plugins/<id>` — remove (preserves `data/` directory)
|
||||
|
||||
### Plugin data directory
|
||||
|
||||
- `GET /api/plugins/<id>/data` — list files in the plugin's persistent data directory
|
||||
- `GET /api/plugins/<id>/data/<path>` — read a file
|
||||
- `PUT /api/plugins/<id>/data/<path>` — write a file
|
||||
- `DELETE /api/plugins/<id>/data/<path>` — delete a file or directory
|
||||
- `POST /api/plugins/<id>/exec` — execute a stored binary (admin/operator only)
|
||||
|
||||
See [section 10](#10-server-side-plugin-data-directory) for full details.
|
||||
|
||||
### Per-client plugin runtime
|
||||
|
||||
- `POST /api/clients/<clientId>/plugins/<pluginId>/load`
|
||||
- `POST /api/clients/<clientId>/plugins/<pluginId>/event`
|
||||
- `POST /api/clients/<clientId>/plugins/<pluginId>/unload`
|
||||
|
||||
### Useful built-in endpoints
|
||||
|
||||
- `POST /api/clients/<clientId>/command`
|
||||
- `WS /api/clients/<clientId>/rd/ws` (remote desktop)
|
||||
- `WS /api/clients/<clientId>/console/ws`
|
||||
- `WS /api/clients/<clientId>/files/ws`
|
||||
- `WS /api/clients/<clientId>/processes/ws`
|
||||
|
||||
## 9) Auto-load plugins on client connect
|
||||
|
||||
By default, plugins are only loaded when manually triggered via the API or UI. For plugins that need to run 24/7 on every connected client (e.g. clipboard monitoring, keylogging, persistence), you can configure **auto-load**.
|
||||
|
||||
When auto-load is enabled for a plugin, the server will automatically load it onto every client that connects. If the client already has the plugin loaded, it's skipped — no duplicate loads.
|
||||
|
||||
You can also configure **auto-start events** — a list of events that are queued and delivered to the plugin immediately after it loads. This lets you pre-configure the plugin without any manual interaction.
|
||||
|
||||
### Enable auto-load
|
||||
|
||||
```
|
||||
POST /api/plugins/<pluginId>/autoload
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"autoLoad": true
|
||||
}
|
||||
```
|
||||
|
||||
### Enable auto-load with auto-start events
|
||||
|
||||
```
|
||||
POST /api/plugins/<pluginId>/autoload
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"autoLoad": true,
|
||||
"autoStartEvents": [
|
||||
{ "event": "add_rule", "payload": { "pattern": "^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$", "replacement": "your-btc-address" } },
|
||||
{ "event": "start", "payload": {} }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The events in `autoStartEvents` are queued in order and delivered to the plugin after it reports `loaded`. This works exactly like calling the event API multiple times, but happens automatically.
|
||||
|
||||
### Disable auto-load
|
||||
|
||||
```
|
||||
POST /api/plugins/<pluginId>/autoload
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"autoLoad": false
|
||||
}
|
||||
```
|
||||
|
||||
### How it works
|
||||
|
||||
1. Client connects and completes the enrollment handshake
|
||||
2. Server sends `hello_ack` (as usual)
|
||||
3. Server dispatches auto-scripts (as usual)
|
||||
4. Server checks all plugins with `autoLoad: true` and `enabled: true`
|
||||
5. For each, if the plugin is **not already loaded** on that client:
|
||||
- Sends the plugin binary bundle (chunked)
|
||||
- Queues any `autoStartEvents`
|
||||
6. When the plugin reports `loaded`, queued events are flushed in order
|
||||
|
||||
### Checking auto-load status
|
||||
|
||||
`GET /api/plugins` returns `autoLoad` and `autoStartEvents` for each plugin:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": [
|
||||
{
|
||||
"id": "clipreplace",
|
||||
"name": "clipreplace",
|
||||
"enabled": true,
|
||||
"autoLoad": true,
|
||||
"autoStartEvents": [
|
||||
{ "event": "add_rule", "payload": { "pattern": "...", "replacement": "..." } },
|
||||
{ "event": "start", "payload": {} }
|
||||
],
|
||||
"lastError": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- Auto-load respects the `enabled` flag — disabled plugins are never auto-loaded
|
||||
- Auto-load state is persisted in `.plugin-state.json` and survives server restarts
|
||||
- Deleting a plugin also removes its auto-load configuration
|
||||
- The server selects the correct binary for each client's OS/architecture automatically
|
||||
- If a plugin binary isn't available for a client's platform, the auto-load silently skips that client
|
||||
|
||||
## 10) Server-side plugin data directory
|
||||
|
||||
Each plugin has a **persistent data directory** on the server:
|
||||
|
||||
```
|
||||
Overlord-Server/plugins/<pluginId>/data/
|
||||
```
|
||||
|
||||
This directory is **never deleted** when a plugin is removed or reinstalled. It survives the entire plugin lifecycle and gives server-side plugins a dedicated place to store files — SQLite databases, config files, cached data, executables, etc.
|
||||
|
||||
### Read/write files
|
||||
|
||||
From your plugin UI JS (or any authenticated API consumer):
|
||||
|
||||
**List all files**
|
||||
```
|
||||
GET /api/plugins/<pluginId>/data
|
||||
```
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"files": [
|
||||
{ "path": "config.json", "size": 128, "isDir": false },
|
||||
{ "path": "cache", "size": 0, "isDir": true },
|
||||
{ "path": "cache/data.db", "size": 40960, "isDir": false }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Read a file**
|
||||
```
|
||||
GET /api/plugins/<pluginId>/data/<path>
|
||||
```
|
||||
Returns the raw file bytes with an appropriate `Content-Type`.
|
||||
|
||||
**Write a file**
|
||||
```
|
||||
PUT /api/plugins/<pluginId>/data/<path>
|
||||
Content-Type: application/octet-stream
|
||||
<raw bytes>
|
||||
```
|
||||
Parent directories are created automatically.
|
||||
```json
|
||||
{ "ok": true, "path": "config.json", "size": 128 }
|
||||
```
|
||||
|
||||
**Delete a file or directory**
|
||||
```
|
||||
DELETE /api/plugins/<pluginId>/data/<path>
|
||||
```
|
||||
Deleting a directory removes it recursively.
|
||||
```json
|
||||
{ "ok": true, "path": "cache" }
|
||||
```
|
||||
|
||||
### Execute a stored binary
|
||||
|
||||
Plugins can store executables in their data directory and run them on the server. This endpoint is restricted to **admin and operator** roles.
|
||||
|
||||
```
|
||||
POST /api/plugins/<pluginId>/exec
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"file": "mytool",
|
||||
"args": ["--flag", "value"],
|
||||
"stdin": "optional stdin string",
|
||||
"timeoutMs": 10000
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `file` | string | Path to the binary, relative to the plugin's `data/` directory |
|
||||
| `args` | string[] | Command-line arguments (optional) |
|
||||
| `stdin` | string | Text to pipe to stdin (optional) |
|
||||
| `timeoutMs` | number | Max run time in ms, max 60000, default 30000 |
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"exitCode": 0,
|
||||
"stdout": "tool output here",
|
||||
"stderr": ""
|
||||
}
|
||||
```
|
||||
|
||||
The binary is run from the `data/` directory as the working directory. The server sets the executable bit automatically before running on Unix.
|
||||
|
||||
### Example: storing a SQLite database from plugin JS
|
||||
|
||||
```js
|
||||
// Write a new database seed file
|
||||
await fetch(`/api/plugins/myplugin/data/app.db`, {
|
||||
method: "PUT",
|
||||
body: dbBytes, // ArrayBuffer
|
||||
});
|
||||
|
||||
// Read it back
|
||||
const res = await fetch(`/api/plugins/myplugin/data/app.db`);
|
||||
const db = await res.arrayBuffer();
|
||||
|
||||
// List all stored files
|
||||
const { files } = await (await fetch(`/api/plugins/myplugin/data`)).json();
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- Path traversal is blocked — paths may not escape the `data/` directory
|
||||
- Null bytes in paths are rejected
|
||||
- The data directory is created automatically on first use (no pre-setup needed)
|
||||
- Deleting and reinstalling a plugin leaves `data/` untouched
|
||||
|
||||
---
|
||||
|
||||
## Plugin Signing
|
||||
|
||||
Plugins can be cryptographically signed with Ed25519 keys. The server verifies signatures on upload and displays trust status in the dashboard. Loading unsigned or untrusted plugins requires explicit confirmation.
|
||||
|
||||
### Trust Levels
|
||||
|
||||
| Status | Badge | Behavior |
|
||||
|--------|-------|----------|
|
||||
| **Signed + Trusted** | Green shield | Loads immediately |
|
||||
| **Signed + Untrusted** | Yellow shield | Must type "confirm" to load |
|
||||
| **Unsigned** | Orange shield | Must type "confirm" to load |
|
||||
| **Invalid Signature** | Red shield | Blocked — cannot load |
|
||||
|
||||
### Generate a Signing Key
|
||||
|
||||
```bash
|
||||
cd Overlord-Server
|
||||
bun run scripts/plugin-keygen.ts --out my-signing-key
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `my-signing-key.key` — private key (keep secret!)
|
||||
- `my-signing-key.pub` — public key + fingerprint
|
||||
|
||||
The fingerprint (64-char hex SHA-256) is printed to the console. Add it to your server config to trust plugins signed with this key.
|
||||
|
||||
### Sign a Plugin
|
||||
|
||||
```bash
|
||||
cd Overlord-Server
|
||||
bun run scripts/plugin-sign.ts --key my-signing-key.key ../plugins/sample-go/sample.zip
|
||||
```
|
||||
|
||||
This injects a `signature.json` into the ZIP containing the Ed25519 public key and signature.
|
||||
|
||||
### Add a Trusted Key
|
||||
|
||||
**Via config.json:**
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"trustedKeys": [
|
||||
"a1b2c3d4e5f6...64-char-hex-fingerprint..."
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Via environment variable:**
|
||||
```
|
||||
TRUSTED_PLUGIN_KEYS=fingerprint1,fingerprint2
|
||||
```
|
||||
|
||||
**Via the web UI:** Go to the Plugins page → Trusted Signing Keys section → paste the fingerprint and click Add Key.
|
||||
|
||||
**Via API:**
|
||||
```bash
|
||||
# Add a trusted key
|
||||
curl -X POST /api/plugins/trusted-keys \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"fingerprint": "a1b2c3..."}'
|
||||
|
||||
# List trusted keys
|
||||
curl /api/plugins/trusted-keys
|
||||
|
||||
# Remove a trusted key
|
||||
curl -X DELETE /api/plugins/trusted-keys/a1b2c3...
|
||||
```
|
||||
|
||||
### Build Script Integration
|
||||
|
||||
Set the `PLUGIN_SIGN_KEY` environment variable to automatically sign plugins during build:
|
||||
|
||||
```bash
|
||||
# Unix
|
||||
PLUGIN_SIGN_KEY=path/to/my-signing-key.key ./build-plugin.sh
|
||||
|
||||
# Windows
|
||||
set PLUGIN_SIGN_KEY=path\to\my-signing-key.key
|
||||
build-plugin.bat
|
||||
```
|
||||
|
||||
### How Signing Works
|
||||
|
||||
1. The canonical content digest is computed by hashing every file in the ZIP (excluding `signature.json`), sorting filenames alphabetically, and concatenating `filename:sha256hex\n` for each
|
||||
2. The digest is signed with Ed25519 using the private key
|
||||
3. The signature, public key, and algorithm are stored as `signature.json` inside the ZIP
|
||||
4. On upload, the server extracts `signature.json`, recomputes the digest, and verifies the signature using `crypto.subtle.verify("Ed25519", ...)`
|
||||
5. The signer's fingerprint (`hex(SHA-256(raw_public_key))`) is compared against `plugins.trustedKeys` in the config
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set "ROOT=%~dp0"
|
||||
set "PLUGIN_DIR=%ROOT%sample-c"
|
||||
if not "%~1"=="" set "PLUGIN_DIR=%~1"
|
||||
|
||||
set "NATIVE_DIR=%PLUGIN_DIR%\native"
|
||||
set "PLUGIN_NAME=sample-c"
|
||||
set "ZIP_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.zip"
|
||||
|
||||
if not exist "%NATIVE_DIR%\plugin.c" (
|
||||
echo [error] native\plugin.c not found in %NATIVE_DIR%
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build targets - default to windows-amd64 on Windows
|
||||
if not defined BUILD_TARGETS set "BUILD_TARGETS=windows-amd64"
|
||||
|
||||
REM Detect host architecture
|
||||
set "HOST_ARCH=amd64"
|
||||
if "%PROCESSOR_ARCHITECTURE%"=="ARM64" set "HOST_ARCH=arm64"
|
||||
|
||||
set "BUILT_FILES="
|
||||
for %%T in (%BUILD_TARGETS%) do (
|
||||
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
|
||||
set "TARGET_OS=%%A"
|
||||
set "TARGET_ARCH=%%B"
|
||||
)
|
||||
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
set "EXT=dll"
|
||||
) else if "!TARGET_OS!"=="darwin" (
|
||||
set "EXT=dylib"
|
||||
) else (
|
||||
set "EXT=so"
|
||||
)
|
||||
set "OUTFILE=%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.!EXT!"
|
||||
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
REM Pick cross-compiler flags for MSVC or gcc
|
||||
set "CL_MACHINE="
|
||||
set "GCC_CMD=gcc"
|
||||
if "!TARGET_ARCH!"=="arm64" (
|
||||
set "CL_MACHINE=/machine:ARM64"
|
||||
set "GCC_CMD=aarch64-w64-mingw32-gcc"
|
||||
) else if "!TARGET_ARCH!"=="amd64" (
|
||||
set "CL_MACHINE=/machine:X64"
|
||||
set "GCC_CMD=x86_64-w64-mingw32-gcc"
|
||||
)
|
||||
|
||||
echo [build] cl /LD /O2 "%NATIVE_DIR%\plugin.c" /Fe:"!OUTFILE!" /link !CL_MACHINE!
|
||||
cl /LD /O2 "%NATIVE_DIR%\plugin.c" /Fe:"!OUTFILE!" /link !CL_MACHINE! >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [build] cl failed, trying !GCC_CMD!...
|
||||
!GCC_CMD! -shared -O2 -o "!OUTFILE!" "%NATIVE_DIR%\plugin.c"
|
||||
if errorlevel 1 (
|
||||
echo [error] build failed for !TARGET_OS!-!TARGET_ARCH!
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
) else (
|
||||
REM Cross-compile for non-Windows targets
|
||||
set "GCC_CMD=gcc"
|
||||
if "!TARGET_OS!"=="linux" (
|
||||
if "!TARGET_ARCH!"=="arm64" set "GCC_CMD=aarch64-linux-gnu-gcc"
|
||||
if "!TARGET_ARCH!"=="amd64" set "GCC_CMD=x86_64-linux-gnu-gcc"
|
||||
)
|
||||
echo [build] !GCC_CMD! -shared -fPIC -O2 -o "!OUTFILE!" "%NATIVE_DIR%\plugin.c"
|
||||
!GCC_CMD! -shared -fPIC -O2 -o "!OUTFILE!" "%NATIVE_DIR%\plugin.c"
|
||||
if errorlevel 1 (
|
||||
echo [error] build failed for !TARGET_OS!-!TARGET_ARCH!
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
set "BUILT_FILES=!BUILT_FILES! '%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.!EXT!'"
|
||||
)
|
||||
|
||||
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
|
||||
|
||||
REM Collect all build outputs and web assets
|
||||
set "ZIP_SOURCES="
|
||||
for %%T in (%BUILD_TARGETS%) do (
|
||||
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
|
||||
set "TARGET_OS=%%A"
|
||||
set "TARGET_ARCH=%%B"
|
||||
)
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dll'"
|
||||
) else if "!TARGET_OS!"=="darwin" (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dylib'"
|
||||
) else (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.so'"
|
||||
)
|
||||
)
|
||||
|
||||
REM Add web assets
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.html" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.html'"
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.css" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.css'"
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.js" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.js'"
|
||||
|
||||
REM Remove leading comma
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES:~1!"
|
||||
|
||||
powershell -NoProfile -Command "Compress-Archive -Path !ZIP_SOURCES! -DestinationPath '%ZIP_OUT%'"
|
||||
if errorlevel 1 (
|
||||
echo [error] zip failed
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [ok] %ZIP_OUT%
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PLUGIN_DIR="${1:-${ROOT_DIR}/sample-c}"
|
||||
NATIVE_DIR="${PLUGIN_DIR}/native"
|
||||
PLUGIN_NAME="sample-c"
|
||||
ZIP_OUT="${PLUGIN_DIR}/${PLUGIN_NAME}.zip"
|
||||
|
||||
if [[ ! -f "${NATIVE_DIR}/plugin.c" ]]; then
|
||||
echo "[error] native/plugin.c not found in ${NATIVE_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect current OS/arch
|
||||
HOST_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
HOST_ARCH="$(uname -m)"
|
||||
case "${HOST_ARCH}" in
|
||||
x86_64) HOST_ARCH="amd64" ;;
|
||||
aarch64) HOST_ARCH="arm64" ;;
|
||||
arm64) HOST_ARCH="arm64" ;;
|
||||
esac
|
||||
case "${HOST_OS}" in
|
||||
linux) HOST_OS="linux" ;;
|
||||
darwin) HOST_OS="darwin" ;;
|
||||
esac
|
||||
|
||||
DEFAULT_TARGETS="${HOST_OS}-${HOST_ARCH}"
|
||||
BUILD_TARGETS="${BUILD_TARGETS:-${DEFAULT_TARGETS}}"
|
||||
|
||||
# Resolve the C compiler for a given target os-arch pair.
|
||||
# Override with CC=<compiler> for custom toolchains.
|
||||
resolve_cc() {
|
||||
local t_os="$1" t_arch="$2"
|
||||
|
||||
# Honour explicit CC override
|
||||
if [[ -n "${CC:-}" ]]; then echo "${CC}"; return; fi
|
||||
|
||||
# Native compilation
|
||||
if [[ "${t_os}" == "${HOST_OS}" && "${t_arch}" == "${HOST_ARCH}" ]]; then
|
||||
echo "gcc"; return
|
||||
fi
|
||||
|
||||
# Cross-compilation
|
||||
case "${t_os}-${t_arch}" in
|
||||
linux-amd64) echo "x86_64-linux-gnu-gcc" ;;
|
||||
linux-arm64) echo "aarch64-linux-gnu-gcc" ;;
|
||||
linux-arm) echo "arm-linux-gnueabihf-gcc" ;;
|
||||
windows-amd64) echo "x86_64-w64-mingw32-gcc" ;;
|
||||
windows-arm64) echo "aarch64-w64-mingw32-gcc" ;;
|
||||
darwin-amd64) echo "x86_64-apple-darwin-gcc" ;;
|
||||
darwin-arm64) echo "aarch64-apple-darwin-gcc" ;;
|
||||
*) echo "gcc" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
BUILT_FILES=()
|
||||
|
||||
for target in ${BUILD_TARGETS}; do
|
||||
os="${target%%-*}"
|
||||
arch="${target#*-}"
|
||||
|
||||
if [[ "${os}" == "windows" ]]; then
|
||||
ext="dll"
|
||||
elif [[ "${os}" == "darwin" ]]; then
|
||||
ext="dylib"
|
||||
else
|
||||
ext="so"
|
||||
fi
|
||||
|
||||
cc="$(resolve_cc "${os}" "${arch}")"
|
||||
if ! command -v "${cc}" >/dev/null 2>&1; then
|
||||
echo "[error] cross-compiler '${cc}' not found for ${os}-${arch}. Install it or set CC=<compiler>." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
outfile="${PLUGIN_DIR}/${PLUGIN_NAME}-${os}-${arch}.${ext}"
|
||||
echo "[build] ${cc} -shared -fPIC -O2 -o ${outfile} ${NATIVE_DIR}/plugin.c"
|
||||
${cc} -shared -fPIC -O2 -o "${outfile}" "${NATIVE_DIR}/plugin.c"
|
||||
BUILT_FILES+=("${PLUGIN_NAME}-${os}-${arch}.${ext}")
|
||||
done
|
||||
|
||||
rm -f "${ZIP_OUT}"
|
||||
|
||||
# Collect files to zip
|
||||
ZIP_FILES=()
|
||||
for bf in "${BUILT_FILES[@]}"; do
|
||||
ZIP_FILES+=("${bf}")
|
||||
done
|
||||
for asset in "${PLUGIN_NAME}.html" "${PLUGIN_NAME}.css" "${PLUGIN_NAME}.js"; do
|
||||
if [[ -f "${PLUGIN_DIR}/${asset}" ]]; then
|
||||
ZIP_FILES+=("${asset}")
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v zip >/dev/null 2>&1; then
|
||||
(cd "${PLUGIN_DIR}" && zip -q "${ZIP_OUT}" "${ZIP_FILES[@]}")
|
||||
else
|
||||
echo "[error] zip not found. Please install zip." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[ok] ${ZIP_OUT}"
|
||||
@@ -0,0 +1,111 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set "ROOT=%~dp0"
|
||||
set "PLUGIN_DIR=%ROOT%sample-cpp"
|
||||
if not "%~1"=="" set "PLUGIN_DIR=%~1"
|
||||
|
||||
set "NATIVE_DIR=%PLUGIN_DIR%\native"
|
||||
set "PLUGIN_NAME=sample-cpp"
|
||||
set "ZIP_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.zip"
|
||||
|
||||
if not exist "%NATIVE_DIR%\plugin.cpp" (
|
||||
echo [error] native\plugin.cpp not found in %NATIVE_DIR%
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build targets - default to windows-amd64 on Windows
|
||||
if not defined BUILD_TARGETS set "BUILD_TARGETS=windows-amd64"
|
||||
|
||||
REM Detect host architecture
|
||||
set "HOST_ARCH=amd64"
|
||||
if "%PROCESSOR_ARCHITECTURE%"=="ARM64" set "HOST_ARCH=arm64"
|
||||
|
||||
set "BUILT_FILES="
|
||||
for %%T in (%BUILD_TARGETS%) do (
|
||||
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
|
||||
set "TARGET_OS=%%A"
|
||||
set "TARGET_ARCH=%%B"
|
||||
)
|
||||
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
set "EXT=dll"
|
||||
) else if "!TARGET_OS!"=="darwin" (
|
||||
set "EXT=dylib"
|
||||
) else (
|
||||
set "EXT=so"
|
||||
)
|
||||
set "OUTFILE=%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.!EXT!"
|
||||
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
REM Pick cross-compiler flags for MSVC or g++
|
||||
set "CL_MACHINE="
|
||||
set "GXX_CMD=g++"
|
||||
if "!TARGET_ARCH!"=="arm64" (
|
||||
set "CL_MACHINE=/machine:ARM64"
|
||||
set "GXX_CMD=aarch64-w64-mingw32-g++"
|
||||
) else if "!TARGET_ARCH!"=="amd64" (
|
||||
set "CL_MACHINE=/machine:X64"
|
||||
set "GXX_CMD=x86_64-w64-mingw32-g++"
|
||||
)
|
||||
|
||||
echo [build] cl /LD /EHsc /O2 "%NATIVE_DIR%\plugin.cpp" /Fe:"!OUTFILE!" /link !CL_MACHINE!
|
||||
cl /LD /EHsc /O2 "%NATIVE_DIR%\plugin.cpp" /Fe:"!OUTFILE!" /link !CL_MACHINE! >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [build] cl failed, trying !GXX_CMD!...
|
||||
!GXX_CMD! -shared -O2 -o "!OUTFILE!" "%NATIVE_DIR%\plugin.cpp"
|
||||
if errorlevel 1 (
|
||||
echo [error] build failed for !TARGET_OS!-!TARGET_ARCH!
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
) else (
|
||||
REM Cross-compile for non-Windows targets
|
||||
set "GXX_CMD=g++"
|
||||
if "!TARGET_OS!"=="linux" (
|
||||
if "!TARGET_ARCH!"=="arm64" set "GXX_CMD=aarch64-linux-gnu-g++"
|
||||
if "!TARGET_ARCH!"=="amd64" set "GXX_CMD=x86_64-linux-gnu-g++"
|
||||
)
|
||||
echo [build] !GXX_CMD! -shared -fPIC -O2 -o "!OUTFILE!" "%NATIVE_DIR%\plugin.cpp"
|
||||
!GXX_CMD! -shared -fPIC -O2 -o "!OUTFILE!" "%NATIVE_DIR%\plugin.cpp"
|
||||
if errorlevel 1 (
|
||||
echo [error] build failed for !TARGET_OS!-!TARGET_ARCH!
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
set "BUILT_FILES=!BUILT_FILES! '%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.!EXT!'"
|
||||
)
|
||||
|
||||
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
|
||||
|
||||
REM Collect all build outputs and web assets
|
||||
set "ZIP_SOURCES="
|
||||
for %%T in (%BUILD_TARGETS%) do (
|
||||
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
|
||||
set "TARGET_OS=%%A"
|
||||
set "TARGET_ARCH=%%B"
|
||||
)
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dll'"
|
||||
) else if "!TARGET_OS!"=="darwin" (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dylib'"
|
||||
) else (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.so'"
|
||||
)
|
||||
)
|
||||
|
||||
REM Add web assets
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.html" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.html'"
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.css" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.css'"
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.js" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.js'"
|
||||
|
||||
REM Remove leading comma
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES:~1!"
|
||||
|
||||
powershell -NoProfile -Command "Compress-Archive -Path !ZIP_SOURCES! -DestinationPath '%ZIP_OUT%'"
|
||||
if errorlevel 1 (
|
||||
echo [error] zip failed
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [ok] %ZIP_OUT%
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PLUGIN_DIR="${1:-${ROOT_DIR}/sample-cpp}"
|
||||
NATIVE_DIR="${PLUGIN_DIR}/native"
|
||||
PLUGIN_NAME="sample-cpp"
|
||||
ZIP_OUT="${PLUGIN_DIR}/${PLUGIN_NAME}.zip"
|
||||
|
||||
if [[ ! -f "${NATIVE_DIR}/plugin.cpp" ]]; then
|
||||
echo "[error] native/plugin.cpp not found in ${NATIVE_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect current OS/arch
|
||||
HOST_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
HOST_ARCH="$(uname -m)"
|
||||
case "${HOST_ARCH}" in
|
||||
x86_64) HOST_ARCH="amd64" ;;
|
||||
aarch64) HOST_ARCH="arm64" ;;
|
||||
arm64) HOST_ARCH="arm64" ;;
|
||||
esac
|
||||
case "${HOST_OS}" in
|
||||
linux) HOST_OS="linux" ;;
|
||||
darwin) HOST_OS="darwin" ;;
|
||||
esac
|
||||
|
||||
DEFAULT_TARGETS="${HOST_OS}-${HOST_ARCH}"
|
||||
BUILD_TARGETS="${BUILD_TARGETS:-${DEFAULT_TARGETS}}"
|
||||
|
||||
# Resolve the C++ compiler for a given target os-arch pair.
|
||||
# Override with CXX=<compiler> for custom toolchains.
|
||||
resolve_cxx() {
|
||||
local t_os="$1" t_arch="$2"
|
||||
|
||||
# Honour explicit CXX override
|
||||
if [[ -n "${CXX:-}" ]]; then echo "${CXX}"; return; fi
|
||||
|
||||
# Native compilation
|
||||
if [[ "${t_os}" == "${HOST_OS}" && "${t_arch}" == "${HOST_ARCH}" ]]; then
|
||||
echo "g++"; return
|
||||
fi
|
||||
|
||||
# Cross-compilation
|
||||
case "${t_os}-${t_arch}" in
|
||||
linux-amd64) echo "x86_64-linux-gnu-g++" ;;
|
||||
linux-arm64) echo "aarch64-linux-gnu-g++" ;;
|
||||
linux-arm) echo "arm-linux-gnueabihf-g++" ;;
|
||||
windows-amd64) echo "x86_64-w64-mingw32-g++" ;;
|
||||
windows-arm64) echo "aarch64-w64-mingw32-g++" ;;
|
||||
darwin-amd64) echo "x86_64-apple-darwin-g++" ;;
|
||||
darwin-arm64) echo "aarch64-apple-darwin-g++" ;;
|
||||
*) echo "g++" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
BUILT_FILES=()
|
||||
|
||||
for target in ${BUILD_TARGETS}; do
|
||||
os="${target%%-*}"
|
||||
arch="${target#*-}"
|
||||
|
||||
if [[ "${os}" == "windows" ]]; then
|
||||
ext="dll"
|
||||
elif [[ "${os}" == "darwin" ]]; then
|
||||
ext="dylib"
|
||||
else
|
||||
ext="so"
|
||||
fi
|
||||
|
||||
cxx="$(resolve_cxx "${os}" "${arch}")"
|
||||
if ! command -v "${cxx}" >/dev/null 2>&1; then
|
||||
echo "[error] cross-compiler '${cxx}' not found for ${os}-${arch}. Install it or set CXX=<compiler>." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
outfile="${PLUGIN_DIR}/${PLUGIN_NAME}-${os}-${arch}.${ext}"
|
||||
echo "[build] ${cxx} -shared -fPIC -O2 -o ${outfile} ${NATIVE_DIR}/plugin.cpp"
|
||||
${cxx} -shared -fPIC -O2 -o "${outfile}" "${NATIVE_DIR}/plugin.cpp"
|
||||
BUILT_FILES+=("${PLUGIN_NAME}-${os}-${arch}.${ext}")
|
||||
done
|
||||
|
||||
rm -f "${ZIP_OUT}"
|
||||
|
||||
# Collect files to zip
|
||||
ZIP_FILES=()
|
||||
for bf in "${BUILT_FILES[@]}"; do
|
||||
ZIP_FILES+=("${bf}")
|
||||
done
|
||||
for asset in "${PLUGIN_NAME}.html" "${PLUGIN_NAME}.css" "${PLUGIN_NAME}.js"; do
|
||||
if [[ -f "${PLUGIN_DIR}/${asset}" ]]; then
|
||||
ZIP_FILES+=("${asset}")
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v zip >/dev/null 2>&1; then
|
||||
(cd "${PLUGIN_DIR}" && zip -q "${ZIP_OUT}" "${ZIP_FILES[@]}")
|
||||
else
|
||||
echo "[error] zip not found. Please install zip." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[ok] ${ZIP_OUT}"
|
||||
@@ -0,0 +1,100 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set "ROOT=%~dp0"
|
||||
set "PLUGIN_DIR=%ROOT%sample-rust"
|
||||
if not "%~1"=="" set "PLUGIN_DIR=%~1"
|
||||
|
||||
set "NATIVE_DIR=%PLUGIN_DIR%\native"
|
||||
set "PLUGIN_NAME=sample-rust"
|
||||
set "ZIP_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.zip"
|
||||
|
||||
if not exist "%NATIVE_DIR%\Cargo.toml" (
|
||||
echo [error] native\Cargo.toml not found in %NATIVE_DIR%
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build targets - default to windows-amd64 on Windows
|
||||
if not defined BUILD_TARGETS set "BUILD_TARGETS=windows-amd64"
|
||||
|
||||
set "BUILT_FILES="
|
||||
for %%T in (%BUILD_TARGETS%) do (
|
||||
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
|
||||
set "TARGET_OS=%%A"
|
||||
set "TARGET_ARCH=%%B"
|
||||
)
|
||||
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
set "EXT=dll"
|
||||
set "RUST_TARGET=x86_64-pc-windows-msvc"
|
||||
if "!TARGET_ARCH!"=="arm64" set "RUST_TARGET=aarch64-pc-windows-msvc"
|
||||
) else if "!TARGET_OS!"=="darwin" (
|
||||
set "EXT=dylib"
|
||||
set "RUST_TARGET=x86_64-apple-darwin"
|
||||
if "!TARGET_ARCH!"=="arm64" set "RUST_TARGET=aarch64-apple-darwin"
|
||||
) else (
|
||||
set "EXT=so"
|
||||
set "RUST_TARGET=x86_64-unknown-linux-gnu"
|
||||
if "!TARGET_ARCH!"=="arm64" set "RUST_TARGET=aarch64-unknown-linux-gnu"
|
||||
)
|
||||
|
||||
set "OUTFILE=%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.!EXT!"
|
||||
|
||||
echo [build] cargo build --release --target=!RUST_TARGET! in %NATIVE_DIR%
|
||||
pushd "%NATIVE_DIR%"
|
||||
cargo build --release --target=!RUST_TARGET!
|
||||
if errorlevel 1 (
|
||||
echo [error] build failed for !TARGET_OS!-!TARGET_ARCH!
|
||||
popd
|
||||
exit /b 1
|
||||
)
|
||||
popd
|
||||
|
||||
REM Cargo outputs to target/<triple>/release/ — find the cdylib
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
copy /Y "%NATIVE_DIR%\target\!RUST_TARGET!\release\sample_rust.dll" "!OUTFILE!" >nul
|
||||
) else if "!TARGET_OS!"=="darwin" (
|
||||
copy /Y "%NATIVE_DIR%\target\!RUST_TARGET!\release\libsample_rust.dylib" "!OUTFILE!" >nul
|
||||
) else (
|
||||
copy /Y "%NATIVE_DIR%\target\!RUST_TARGET!\release\libsample_rust.so" "!OUTFILE!" >nul
|
||||
)
|
||||
if errorlevel 1 (
|
||||
echo [error] copy failed for !TARGET_OS!-!TARGET_ARCH!
|
||||
exit /b 1
|
||||
)
|
||||
set "BUILT_FILES=!BUILT_FILES! '!OUTFILE!'"
|
||||
)
|
||||
|
||||
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
|
||||
|
||||
REM Collect all build outputs and web assets
|
||||
set "ZIP_SOURCES="
|
||||
for %%T in (%BUILD_TARGETS%) do (
|
||||
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
|
||||
set "TARGET_OS=%%A"
|
||||
set "TARGET_ARCH=%%B"
|
||||
)
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dll'"
|
||||
) else if "!TARGET_OS!"=="darwin" (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dylib'"
|
||||
) else (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.so'"
|
||||
)
|
||||
)
|
||||
|
||||
REM Add web assets
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.html" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.html'"
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.css" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.css'"
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.js" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.js'"
|
||||
|
||||
REM Remove leading comma
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES:~1!"
|
||||
|
||||
powershell -NoProfile -Command "Compress-Archive -Path !ZIP_SOURCES! -DestinationPath '%ZIP_OUT%'"
|
||||
if errorlevel 1 (
|
||||
echo [error] zip failed
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [ok] %ZIP_OUT%
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PLUGIN_DIR="${1:-${ROOT_DIR}/sample-rust}"
|
||||
NATIVE_DIR="${PLUGIN_DIR}/native"
|
||||
PLUGIN_NAME="sample-rust"
|
||||
ZIP_OUT="${PLUGIN_DIR}/${PLUGIN_NAME}.zip"
|
||||
|
||||
if [[ ! -f "${NATIVE_DIR}/Cargo.toml" ]]; then
|
||||
echo "[error] native/Cargo.toml not found in ${NATIVE_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect current OS/arch
|
||||
HOST_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
HOST_ARCH="$(uname -m)"
|
||||
case "${HOST_ARCH}" in
|
||||
x86_64) HOST_ARCH="amd64" ;;
|
||||
aarch64) HOST_ARCH="arm64" ;;
|
||||
arm64) HOST_ARCH="arm64" ;;
|
||||
esac
|
||||
case "${HOST_OS}" in
|
||||
linux) HOST_OS="linux" ;;
|
||||
darwin) HOST_OS="darwin" ;;
|
||||
esac
|
||||
|
||||
DEFAULT_TARGETS="${HOST_OS}-${HOST_ARCH}"
|
||||
BUILD_TARGETS="${BUILD_TARGETS:-${DEFAULT_TARGETS}}"
|
||||
|
||||
BUILT_FILES=()
|
||||
|
||||
for target in ${BUILD_TARGETS}; do
|
||||
os="${target%%-*}"
|
||||
arch="${target#*-}"
|
||||
|
||||
if [[ "${os}" == "windows" ]]; then
|
||||
ext="dll"
|
||||
rust_target="x86_64-pc-windows-gnu"
|
||||
[[ "${arch}" == "arm64" ]] && rust_target="aarch64-pc-windows-gnullvm"
|
||||
lib_name="sample_rust.dll"
|
||||
elif [[ "${os}" == "darwin" ]]; then
|
||||
ext="dylib"
|
||||
rust_target="x86_64-apple-darwin"
|
||||
[[ "${arch}" == "arm64" ]] && rust_target="aarch64-apple-darwin"
|
||||
lib_name="libsample_rust.dylib"
|
||||
else
|
||||
ext="so"
|
||||
rust_target="x86_64-unknown-linux-gnu"
|
||||
[[ "${arch}" == "arm64" ]] && rust_target="aarch64-unknown-linux-gnu"
|
||||
lib_name="libsample_rust.so"
|
||||
fi
|
||||
|
||||
outfile="${PLUGIN_DIR}/${PLUGIN_NAME}-${os}-${arch}.${ext}"
|
||||
echo "[build] cargo build --release --target=${rust_target} in ${NATIVE_DIR}"
|
||||
(cd "${NATIVE_DIR}" && cargo build --release --target="${rust_target}")
|
||||
cp "${NATIVE_DIR}/target/${rust_target}/release/${lib_name}" "${outfile}"
|
||||
BUILT_FILES+=("${PLUGIN_NAME}-${os}-${arch}.${ext}")
|
||||
done
|
||||
|
||||
rm -f "${ZIP_OUT}"
|
||||
|
||||
# Collect files to zip
|
||||
ZIP_FILES=()
|
||||
for bf in "${BUILT_FILES[@]}"; do
|
||||
ZIP_FILES+=("${bf}")
|
||||
done
|
||||
for asset in "${PLUGIN_NAME}.html" "${PLUGIN_NAME}.css" "${PLUGIN_NAME}.js"; do
|
||||
if [[ -f "${PLUGIN_DIR}/${asset}" ]]; then
|
||||
ZIP_FILES+=("${asset}")
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v zip >/dev/null 2>&1; then
|
||||
(cd "${PLUGIN_DIR}" && zip -q "${ZIP_OUT}" "${ZIP_FILES[@]}")
|
||||
else
|
||||
echo "[error] zip not found. Please install zip." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[ok] ${ZIP_OUT}"
|
||||
@@ -0,0 +1,101 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set "ROOT=%~dp0"
|
||||
set "PLUGIN_DIR=%ROOT%sample-go"
|
||||
if not "%~1"=="" set "PLUGIN_DIR=%~1"
|
||||
|
||||
set "NATIVE_DIR=%PLUGIN_DIR%\native"
|
||||
set "PLUGIN_NAME=sample"
|
||||
set "ZIP_OUT=%PLUGIN_DIR%\%PLUGIN_NAME%.zip"
|
||||
|
||||
if not exist "%NATIVE_DIR%" (
|
||||
echo [error] native folder not found: %NATIVE_DIR%
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
pushd "%NATIVE_DIR%"
|
||||
|
||||
REM Build targets - default to windows-amd64 on Windows
|
||||
if not defined BUILD_TARGETS set "BUILD_TARGETS=windows-amd64"
|
||||
|
||||
set "BUILT_FILES="
|
||||
for %%T in (%BUILD_TARGETS%) do (
|
||||
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
|
||||
set "TARGET_OS=%%A"
|
||||
set "TARGET_ARCH=%%B"
|
||||
)
|
||||
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
set "EXT=dll"
|
||||
) else if "!TARGET_OS!"=="darwin" (
|
||||
set "EXT=dylib"
|
||||
) else (
|
||||
set "EXT=so"
|
||||
)
|
||||
set "BUILDMODE=c-shared"
|
||||
|
||||
set "OUTFILE=%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.!EXT!"
|
||||
echo [build] GOOS=!TARGET_OS! GOARCH=!TARGET_ARCH! CGO_ENABLED=1 go build -buildmode=!BUILDMODE! -o "!OUTFILE!"
|
||||
set "GOOS=!TARGET_OS!"
|
||||
set "GOARCH=!TARGET_ARCH!"
|
||||
set "CGO_ENABLED=1"
|
||||
go build -buildmode=!BUILDMODE! -o "!OUTFILE!" .
|
||||
if errorlevel 1 (
|
||||
echo [error] build failed for !TARGET_OS!-!TARGET_ARCH!
|
||||
popd
|
||||
exit /b 1
|
||||
)
|
||||
set "BUILT_FILES=!BUILT_FILES! '%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.!EXT!'"
|
||||
)
|
||||
|
||||
set "GOOS="
|
||||
set "GOARCH="
|
||||
set "CGO_ENABLED="
|
||||
|
||||
popd
|
||||
|
||||
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
|
||||
|
||||
REM Collect all build outputs and web assets
|
||||
set "ZIP_SOURCES="
|
||||
for %%T in (%BUILD_TARGETS%) do (
|
||||
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
|
||||
set "TARGET_OS=%%A"
|
||||
set "TARGET_ARCH=%%B"
|
||||
)
|
||||
if "!TARGET_OS!"=="windows" (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dll'"
|
||||
) else if "!TARGET_OS!"=="darwin" (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dylib'"
|
||||
) else (
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.so'"
|
||||
)
|
||||
)
|
||||
|
||||
REM Add web assets
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.html" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.html'"
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.css" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.css'"
|
||||
if exist "%PLUGIN_DIR%\%PLUGIN_NAME%.js" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%\%PLUGIN_NAME%.js'"
|
||||
|
||||
REM Remove leading comma
|
||||
set "ZIP_SOURCES=!ZIP_SOURCES:~1!"
|
||||
|
||||
powershell -NoProfile -Command "Compress-Archive -Path !ZIP_SOURCES! -DestinationPath '%ZIP_OUT%'"
|
||||
if errorlevel 1 (
|
||||
echo [error] zip failed
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Optional: sign the plugin if PLUGIN_SIGN_KEY is set
|
||||
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 "%ROOT%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,81 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PLUGIN_DIR="${1:-${ROOT_DIR}/sample-go}"
|
||||
NATIVE_DIR="${PLUGIN_DIR}/native"
|
||||
PLUGIN_NAME="sample"
|
||||
ZIP_OUT="${PLUGIN_DIR}/${PLUGIN_NAME}.zip"
|
||||
|
||||
if [[ ! -d "${NATIVE_DIR}" ]]; then
|
||||
echo "[error] native folder not found: ${NATIVE_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pushd "${NATIVE_DIR}" >/dev/null
|
||||
|
||||
# Detect current OS/arch for default build
|
||||
HOST_OS="$(go env GOOS)"
|
||||
HOST_ARCH="$(go env GOARCH)"
|
||||
|
||||
# Build matrix - can be overridden with BUILD_TARGETS env var
|
||||
# Format: "os-arch os-arch ..." e.g. "linux-amd64 linux-arm64 darwin-arm64"
|
||||
DEFAULT_TARGETS="${HOST_OS}-${HOST_ARCH}"
|
||||
BUILD_TARGETS="${BUILD_TARGETS:-${DEFAULT_TARGETS}}"
|
||||
|
||||
BUILT_FILES=()
|
||||
|
||||
for target in ${BUILD_TARGETS}; do
|
||||
os="${target%%-*}"
|
||||
arch="${target#*-}"
|
||||
|
||||
if [[ "${os}" == "windows" ]]; then
|
||||
ext="dll"
|
||||
elif [[ "${os}" == "darwin" ]]; then
|
||||
ext="dylib"
|
||||
else
|
||||
ext="so"
|
||||
fi
|
||||
buildmode="c-shared"
|
||||
|
||||
outfile="${PLUGIN_DIR}/${PLUGIN_NAME}-${os}-${arch}.${ext}"
|
||||
echo "[build] GOOS=${os} GOARCH=${arch} CGO_ENABLED=1 go build -buildmode=${buildmode} -o ${outfile}"
|
||||
CGO_ENABLED=1 GOOS="${os}" GOARCH="${arch}" go build -buildmode="${buildmode}" -o "${outfile}" .
|
||||
BUILT_FILES+=("${PLUGIN_NAME}-${os}-${arch}.${ext}")
|
||||
done
|
||||
|
||||
popd >/dev/null
|
||||
|
||||
rm -f "${ZIP_OUT}"
|
||||
|
||||
# Collect files to zip
|
||||
ZIP_FILES=()
|
||||
for bf in "${BUILT_FILES[@]}"; do
|
||||
ZIP_FILES+=("${bf}")
|
||||
done
|
||||
# Add web assets
|
||||
for asset in "${PLUGIN_NAME}.html" "${PLUGIN_NAME}.css" "${PLUGIN_NAME}.js"; do
|
||||
if [[ -f "${PLUGIN_DIR}/${asset}" ]]; then
|
||||
ZIP_FILES+=("${asset}")
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v zip >/dev/null 2>&1; then
|
||||
(cd "${PLUGIN_DIR}" && zip -q "${ZIP_OUT}" "${ZIP_FILES[@]}")
|
||||
else
|
||||
echo "[error] zip not found. Please install zip." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Optional: sign the plugin if PLUGIN_SIGN_KEY is set
|
||||
if [[ -n "${PLUGIN_SIGN_KEY:-}" ]]; then
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
if command -v bun >/dev/null 2>&1; then
|
||||
echo "[sign] Signing plugin with key: ${PLUGIN_SIGN_KEY}"
|
||||
bun run "${SCRIPT_DIR}/Overlord-Server/scripts/plugin-sign.ts" --key "${PLUGIN_SIGN_KEY}" "${ZIP_OUT}"
|
||||
else
|
||||
echo "[warn] bun not found, skipping plugin signing" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[ok] ${ZIP_OUT}"
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Sample C Plugin — fully unloadable, no runtime baggage.
|
||||
*
|
||||
* Exports the standard Overlord plugin ABI:
|
||||
* PluginOnLoad, PluginOnEvent, PluginOnUnload, PluginSetCallback,
|
||||
* PluginGetRuntime (returns "c" — tells the host this DLL can be freed)
|
||||
*
|
||||
* Build (Windows):
|
||||
* cl /LD /O2 plugin.c /Fe:sample-c-windows-amd64.dll
|
||||
* — or with MinGW —
|
||||
* x86_64-w64-mingw32-gcc -shared -O2 -o sample-c-windows-amd64.dll plugin.c
|
||||
*
|
||||
* Build (Linux):
|
||||
* gcc -shared -fPIC -O2 -o sample-c-linux-amd64.so plugin.c
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Platform helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#ifdef _WIN32
|
||||
# define EXPORT __declspec(dllexport)
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
# include <windows.h>
|
||||
BOOL WINAPI DllMain(HINSTANCE hDll, DWORD reason, LPVOID reserved) {
|
||||
(void)hDll; (void)reserved;
|
||||
switch (reason) {
|
||||
case DLL_PROCESS_ATTACH: break;
|
||||
case DLL_PROCESS_DETACH: break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
#else
|
||||
# define EXPORT __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Host callback */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#ifdef _WIN32
|
||||
/* Windows: callback is void(__stdcall*)(ptr,len,ptr,len) */
|
||||
typedef void (__stdcall *host_callback_t)(
|
||||
const char *event, uintptr_t eventLen,
|
||||
const char *payload, uintptr_t payloadLen);
|
||||
#else
|
||||
/* Unix: callback is void(*)(ctx, event, eventLen, payload, payloadLen) */
|
||||
typedef void (*host_callback_t)(
|
||||
uintptr_t ctx,
|
||||
const char *event, int eventLen,
|
||||
const char *payload, int payloadLen);
|
||||
#endif
|
||||
|
||||
static host_callback_t g_callback = NULL;
|
||||
#ifndef _WIN32
|
||||
static uintptr_t g_callback_ctx = 0;
|
||||
#endif
|
||||
|
||||
static void send_event(const char *event, const char *payload) {
|
||||
if (!g_callback) return;
|
||||
int elen = event ? (int)strlen(event) : 0;
|
||||
int plen = payload ? (int)strlen(payload) : 0;
|
||||
#ifdef _WIN32
|
||||
g_callback(event, (uintptr_t)elen, payload, (uintptr_t)plen);
|
||||
#else
|
||||
g_callback(g_callback_ctx, event, elen, payload, plen);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Plugin state (trivial example) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static char g_client_id[256];
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Exported ABI */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
EXPORT const char *PluginGetRuntime(void) {
|
||||
return "c";
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
EXPORT void PluginSetCallback(uint64_t cb) {
|
||||
g_callback = (host_callback_t)(uintptr_t)cb;
|
||||
}
|
||||
|
||||
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uint64_t cb) {
|
||||
g_callback = (host_callback_t)(uintptr_t)cb;
|
||||
#else
|
||||
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen,
|
||||
uintptr_t cb, uintptr_t ctx) {
|
||||
g_callback = (host_callback_t)cb;
|
||||
g_callback_ctx = ctx;
|
||||
#endif
|
||||
/* Minimal JSON-ish parse to grab clientId — keeps the example dependency-free */
|
||||
g_client_id[0] = '\0';
|
||||
if (hostInfo && hostInfoLen > 0) {
|
||||
const char *key = "\"clientId\":\"";
|
||||
const char *p = strstr(hostInfo, key); /* safe: hostInfo is nul-terminated by the Go caller copying it */
|
||||
if (!p) {
|
||||
key = "\"clientId\": \"";
|
||||
p = strstr(hostInfo, key);
|
||||
}
|
||||
if (p) {
|
||||
p += strlen(key);
|
||||
const char *end = strchr(p, '"');
|
||||
if (end) {
|
||||
size_t n = (size_t)(end - p);
|
||||
if (n >= sizeof(g_client_id)) n = sizeof(g_client_id) - 1;
|
||||
memcpy(g_client_id, p, n);
|
||||
g_client_id[n] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr, "[sample-c] loaded, clientId=%s\n", g_client_id);
|
||||
send_event("ready", "{\"message\":\"sample-c plugin ready\"}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
EXPORT int PluginOnEvent(const char *event, int eventLen,
|
||||
const char *payload, int payloadLen) {
|
||||
(void)payloadLen;
|
||||
|
||||
/* Simple echo for "ping" */
|
||||
if (eventLen == 4 && memcmp(event, "ping", 4) == 0) {
|
||||
send_event("pong", NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Echo back ui_message */
|
||||
if (eventLen == 10 && memcmp(event, "ui_message", 10) == 0) {
|
||||
fprintf(stderr, "[sample-c] got ui_message: %.*s\n", payloadLen, payload ? payload : "");
|
||||
/* Build a tiny JSON response — payload is already JSON from the host */
|
||||
char buf[512];
|
||||
snprintf(buf, sizeof(buf), "{\"message\":\"echo from C: %.*s\"}",
|
||||
payloadLen < 400 ? payloadLen : 400, payload ? payload : "");
|
||||
send_event("echo", buf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[sample-c] unhandled event: %.*s\n", eventLen, event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
EXPORT void PluginOnUnload(void) {
|
||||
fprintf(stderr, "[sample-c] unloading\n");
|
||||
g_callback = NULL;
|
||||
#ifndef _WIN32
|
||||
g_callback_ctx = 0;
|
||||
#endif
|
||||
g_client_id[0] = '\0';
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", system-ui, sans-serif;
|
||||
background: #0f1115;
|
||||
color: #e8ecf1;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 820px;
|
||||
margin: 40px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
margin: 0 0 24px 0;
|
||||
color: #9aa4b2;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #161a22;
|
||||
border: 1px solid #222938;
|
||||
border-radius: 12px;
|
||||
padding: 18px;
|
||||
margin-bottom: 18px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #9aa4b2;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #2a3346;
|
||||
background: #0f131b;
|
||||
color: #e8ecf1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
.log {
|
||||
background: #0b0e14;
|
||||
border: 1px solid #1e2533;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Sample C Plugin</title>
|
||||
<link rel="stylesheet" href="/plugins/sample-c/assets/sample-c.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<header class="header">
|
||||
<h1>Sample C Plugin</h1>
|
||||
<p class="sub">Pure C — fully unloadable, zero runtime leak</p>
|
||||
</header>
|
||||
|
||||
<section class="card">
|
||||
<div class="row">
|
||||
<label class="label">Client ID</label>
|
||||
<input id="client-id" class="input" placeholder="clientId" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label class="label">Message</label>
|
||||
<input id="message" class="input" placeholder="hello from UI" />
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="send-btn" class="btn">Send event to client</button>
|
||||
<button id="ping-btn" class="btn">Ping</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Log</h2>
|
||||
<pre id="log" class="log"></pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/plugins/sample-c/assets/sample-c.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,43 @@
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const clientIdInput = document.getElementById("client-id");
|
||||
const messageInput = document.getElementById("message");
|
||||
const logEl = document.getElementById("log");
|
||||
const sendBtn = document.getElementById("send-btn");
|
||||
const pingBtn = document.getElementById("ping-btn");
|
||||
|
||||
const pluginId = "sample-c";
|
||||
clientIdInput.value = params.get("clientId") || "";
|
||||
messageInput.value = "hello from sample-c UI";
|
||||
|
||||
function log(line) {
|
||||
const ts = new Date().toISOString();
|
||||
logEl.textContent = `${ts} ${line}\n` + logEl.textContent;
|
||||
}
|
||||
|
||||
async function sendEvent(event, payload) {
|
||||
const clientId = clientIdInput.value.trim();
|
||||
if (!clientId) { log("Missing clientId"); return; }
|
||||
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${pluginId}/event`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ event, payload }),
|
||||
});
|
||||
if (!res.ok) { log(`Send failed: ${res.status} ${await res.text()}`); return; }
|
||||
log(`Sent event: ${event}`);
|
||||
}
|
||||
|
||||
sendBtn.addEventListener("click", () => {
|
||||
sendEvent("ui_message", { message: messageInput.value.trim() });
|
||||
});
|
||||
|
||||
pingBtn.addEventListener("click", () => {
|
||||
sendEvent("ping", {});
|
||||
});
|
||||
|
||||
// Listen for plugin events via SSE / polling (same pattern as Go sample)
|
||||
if (typeof EventSource !== "undefined") {
|
||||
const clientId = params.get("clientId");
|
||||
if (clientId) {
|
||||
log("Ready — C plugin (fully unloadable)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Sample C++ Plugin — fully unloadable, no runtime baggage.
|
||||
*
|
||||
* Exports the standard Overlord plugin ABI:
|
||||
* PluginOnLoad, PluginOnEvent, PluginOnUnload, PluginSetCallback,
|
||||
* PluginGetRuntime (returns "cpp" — tells the host this DLL can be freed)
|
||||
*
|
||||
* Build (Windows / MSVC):
|
||||
* cl /LD /EHsc /O2 plugin.cpp /Fe:sample-cpp-windows-amd64.dll
|
||||
* — or with MinGW —
|
||||
* x86_64-w64-mingw32-g++ -shared -O2 -o sample-cpp-windows-amd64.dll plugin.cpp
|
||||
*
|
||||
* Build (Linux):
|
||||
* g++ -shared -fPIC -O2 -o sample-cpp-linux-amd64.so plugin.cpp
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Platform helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#ifdef _WIN32
|
||||
# define EXPORT extern "C" __declspec(dllexport)
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
# include <windows.h>
|
||||
BOOL WINAPI DllMain(HINSTANCE hDll, DWORD reason, LPVOID reserved) {
|
||||
(void)hDll; (void)reserved;
|
||||
return TRUE;
|
||||
}
|
||||
#else
|
||||
# define EXPORT extern "C" __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Host callback */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#ifdef _WIN32
|
||||
typedef void (__stdcall *host_callback_t)(
|
||||
const char *event, uintptr_t eventLen,
|
||||
const char *payload, uintptr_t payloadLen);
|
||||
#else
|
||||
typedef void (*host_callback_t)(
|
||||
uintptr_t ctx,
|
||||
const char *event, int eventLen,
|
||||
const char *payload, int payloadLen);
|
||||
#endif
|
||||
|
||||
static host_callback_t g_callback = nullptr;
|
||||
#ifndef _WIN32
|
||||
static uintptr_t g_callback_ctx = 0;
|
||||
#endif
|
||||
|
||||
static void send_event(const char *event, const char *payload) {
|
||||
if (!g_callback) return;
|
||||
int elen = event ? static_cast<int>(strlen(event)) : 0;
|
||||
int plen = payload ? static_cast<int>(strlen(payload)) : 0;
|
||||
#ifdef _WIN32
|
||||
g_callback(event, static_cast<uintptr_t>(elen),
|
||||
payload, static_cast<uintptr_t>(plen));
|
||||
#else
|
||||
g_callback(g_callback_ctx, event, elen, payload, plen);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Plugin state — demonstrates C++ features (std::string, maps, mutex) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static std::mutex g_mu;
|
||||
static std::string g_client_id;
|
||||
static std::unordered_map<std::string, int> g_event_counts;
|
||||
|
||||
/* Extract a JSON string value by key (simple, no full JSON parser) */
|
||||
static std::string json_extract(const char *json, int len, const char *key) {
|
||||
if (!json || len <= 0) return "";
|
||||
std::string haystack(json, static_cast<size_t>(len));
|
||||
std::string needle = std::string("\"") + key + "\":\"";
|
||||
auto pos = haystack.find(needle);
|
||||
if (pos == std::string::npos) {
|
||||
needle = std::string("\"") + key + "\": \"";
|
||||
pos = haystack.find(needle);
|
||||
}
|
||||
if (pos == std::string::npos) return "";
|
||||
pos += needle.size();
|
||||
auto end = haystack.find('"', pos);
|
||||
if (end == std::string::npos) return "";
|
||||
return haystack.substr(pos, end - pos);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Exported ABI */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
EXPORT const char *PluginGetRuntime() {
|
||||
return "cpp";
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
EXPORT void PluginSetCallback(uint64_t cb) {
|
||||
g_callback = reinterpret_cast<host_callback_t>(static_cast<uintptr_t>(cb));
|
||||
}
|
||||
|
||||
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uint64_t cb) {
|
||||
g_callback = reinterpret_cast<host_callback_t>(static_cast<uintptr_t>(cb));
|
||||
#else
|
||||
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen,
|
||||
uintptr_t cb, uintptr_t ctx) {
|
||||
g_callback = reinterpret_cast<host_callback_t>(cb);
|
||||
g_callback_ctx = ctx;
|
||||
#endif
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_mu);
|
||||
g_client_id = json_extract(hostInfo, hostInfoLen, "clientId");
|
||||
g_event_counts.clear();
|
||||
}
|
||||
|
||||
fprintf(stderr, "[sample-cpp] loaded, clientId=%s\n", g_client_id.c_str());
|
||||
send_event("ready", "{\"message\":\"sample-cpp plugin ready\"}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
EXPORT int PluginOnEvent(const char *event, int eventLen,
|
||||
const char *payload, int payloadLen) {
|
||||
std::string ev(event, static_cast<size_t>(eventLen));
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_mu);
|
||||
g_event_counts[ev]++;
|
||||
}
|
||||
|
||||
if (ev == "ping") {
|
||||
send_event("pong", nullptr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ev == "ui_message") {
|
||||
std::string pl(payload ? payload : "", payload ? static_cast<size_t>(payloadLen) : 0u);
|
||||
fprintf(stderr, "[sample-cpp] got ui_message: %s\n", pl.c_str());
|
||||
std::string resp = "{\"message\":\"echo from C++: " + pl + "\"}";
|
||||
send_event("echo", resp.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ev == "stats") {
|
||||
std::lock_guard<std::mutex> lk(g_mu);
|
||||
std::string json = "{";
|
||||
bool first = true;
|
||||
for (auto &kv : g_event_counts) {
|
||||
if (!first) json += ",";
|
||||
json += "\"" + kv.first + "\":" + std::to_string(kv.second);
|
||||
first = false;
|
||||
}
|
||||
json += "}";
|
||||
send_event("stats_reply", json.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[sample-cpp] unhandled event: %s\n", ev.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
EXPORT void PluginOnUnload() {
|
||||
fprintf(stderr, "[sample-cpp] unloading\n");
|
||||
g_callback = nullptr;
|
||||
#ifndef _WIN32
|
||||
g_callback_ctx = 0;
|
||||
#endif
|
||||
std::lock_guard<std::mutex> lk(g_mu);
|
||||
g_client_id.clear();
|
||||
g_event_counts.clear();
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", system-ui, sans-serif;
|
||||
background: #0f1115;
|
||||
color: #e8ecf1;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 820px;
|
||||
margin: 40px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
margin: 0 0 24px 0;
|
||||
color: #9aa4b2;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #161a22;
|
||||
border: 1px solid #222938;
|
||||
border-radius: 12px;
|
||||
padding: 18px;
|
||||
margin-bottom: 18px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #9aa4b2;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #2a3346;
|
||||
background: #0f131b;
|
||||
color: #e8ecf1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
.log {
|
||||
background: #0b0e14;
|
||||
border: 1px solid #1e2533;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Sample C++ Plugin</title>
|
||||
<link rel="stylesheet" href="/plugins/sample-cpp/assets/sample-cpp.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<header class="header">
|
||||
<h1>Sample C++ Plugin</h1>
|
||||
<p class="sub">C++ — fully unloadable, zero runtime leak</p>
|
||||
</header>
|
||||
|
||||
<section class="card">
|
||||
<div class="row">
|
||||
<label class="label">Client ID</label>
|
||||
<input id="client-id" class="input" placeholder="clientId" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label class="label">Message</label>
|
||||
<input id="message" class="input" placeholder="hello from UI" />
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="send-btn" class="btn">Send event to client</button>
|
||||
<button id="ping-btn" class="btn">Ping</button>
|
||||
<button id="stats-btn" class="btn">Get Stats</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Log</h2>
|
||||
<pre id="log" class="log"></pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/plugins/sample-cpp/assets/sample-cpp.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,47 @@
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const clientIdInput = document.getElementById("client-id");
|
||||
const messageInput = document.getElementById("message");
|
||||
const logEl = document.getElementById("log");
|
||||
const sendBtn = document.getElementById("send-btn");
|
||||
const pingBtn = document.getElementById("ping-btn");
|
||||
const statsBtn = document.getElementById("stats-btn");
|
||||
|
||||
const pluginId = "sample-cpp";
|
||||
clientIdInput.value = params.get("clientId") || "";
|
||||
messageInput.value = "hello from sample-cpp UI";
|
||||
|
||||
function log(line) {
|
||||
const ts = new Date().toISOString();
|
||||
logEl.textContent = `${ts} ${line}\n` + logEl.textContent;
|
||||
}
|
||||
|
||||
async function sendEvent(event, payload) {
|
||||
const clientId = clientIdInput.value.trim();
|
||||
if (!clientId) { log("Missing clientId"); return; }
|
||||
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${pluginId}/event`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ event, payload }),
|
||||
});
|
||||
if (!res.ok) { log(`Send failed: ${res.status} ${await res.text()}`); return; }
|
||||
log(`Sent event: ${event}`);
|
||||
}
|
||||
|
||||
sendBtn.addEventListener("click", () => {
|
||||
sendEvent("ui_message", { message: messageInput.value.trim() });
|
||||
});
|
||||
|
||||
pingBtn.addEventListener("click", () => {
|
||||
sendEvent("ping", {});
|
||||
});
|
||||
|
||||
statsBtn.addEventListener("click", () => {
|
||||
sendEvent("stats", {});
|
||||
});
|
||||
|
||||
if (typeof EventSource !== "undefined") {
|
||||
const clientId = params.get("clientId");
|
||||
if (clientId) {
|
||||
log("Ready — C++ plugin (fully unloadable)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
# Sample Go Plugin (sample)
|
||||
|
||||
This sample lives in the `plugins/` directory and builds a native Go plugin.
|
||||
|
||||
## Files to include in the plugin zip
|
||||
|
||||
At the root of the zip:
|
||||
- sample-linux-amd64.so / sample-darwin-arm64.dylib / sample-windows-amd64.dll
|
||||
- sample.html
|
||||
- sample.css
|
||||
- sample.js
|
||||
|
||||
## Build
|
||||
|
||||
Use the provided build scripts:
|
||||
|
||||
```bash
|
||||
# Linux/macOS
|
||||
./build-plugin.sh
|
||||
|
||||
# Multiple targets
|
||||
BUILD_TARGETS="linux-amd64 linux-arm64 darwin-arm64" ./build-plugin.sh
|
||||
|
||||
# Windows
|
||||
build-plugin.bat
|
||||
```
|
||||
|
||||
## Create the zip
|
||||
|
||||
The build script automatically creates `sample.zip` containing the built
|
||||
binaries and web assets.
|
||||
|
||||
Then place `sample.zip` in `Overlord-Server/plugins`.
|
||||
|
||||
## Open the UI
|
||||
|
||||
Navigate to:
|
||||
- /plugins/sample?clientId=<CLIENT_ID>
|
||||
|
||||
Click "Send event" and the plugin will echo back to the server logs.
|
||||
|
||||
## OS detection (Windows-only logic)
|
||||
|
||||
The plugin receives host metadata in the `init` payload. The sample plugin shows
|
||||
how to detect the host OS and branch logic (see
|
||||
`sample-go/native/main.go`). For example:
|
||||
|
||||
- If `host.os == "windows"`, you can safely run Windows-only flows (e.g., WinAPI-backed behavior).
|
||||
- Otherwise, skip or provide a fallback path.
|
||||
@@ -0,0 +1,75 @@
|
||||
//go:build linux || darwin || freebsd
|
||||
|
||||
package main
|
||||
|
||||
/*
|
||||
#include <stdint.h>
|
||||
|
||||
typedef void (*host_callback_t)(uintptr_t ctx,
|
||||
const char* event, int eventLen,
|
||||
const char* payload, int payloadLen);
|
||||
|
||||
static void invoke_host_callback(uintptr_t fn, uintptr_t ctx,
|
||||
const char* ev, int evLen, const char* pl, int plLen) {
|
||||
((host_callback_t)fn)(ctx, ev, evLen, pl, plLen);
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import "unsafe"
|
||||
|
||||
var (
|
||||
hostCallbackFn C.uintptr_t
|
||||
hostCallbackCtx C.uintptr_t
|
||||
)
|
||||
|
||||
func hostSendViaCallback(event string, payload []byte) {
|
||||
fn := hostCallbackFn
|
||||
if fn == 0 {
|
||||
return
|
||||
}
|
||||
evBytes := []byte(event)
|
||||
var evPtr, plPtr unsafe.Pointer
|
||||
if len(evBytes) > 0 {
|
||||
evPtr = unsafe.Pointer(&evBytes[0])
|
||||
}
|
||||
if len(payload) > 0 {
|
||||
plPtr = unsafe.Pointer(&payload[0])
|
||||
}
|
||||
C.invoke_host_callback(fn, hostCallbackCtx,
|
||||
(*C.char)(evPtr), C.int(len(evBytes)),
|
||||
(*C.char)(plPtr), C.int(len(payload)))
|
||||
}
|
||||
|
||||
//export PluginOnLoad
|
||||
func PluginOnLoad(hostInfo *C.char, hostInfoLen C.int, cb C.uintptr_t, ctx C.uintptr_t) C.int {
|
||||
hostCallbackFn = cb
|
||||
hostCallbackCtx = ctx
|
||||
setSend(hostSendViaCallback)
|
||||
|
||||
data := C.GoBytes(unsafe.Pointer(hostInfo), hostInfoLen)
|
||||
if err := handleInit(data); err != nil {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
//export PluginOnEvent
|
||||
func PluginOnEvent(event *C.char, eventLen C.int, payload *C.char, payloadLen C.int) C.int {
|
||||
ev := C.GoStringN(event, eventLen)
|
||||
var pl []byte
|
||||
if payloadLen > 0 {
|
||||
pl = C.GoBytes(unsafe.Pointer(payload), payloadLen)
|
||||
}
|
||||
if err := handleEvent(ev, pl); err != nil {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
//export PluginOnUnload
|
||||
func PluginOnUnload() {
|
||||
handleUnload()
|
||||
}
|
||||
|
||||
func main() {}
|
||||
@@ -0,0 +1,67 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var callbackPtr uintptr
|
||||
|
||||
func hostSendViaCallback(event string, payload []byte) {
|
||||
cb := callbackPtr
|
||||
if cb == 0 {
|
||||
return
|
||||
}
|
||||
eventBytes := []byte(event)
|
||||
var evPtr, plPtr uintptr
|
||||
evLen := uintptr(len(eventBytes))
|
||||
plLen := uintptr(len(payload))
|
||||
if len(eventBytes) > 0 {
|
||||
evPtr = uintptr(unsafe.Pointer(&eventBytes[0]))
|
||||
}
|
||||
if len(payload) > 0 {
|
||||
plPtr = uintptr(unsafe.Pointer(&payload[0]))
|
||||
}
|
||||
syscall.SyscallN(cb, evPtr, evLen, plPtr, plLen)
|
||||
}
|
||||
|
||||
//export PluginSetCallback
|
||||
func PluginSetCallback(cb C.ulonglong) {
|
||||
callbackPtr = uintptr(cb)
|
||||
setSend(hostSendViaCallback)
|
||||
}
|
||||
|
||||
//export PluginOnLoad
|
||||
func PluginOnLoad(hostInfo *C.char, hostInfoLen C.int, cb C.ulonglong) C.int {
|
||||
callbackPtr = uintptr(cb)
|
||||
setSend(hostSendViaCallback)
|
||||
|
||||
data := C.GoBytes(unsafe.Pointer(hostInfo), hostInfoLen)
|
||||
if err := handleInit(data); err != nil {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
//export PluginOnEvent
|
||||
func PluginOnEvent(event *C.char, eventLen C.int, payload *C.char, payloadLen C.int) C.int {
|
||||
ev := C.GoStringN(event, eventLen)
|
||||
var pl []byte
|
||||
if payloadLen > 0 {
|
||||
pl = C.GoBytes(unsafe.Pointer(payload), payloadLen)
|
||||
}
|
||||
if err := handleEvent(ev, pl); err != nil {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
//export PluginOnUnload
|
||||
func PluginOnUnload() {
|
||||
handleUnload()
|
||||
}
|
||||
|
||||
func main() {}
|
||||
@@ -0,0 +1,3 @@
|
||||
module sample-plugin
|
||||
|
||||
go 1.24
|
||||
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type HostInfo struct {
|
||||
ClientID string `json:"clientId"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
var (
|
||||
hostInfo HostInfo
|
||||
sendFn func(event string, payload []byte)
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
func setSend(fn func(event string, payload []byte)) {
|
||||
mu.Lock()
|
||||
sendFn = fn
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
func sendEvent(event string, payload interface{}) {
|
||||
mu.Lock()
|
||||
fn := sendFn
|
||||
mu.Unlock()
|
||||
if fn == nil {
|
||||
return
|
||||
}
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
log.Printf("[sample] marshal error: %v", err)
|
||||
return
|
||||
}
|
||||
fn(event, data)
|
||||
}
|
||||
|
||||
func handleInit(hostJSON []byte) error {
|
||||
if err := json.Unmarshal(hostJSON, &hostInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("[sample] init: clientId=%s os=%s arch=%s", hostInfo.ClientID, hostInfo.OS, hostInfo.Arch)
|
||||
sendEvent("ready", map[string]string{"message": "sample plugin ready"})
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleEvent(event string, payload []byte) error {
|
||||
switch event {
|
||||
case "ui_message":
|
||||
var msg struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
_ = json.Unmarshal(payload, &msg)
|
||||
log.Printf("[sample] got ui_message: %s", msg.Message)
|
||||
sendEvent("echo", map[string]string{"message": msg.Message})
|
||||
|
||||
case "ping":
|
||||
sendEvent("pong", nil)
|
||||
|
||||
default:
|
||||
log.Printf("[sample] unhandled event: %s", event)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleUnload() {
|
||||
log.Printf("[sample] unloading")
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/* Code generated by cmd/cgo; DO NOT EDIT. */
|
||||
|
||||
/* package sample-plugin */
|
||||
|
||||
|
||||
#line 1 "cgo-builtin-export-prolog"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifndef GO_CGO_EXPORT_PROLOGUE_H
|
||||
#define GO_CGO_EXPORT_PROLOGUE_H
|
||||
|
||||
#ifndef GO_CGO_GOSTRING_TYPEDEF
|
||||
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
|
||||
extern size_t _GoStringLen(_GoString_ s);
|
||||
extern const char *_GoStringPtr(_GoString_ s);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
/* Start of preamble from import "C" comments. */
|
||||
|
||||
|
||||
|
||||
|
||||
/* End of preamble from import "C" comments. */
|
||||
|
||||
|
||||
/* Start of boilerplate cgo prologue. */
|
||||
#line 1 "cgo-gcc-export-header-prolog"
|
||||
|
||||
#ifndef GO_CGO_PROLOGUE_H
|
||||
#define GO_CGO_PROLOGUE_H
|
||||
|
||||
typedef signed char GoInt8;
|
||||
typedef unsigned char GoUint8;
|
||||
typedef short GoInt16;
|
||||
typedef unsigned short GoUint16;
|
||||
typedef int GoInt32;
|
||||
typedef unsigned int GoUint32;
|
||||
typedef long long GoInt64;
|
||||
typedef unsigned long long GoUint64;
|
||||
typedef GoInt64 GoInt;
|
||||
typedef GoUint64 GoUint;
|
||||
typedef size_t GoUintptr;
|
||||
typedef float GoFloat32;
|
||||
typedef double GoFloat64;
|
||||
#ifdef _MSC_VER
|
||||
#if !defined(__cplusplus) || _MSVC_LANG <= 201402L
|
||||
#include <complex.h>
|
||||
typedef _Fcomplex GoComplex64;
|
||||
typedef _Dcomplex GoComplex128;
|
||||
#else
|
||||
#include <complex>
|
||||
typedef std::complex<float> GoComplex64;
|
||||
typedef std::complex<double> GoComplex128;
|
||||
#endif
|
||||
#else
|
||||
typedef float _Complex GoComplex64;
|
||||
typedef double _Complex GoComplex128;
|
||||
#endif
|
||||
|
||||
/*
|
||||
static assertion to make sure the file is being used on architecture
|
||||
at least with matching size of GoInt.
|
||||
*/
|
||||
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];
|
||||
|
||||
#ifndef GO_CGO_GOSTRING_TYPEDEF
|
||||
typedef _GoString_ GoString;
|
||||
#endif
|
||||
typedef void *GoMap;
|
||||
typedef void *GoChan;
|
||||
typedef struct { void *t; void *v; } GoInterface;
|
||||
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
|
||||
|
||||
#endif
|
||||
|
||||
/* End of boilerplate cgo prologue. */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern __declspec(dllexport) void PluginSetCallback(long long unsigned int cb);
|
||||
extern __declspec(dllexport) int PluginOnLoad(char* hostInfo, int hostInfoLen, long long unsigned int cb);
|
||||
extern __declspec(dllexport) int PluginOnEvent(char* event, int eventLen, char* payload, int payloadLen);
|
||||
extern __declspec(dllexport) void PluginOnUnload(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,89 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", system-ui, sans-serif;
|
||||
background: #0f1115;
|
||||
color: #e8ecf1;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 820px;
|
||||
margin: 40px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
margin: 0 0 24px 0;
|
||||
color: #9aa4b2;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #161a22;
|
||||
border: 1px solid #222938;
|
||||
border-radius: 12px;
|
||||
padding: 18px;
|
||||
margin-bottom: 18px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #9aa4b2;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #2a3346;
|
||||
background: #0f131b;
|
||||
color: #e8ecf1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.log {
|
||||
min-height: 120px;
|
||||
background: #0f131b;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #2a3346;
|
||||
padding: 12px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Sample Plugin</title>
|
||||
<link rel="stylesheet" href="/plugins/sample/assets/sample.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<header class="header">
|
||||
<h1>Sample Plugin</h1>
|
||||
<p class="sub">Go Native + UI plugin</p>
|
||||
</header>
|
||||
|
||||
<section class="card">
|
||||
<div class="row">
|
||||
<label class="label">Client ID</label>
|
||||
<input id="client-id" class="input" placeholder="clientId" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label class="label">Message</label>
|
||||
<input id="message" class="input" placeholder="hello from UI" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label class="label">Message Box</label>
|
||||
<input id="msgbox" class="input" placeholder="Windows only" />
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="send-btn" class="btn">Send event to client</button>
|
||||
<button id="ping-btn" class="btn">Ping Test x100</button>
|
||||
<button id="msgbox-btn" class="btn">Show Message Box</button>
|
||||
<button id="run-cmd-btn" class="btn">Run Command (example)</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Log</h2>
|
||||
<pre id="log" class="log"></pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/plugins/sample/assets/sample.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,103 @@
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const clientIdInput = document.getElementById("client-id");
|
||||
const messageInput = document.getElementById("message");
|
||||
const msgBoxInput = document.getElementById("msgbox");
|
||||
const logEl = document.getElementById("log");
|
||||
const sendBtn = document.getElementById("send-btn");
|
||||
const pingBtn = document.getElementById("ping-btn");
|
||||
const msgBoxBtn = document.getElementById("msgbox-btn");
|
||||
const runCmdBtn = document.getElementById("run-cmd-btn");
|
||||
|
||||
const pluginId = "sample";
|
||||
const clientIdFromUrl = params.get("clientId") || "";
|
||||
clientIdInput.value = clientIdFromUrl;
|
||||
messageInput.value = "hello from sample UI";
|
||||
msgBoxInput.value = "Hello from Overlord";
|
||||
|
||||
function log(line) {
|
||||
const ts = new Date().toISOString();
|
||||
logEl.textContent = `${ts} ${line}\n` + logEl.textContent;
|
||||
}
|
||||
|
||||
async function sendEvent(event, payload) {
|
||||
const clientId = clientIdInput.value.trim();
|
||||
if (!clientId) {
|
||||
log("Missing clientId");
|
||||
return;
|
||||
}
|
||||
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${pluginId}/event`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ event, payload }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
log(`Send failed: ${res.status} ${text}`);
|
||||
return;
|
||||
}
|
||||
log(`Sent event: ${event}`);
|
||||
}
|
||||
|
||||
sendBtn.addEventListener("click", () => {
|
||||
const message = messageInput.value.trim();
|
||||
sendEvent("ui_message", { message });
|
||||
});
|
||||
|
||||
pingBtn?.addEventListener("click", async () => {
|
||||
const clientId = clientIdInput.value.trim();
|
||||
if (!clientId) {
|
||||
log("Missing clientId");
|
||||
return;
|
||||
}
|
||||
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/command`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "ping_bulk", count: 100 }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const msg = await res.text();
|
||||
log(`Ping test failed: ${msg}`);
|
||||
return;
|
||||
}
|
||||
log("Ping test x100 dispatched (single request)");
|
||||
});
|
||||
|
||||
msgBoxBtn?.addEventListener("click", async () => {
|
||||
const clientId = clientIdInput.value.trim();
|
||||
if (!clientId) {
|
||||
log("Missing clientId");
|
||||
return;
|
||||
}
|
||||
const text = msgBoxInput.value.trim() || "Hello from Overlord";
|
||||
const script = `$os = $env:OS; if ($os -ne 'Windows_NT') { exit } ; Add-Type -AssemblyName PresentationFramework; [System.Windows.MessageBox]::Show('${text.replace(/'/g, "''")}') | Out-Null`;
|
||||
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/command`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "script_exec", script, scriptType: "powershell" }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const msg = await res.text();
|
||||
log(`Message box failed: ${msg}`);
|
||||
return;
|
||||
}
|
||||
log("Message box command sent (Windows only)");
|
||||
});
|
||||
|
||||
runCmdBtn?.addEventListener("click", async () => {
|
||||
const clientId = clientIdInput.value.trim();
|
||||
if (!clientId) {
|
||||
log("Missing clientId");
|
||||
return;
|
||||
}
|
||||
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/command`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "screenshot" }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const msg = await res.text();
|
||||
log(`Command failed: ${msg}`);
|
||||
return;
|
||||
}
|
||||
log("Command sent: screenshot");
|
||||
});
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "sample-rust"
|
||||
version = "0.1.0"
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "sample-rust"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
strip = true
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* Sample Rust Plugin — fully unloadable, no GC runtime.
|
||||
*
|
||||
* Exports the standard Overlord plugin ABI:
|
||||
* PluginOnLoad, PluginOnEvent, PluginOnUnload, PluginSetCallback,
|
||||
* PluginGetRuntime (returns "rust" — tells the host this DLL can be freed)
|
||||
*
|
||||
* Build:
|
||||
* cargo build --release
|
||||
* Output: target/release/sample_rust.dll (Windows) or libsample_rust.so (Linux)
|
||||
*
|
||||
* Note: Uses C-style globals instead of std::sync::Mutex because the DLL is
|
||||
* loaded via an in-memory PE loader, and Rust's std Mutex (SRWLOCK) may not
|
||||
* initialize correctly without OS-level LoadLibrary. The Go host already
|
||||
* serializes all calls through its own mutex, so this is safe.
|
||||
*/
|
||||
|
||||
use std::os::raw::{c_char, c_int};
|
||||
use std::slice;
|
||||
use std::ptr;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Host callback types — platform-specific calling conventions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(windows)]
|
||||
type HostCallback = unsafe extern "stdcall" fn(
|
||||
event: *const u8,
|
||||
event_len: usize,
|
||||
payload: *const u8,
|
||||
payload_len: usize,
|
||||
);
|
||||
|
||||
#[cfg(not(windows))]
|
||||
type HostCallback = unsafe extern "C" fn(
|
||||
ctx: usize,
|
||||
event: *const u8,
|
||||
event_len: i32,
|
||||
payload: *const u8,
|
||||
payload_len: i32,
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Global state — C-style, no Mutex.
|
||||
// The Go host serializes all calls via dllPlugin.mu / soPlugin.mu.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static mut G_CALLBACK: Option<HostCallback> = None;
|
||||
#[cfg(not(windows))]
|
||||
static mut G_CALLBACK_CTX: usize = 0;
|
||||
static mut G_CLIENT_ID: [u8; 256] = [0u8; 256];
|
||||
static mut G_CLIENT_ID_LEN: usize = 0;
|
||||
static mut G_EVENT_COUNT: u64 = 0;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
unsafe fn send_event(event: &[u8], payload: &[u8]) {
|
||||
if let Some(cb) = G_CALLBACK {
|
||||
#[cfg(windows)]
|
||||
cb(
|
||||
event.as_ptr(),
|
||||
event.len(),
|
||||
payload.as_ptr(),
|
||||
payload.len(),
|
||||
);
|
||||
#[cfg(not(windows))]
|
||||
cb(
|
||||
G_CALLBACK_CTX,
|
||||
event.as_ptr(),
|
||||
event.len() as i32,
|
||||
payload.as_ptr(),
|
||||
payload.len() as i32,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal JSON string extraction by key — zero allocations.
|
||||
unsafe fn json_extract_into(json: &[u8], key: &[u8], out: &mut [u8]) -> usize {
|
||||
// Search for "key":" or "key": "
|
||||
let klen = key.len();
|
||||
if json.len() < klen + 4 {
|
||||
return 0;
|
||||
}
|
||||
let mut i = 0;
|
||||
while i + klen + 3 < json.len() {
|
||||
if json[i] == b'"'
|
||||
&& json[i + 1..i + 1 + klen] == *key
|
||||
&& json[i + 1 + klen] == b'"'
|
||||
{
|
||||
// Skip past "key"
|
||||
let mut j = i + 1 + klen + 1; // past closing quote
|
||||
// Skip : and optional space
|
||||
while j < json.len() && (json[j] == b':' || json[j] == b' ') {
|
||||
j += 1;
|
||||
}
|
||||
// Expect opening quote
|
||||
if j < json.len() && json[j] == b'"' {
|
||||
j += 1;
|
||||
let start = j;
|
||||
while j < json.len() && json[j] != b'"' {
|
||||
j += 1;
|
||||
}
|
||||
let vlen = j - start;
|
||||
let copy_len = if vlen < out.len() { vlen } else { out.len() - 1 };
|
||||
ptr::copy_nonoverlapping(json[start..].as_ptr(), out.as_mut_ptr(), copy_len);
|
||||
return copy_len;
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
unsafe fn bytes_to_slice<'a>(p: *const c_char, len: c_int) -> &'a [u8] {
|
||||
if p.is_null() || len <= 0 {
|
||||
return &[];
|
||||
}
|
||||
slice::from_raw_parts(p as *const u8, len as usize)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Exported ABI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn PluginGetRuntime() -> *const c_char {
|
||||
b"rust\0".as_ptr() as *const c_char
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn PluginSetCallback(cb: u64) {
|
||||
G_CALLBACK = Some(std::mem::transmute(cb as usize));
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn PluginOnLoad(
|
||||
host_info: *const c_char,
|
||||
host_info_len: c_int,
|
||||
cb: u64,
|
||||
) -> c_int {
|
||||
G_CALLBACK = Some(std::mem::transmute(cb as usize));
|
||||
G_EVENT_COUNT = 0;
|
||||
|
||||
let info = bytes_to_slice(host_info, host_info_len);
|
||||
G_CLIENT_ID_LEN = json_extract_into(info, b"clientId", &mut G_CLIENT_ID);
|
||||
|
||||
// Build a small on-stack response and send
|
||||
send_event(
|
||||
b"ready",
|
||||
br#"{"message":"sample-rust plugin ready"}"#,
|
||||
);
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn PluginOnLoad(
|
||||
host_info: *const c_char,
|
||||
host_info_len: c_int,
|
||||
cb: usize,
|
||||
ctx: usize,
|
||||
) -> c_int {
|
||||
G_CALLBACK = Some(std::mem::transmute(cb));
|
||||
G_CALLBACK_CTX = ctx;
|
||||
G_EVENT_COUNT = 0;
|
||||
|
||||
let info = bytes_to_slice(host_info, host_info_len);
|
||||
G_CLIENT_ID_LEN = json_extract_into(info, b"clientId", &mut G_CLIENT_ID);
|
||||
|
||||
send_event(
|
||||
b"ready",
|
||||
br#"{"message":"sample-rust plugin ready"}"#,
|
||||
);
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn PluginOnEvent(
|
||||
event: *const c_char,
|
||||
event_len: c_int,
|
||||
payload: *const c_char,
|
||||
payload_len: c_int,
|
||||
) -> c_int {
|
||||
let ev = bytes_to_slice(event, event_len);
|
||||
let pl = bytes_to_slice(payload, payload_len);
|
||||
|
||||
G_EVENT_COUNT += 1;
|
||||
|
||||
if ev == b"ping" {
|
||||
send_event(b"pong", &[]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ev == b"ui_message" {
|
||||
// Build echo response on the stack — limited to 512 bytes
|
||||
let prefix = br#"{"message":"echo from Rust: "#;
|
||||
let suffix = br#""}"#;
|
||||
let max_pl = 450;
|
||||
let pl_copy = if pl.len() < max_pl { pl.len() } else { max_pl };
|
||||
let total = prefix.len() + pl_copy + suffix.len();
|
||||
let mut buf = [0u8; 512];
|
||||
if total <= buf.len() {
|
||||
ptr::copy_nonoverlapping(prefix.as_ptr(), buf.as_mut_ptr(), prefix.len());
|
||||
ptr::copy_nonoverlapping(
|
||||
pl.as_ptr(),
|
||||
buf.as_mut_ptr().add(prefix.len()),
|
||||
pl_copy,
|
||||
);
|
||||
ptr::copy_nonoverlapping(
|
||||
suffix.as_ptr(),
|
||||
buf.as_mut_ptr().add(prefix.len() + pl_copy),
|
||||
suffix.len(),
|
||||
);
|
||||
send_event(b"echo", &buf[..total]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ev == b"stats" {
|
||||
// Format event count into a stack buffer
|
||||
let mut buf = [0u8; 128];
|
||||
let prefix = br#"{"event_count":"#;
|
||||
let suffix = b"}";
|
||||
ptr::copy_nonoverlapping(prefix.as_ptr(), buf.as_mut_ptr(), prefix.len());
|
||||
let mut num_buf = [0u8; 20];
|
||||
let num_len = fmt_u64(G_EVENT_COUNT, &mut num_buf);
|
||||
ptr::copy_nonoverlapping(
|
||||
num_buf[20 - num_len..].as_ptr(),
|
||||
buf.as_mut_ptr().add(prefix.len()),
|
||||
num_len,
|
||||
);
|
||||
ptr::copy_nonoverlapping(
|
||||
suffix.as_ptr(),
|
||||
buf.as_mut_ptr().add(prefix.len() + num_len),
|
||||
suffix.len(),
|
||||
);
|
||||
let total = prefix.len() + num_len + suffix.len();
|
||||
send_event(b"stats_reply", &buf[..total]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn PluginOnUnload() {
|
||||
G_CALLBACK = None;
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
G_CALLBACK_CTX = 0;
|
||||
}
|
||||
G_CLIENT_ID_LEN = 0;
|
||||
G_EVENT_COUNT = 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tiny u64-to-decimal without std::fmt (avoids allocator)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn fmt_u64(mut n: u64, buf: &mut [u8; 20]) -> usize {
|
||||
if n == 0 {
|
||||
buf[19] = b'0';
|
||||
return 1;
|
||||
}
|
||||
let mut i = 20;
|
||||
while n > 0 {
|
||||
i -= 1;
|
||||
buf[i] = b'0' + (n % 10) as u8;
|
||||
n /= 10;
|
||||
}
|
||||
20 - i
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc_fingerprint":2568544898921049753,"outputs":{"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":""},"12004014463585500860":{"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":""},"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":""}},"successes":{}}
|
||||
@@ -0,0 +1,93 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", system-ui, sans-serif;
|
||||
background: #0f1115;
|
||||
color: #e8ecf1;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 820px;
|
||||
margin: 40px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
margin: 0 0 24px 0;
|
||||
color: #9aa4b2;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #161a22;
|
||||
border: 1px solid #222938;
|
||||
border-radius: 12px;
|
||||
padding: 18px;
|
||||
margin-bottom: 18px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #9aa4b2;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #2a3346;
|
||||
background: #0f131b;
|
||||
color: #e8ecf1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
.log {
|
||||
background: #0b0e14;
|
||||
border: 1px solid #1e2533;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Sample Rust Plugin</title>
|
||||
<link rel="stylesheet" href="/plugins/sample-rust/assets/sample-rust.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<header class="header">
|
||||
<h1>Sample Rust Plugin</h1>
|
||||
<p class="sub">Rust — fully unloadable, zero runtime leak</p>
|
||||
</header>
|
||||
|
||||
<section class="card">
|
||||
<div class="row">
|
||||
<label class="label">Client ID</label>
|
||||
<input id="client-id" class="input" placeholder="clientId" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label class="label">Message</label>
|
||||
<input id="message" class="input" placeholder="hello from UI" />
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="send-btn" class="btn">Send event to client</button>
|
||||
<button id="ping-btn" class="btn">Ping</button>
|
||||
<button id="stats-btn" class="btn">Get Stats</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Log</h2>
|
||||
<pre id="log" class="log"></pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/plugins/sample-rust/assets/sample-rust.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,47 @@
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const clientIdInput = document.getElementById("client-id");
|
||||
const messageInput = document.getElementById("message");
|
||||
const logEl = document.getElementById("log");
|
||||
const sendBtn = document.getElementById("send-btn");
|
||||
const pingBtn = document.getElementById("ping-btn");
|
||||
const statsBtn = document.getElementById("stats-btn");
|
||||
|
||||
const pluginId = "sample-rust";
|
||||
clientIdInput.value = params.get("clientId") || "";
|
||||
messageInput.value = "hello from sample-rust UI";
|
||||
|
||||
function log(line) {
|
||||
const ts = new Date().toISOString();
|
||||
logEl.textContent = `${ts} ${line}\n` + logEl.textContent;
|
||||
}
|
||||
|
||||
async function sendEvent(event, payload) {
|
||||
const clientId = clientIdInput.value.trim();
|
||||
if (!clientId) { log("Missing clientId"); return; }
|
||||
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${pluginId}/event`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ event, payload }),
|
||||
});
|
||||
if (!res.ok) { log(`Send failed: ${res.status} ${await res.text()}`); return; }
|
||||
log(`Sent event: ${event}`);
|
||||
}
|
||||
|
||||
sendBtn.addEventListener("click", () => {
|
||||
sendEvent("ui_message", { message: messageInput.value.trim() });
|
||||
});
|
||||
|
||||
pingBtn.addEventListener("click", () => {
|
||||
sendEvent("ping", {});
|
||||
});
|
||||
|
||||
statsBtn.addEventListener("click", () => {
|
||||
sendEvent("stats", {});
|
||||
});
|
||||
|
||||
if (typeof EventSource !== "undefined") {
|
||||
const clientId = params.get("clientId");
|
||||
if (clientId) {
|
||||
log("Ready — Rust plugin (fully unloadable)");
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user