Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 185 additions & 7 deletions sandcrate-backend/src/api.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
use axum::{routing::get, Json, Router, extract::State};
use serde::Serialize;
use axum::{
routing::{get, post},
Json, Router, extract::{State, Path},
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::{Serialize, Deserialize};
use std::sync::Arc;
use std::fs;
use std::path::Path;
use std::path::Path as FsPath;

use crate::auth::AuthConfig;
use crate::plugin;

#[derive(Serialize)]
struct Plugin {
Expand All @@ -13,17 +19,46 @@ struct Plugin {
filename: String,
size: u64,
created_at: String,
status: String,
}

#[derive(Serialize)]
struct PluginList {
plugins: Vec<Plugin>,
}

#[derive(Serialize)]
struct PluginExecutionRequest {
parameters: Option<serde_json::Value>,
timeout: Option<u64>,
}

#[derive(Serialize)]
struct PluginExecutionResponse {
success: bool,
result: String,
execution_time_ms: u64,
error: Option<String>,
}

#[derive(Serialize)]
struct ApiResponse<T> {
success: bool,
data: Option<T>,
error: Option<String>,
}

#[derive(Serialize)]
struct ErrorResponse {
success: bool,
error: String,
code: String,
}

async fn get_plugins(
State(_config): State<Arc<AuthConfig>>,
) -> Json<PluginList> {
let plugins_dir = Path::new("../assets/plugins");
) -> Json<ApiResponse<PluginList>> {
let plugins_dir = FsPath::new("assets/plugins");
let mut plugins = Vec::new();

if let Ok(entries) = fs::read_dir(plugins_dir) {
Expand Down Expand Up @@ -58,6 +93,7 @@ async fn get_plugins(
filename,
size: metadata.len(),
created_at,
status: "ready".to_string(),
});
}
}
Expand All @@ -66,9 +102,151 @@ async fn get_plugins(
}
}

Json(PluginList { plugins })
Json(ApiResponse {
success: true,
data: Some(PluginList { plugins }),
error: None,
})
}

async fn get_plugin(
Path(plugin_id): Path<String>,
) -> Response {
let plugins_dir = FsPath::new("assets/plugins");
let plugin_path = plugins_dir.join(format!("{}.wasm", plugin_id));

if !plugin_path.exists() {
return (
StatusCode::NOT_FOUND,
Json(ApiResponse::<Plugin> {
success: false,
data: None,
error: Some(format!("Plugin '{}' not found", plugin_id)),
})
).into_response();
}

if let Ok(metadata) = fs::metadata(&plugin_path) {
let filename = plugin_path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();

let name = filename.replace(".wasm", "");
let id = name.clone();

let created_at = metadata.created()
.ok()
.and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok())
.map(|duration| {
chrono::DateTime::from_timestamp(duration.as_secs() as i64, 0)
.unwrap_or_default()
.format("%Y-%m-%d %H:%M:%S")
.to_string()
})
.unwrap_or_else(|| "Unknown".to_string());

let plugin = Plugin {
id,
name,
filename,
size: metadata.len(),
created_at,
status: "ready".to_string(),
};

(
StatusCode::OK,
Json(ApiResponse {
success: true,
data: Some(plugin),
error: None,
})
).into_response()
} else {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiResponse::<Plugin> {
success: false,
data: None,
error: Some("Failed to read plugin metadata".to_string()),
})
).into_response()
}
}

async fn execute_plugin(
Path(plugin_id): Path<String>,
Json(request): Json<PluginExecutionRequest>,
) -> Response {
let start_time = std::time::Instant::now();

// Find the plugin file
let plugins_dir = FsPath::new("assets/plugins");
let plugin_path = plugins_dir.join(format!("{}.wasm", plugin_id));

if !plugin_path.exists() {
return (
StatusCode::NOT_FOUND,
Json(ApiResponse::<PluginExecutionResponse> {
success: false,
data: None,
error: Some(format!("Plugin '{}' not found", plugin_id)),
})
).into_response();
}

// Execute the plugin with parameters and timeout
let plugin_path_str = plugin_path.to_str().unwrap_or("");
let execution_result = plugin::run_plugin_with_params(
plugin_path_str,
request.parameters,
request.timeout
);

let execution_time = start_time.elapsed();
let execution_time_ms = execution_time.as_millis() as u64;

match execution_result {
Ok(result) => {
let response = PluginExecutionResponse {
success: true,
result,
execution_time_ms,
error: None,
};

(
StatusCode::OK,
Json(ApiResponse {
success: true,
data: Some(response),
error: None,
})
).into_response()
}
Err(e) => {
let response = PluginExecutionResponse {
success: false,
result: String::new(),
execution_time_ms,
error: Some(e.to_string()),
};

(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiResponse {
success: true,
data: Some(response),
error: None,
})
).into_response()
}
}
}

pub fn routes() -> Router<Arc<AuthConfig>> {
Router::new().route("/plugins", get(get_plugins))
Router::new()
.route("/plugins", get(get_plugins))
.route("/plugins/:id", get(get_plugin))
}
57 changes: 56 additions & 1 deletion sandcrate-backend/src/plugin.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::fs;
use std::path::Path;
use std::time::Duration;
use wasmtime::*;
use wasmtime_wasi::WasiCtxBuilder;
use serde_json::Value;

pub fn list_plugins() -> Vec<String> {
let plugins_dir = Path::new("assets/plugins");
Expand All @@ -26,6 +28,14 @@ pub fn list_plugins() -> Vec<String> {
}

pub fn run_plugin(plugin_path: &str) -> Result<String, Box<dyn std::error::Error>> {
run_plugin_with_params(plugin_path, None, None)
}

pub fn run_plugin_with_params(
plugin_path: &str,
parameters: Option<Value>,
_timeout: Option<u64>
) -> Result<String, Box<dyn std::error::Error>> {
// Create a WASM engine
let engine = Engine::default();

Expand Down Expand Up @@ -53,10 +63,13 @@ pub fn run_plugin(plugin_path: &str) -> Result<String, Box<dyn std::error::Error
let function_names = ["_start", "start", "main", "run"];

let mut executed = false;
let mut result = String::new();

for func_name in &function_names {
if let Ok(func) = instance.get_typed_func::<(), ()>(&mut store, func_name) {
func.call(&mut store, ())?;
executed = true;
result = format!("Plugin executed successfully using function '{}'", func_name);
break;
}
}
Expand All @@ -65,5 +78,47 @@ pub fn run_plugin(plugin_path: &str) -> Result<String, Box<dyn std::error::Error
return Err("No suitable entry function found in WASM module".into());
}

Ok("Plugin executed successfully".to_string())
Ok(result)
}

pub fn get_plugin_info(plugin_path: &str) -> Result<PluginInfo, Box<dyn std::error::Error>> {
let path = Path::new(plugin_path);

if !path.exists() {
return Err("Plugin file does not exist".into());
}

let metadata = fs::metadata(path)?;
let file_size = metadata.len();

// Try to read WASM module info
let wasm_bytes = fs::read(path)?;
let engine = Engine::default();
let module = Module::new(&engine, &wasm_bytes)?;

// Get exported functions from module
let exports: Vec<String> = module
.exports()
.map(|export| export.name().to_string())
.collect();

let has_start = exports.contains(&"_start".to_string()) ||
exports.contains(&"start".to_string()) ||
exports.contains(&"main".to_string()) ||
exports.contains(&"run".to_string());

Ok(PluginInfo {
path: plugin_path.to_string(),
size: file_size,
exports,
has_start,
})
}

#[derive(Debug)]
pub struct PluginInfo {
pub path: String,
pub size: u64,
pub exports: Vec<String>,
pub has_start: bool,
}
48 changes: 48 additions & 0 deletions sandcrate-plugin/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,52 @@
use std::ffi::CStr;
use std::os::raw::c_char;

#[no_mangle]
pub extern "C" fn main() {
println!("Hello from WASM plugin!");
println!("Plugin execution started");

// Try to get parameters if available
if let Some(params) = get_parameters() {
println!("Received parameters: {}", params);
} else {
println!("No parameters provided");
}

// Simulate some work
for i in 1..=3 {
println!("Processing step {}", i);
}

println!("Plugin execution completed successfully!");
}

#[no_mangle]
pub extern "C" fn run() {
println!("Alternative run function called!");
println!("This demonstrates multiple entry points");
}

fn get_parameters() -> Option<String> {
// This would be called by the host environment
// For now, we'll return None to indicate no parameters
None
}

// Export a function that can be called from the host
#[no_mangle]
pub extern "C" fn process_data(input: *const c_char) -> *const c_char {
if input.is_null() {
return std::ptr::null();
}

let input_str = unsafe {
CStr::from_ptr(input).to_string_lossy().into_owned()
};

let result = format!("Processed: {}", input_str);

// In a real implementation, you'd need to manage memory properly
// For this demo, we'll just return a static string
"Data processed successfully\0".as_ptr() as *const c_char
}
Loading