/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2026 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
use darkfi_serial::{Decodable, Encodable};
use std::{
io::Cursor,
sync::{mpsc, Arc, Mutex},
thread,
time::{Duration, Instant},
};
use crate::{
error::Result,
prop::{Property, PropertySubType, PropertyType},
py::PythonPlugin,
res::{ResourceId, ResourceManager},
scene::{MethodResponseFn, SceneGraph, SceneGraphPtr, SceneNodeId, SceneNodeType},
};
pub enum Category {
Null,
}
pub enum SubCategory {
Null,
}
pub struct SemVer {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub pre: String,
pub build: String,
}
pub struct PluginMetadata {
pub name: String,
pub title: String,
pub desc: String,
pub author: String,
pub version: SemVer,
pub cat: Category,
pub subcat: SubCategory,
// icon
// Permissions
// whitelisted nodes + props/methods (use * for all)
// /window/input/*
}
pub enum PluginEvent {
// (signal_data, user_data)
RecvSignal((Vec, Vec)),
}
pub type PluginInstancePtr = Arc>>;
pub trait Plugin {
fn metadata(&self) -> PluginMetadata;
// Spawns a new context and begins running the plugin in that context
fn start(&self) -> Result;
}
pub trait PluginInstance {
fn update(&mut self, event: PluginEvent) -> Result<()>;
}
enum SentinelMethodEvent {
ImportPlugin,
StartPlugin(ResourceId),
}
pub struct Sentinel {
scene_graph: SceneGraphPtr,
plugins: ResourceManager>,
insts: ResourceManager,
method_recvr: mpsc::Receiver<(SentinelMethodEvent, SceneNodeId, Vec, MethodResponseFn)>,
method_sender: mpsc::SyncSender<(SentinelMethodEvent, SceneNodeId, Vec, MethodResponseFn)>,
}
impl Sentinel {
pub fn new(scene_graph: SceneGraphPtr) -> Self {
// Create /plugin in scene graph
//
// Methods provided in SceneGraph under /plugin:
//
// * import_plugin(pycode)
let mut sg = scene_graph.lock().unwrap();
let (method_sender, method_recvr) = mpsc::sync_channel(100);
let node = sg.add_node("plugin", SceneNodeType::Plugins);
let sender = method_sender.clone();
let node_id = node.id;
let method_fn = Box::new(move |arg_data, response_fn| {
sender.send((SentinelMethodEvent::ImportPlugin, node_id, arg_data, response_fn));
});
node.add_method("import", vec![("pycode", "", PropertyType::Str)], vec![], method_fn);
sg.link(node_id, SceneGraph::ROOT_ID).unwrap();
drop(sg);
Self {
scene_graph,
plugins: ResourceManager::new(),
insts: ResourceManager::new(),
method_recvr,
method_sender,
}
}
pub fn run(&mut self) {
loop {
// Monitor all running plugins
// Check last update times
// Kill any slowpokes
// Check any SceneGraph method requests
let deadline = Instant::now() + Duration::from_millis(4000);
let Ok((event, node_id, arg_data, response_fn)) =
self.method_recvr.recv_deadline(deadline)
else {
break
};
let res = match event {
SentinelMethodEvent::ImportPlugin => self.import_py_plugin(node_id, arg_data),
SentinelMethodEvent::StartPlugin(rid) => self.start_plugin(rid, node_id, arg_data),
};
response_fn(res);
}
}
fn import_py_plugin(&mut self, node_id: SceneNodeId, arg_data: Vec) -> Result> {
// Load the python code
let mut cur = Cursor::new(&arg_data);
let pycode = String::decode(&mut cur).unwrap();
let plugin = Box::new(PythonPlugin::new(self.scene_graph.clone(), pycode));
self.import_plugin(plugin)?;
// This function doesn't return anything
// Only success or an Err which is already handled elsewhere
Ok(vec![])
}
fn import_plugin(&mut self, plugin: Box) -> Result<()> {
let metadata = plugin.metadata();
let plugin_rid = self.plugins.alloc(plugin);
let mut scene_graph = self.scene_graph.lock().unwrap();
// Create /plugin/foo
let node = scene_graph.add_node(metadata.name.clone(), SceneNodeType::Plugin);
let node_id = node.id;
// name
let mut prop = Property::new("name", PropertyType::Str, PropertySubType::Null);
prop.set_str(0, metadata.name);
node.add_property(prop).unwrap();
// title
let mut prop = Property::new("title", PropertyType::Str, PropertySubType::Null);
prop.set_str(0, metadata.title);
node.add_property(prop).unwrap();
// desc
let mut prop = Property::new("desc", PropertyType::Str, PropertySubType::Null);
prop.set_str(0, metadata.desc);
node.add_property(prop).unwrap();
// author
let mut prop = Property::new("author", PropertyType::Str, PropertySubType::Null);
prop.set_str(0, metadata.author);
node.add_property(prop).unwrap();
// version
let mut prop = Property::new("version", PropertyType::Uint32, PropertySubType::Null);
prop.set_array_len(3);
prop.set_u32(0, metadata.version.major);
prop.set_u32(1, metadata.version.minor);
prop.set_u32(2, metadata.version.patch);
node.add_property(prop).unwrap();
// TODO: add version.pre and patch, and cat/subcat enums
let mut prop = Property::new("insts", PropertyType::Uint32, PropertySubType::ResourceId);
prop.set_ui_text("instance resource IDs", "The currently running instances of this plugin");
prop.set_unbounded();
node.add_property(prop).unwrap();
// Add method start()
let sender = self.method_sender.clone();
let method_fn = Box::new(move |arg_data, response_fn| {
sender.send((
SentinelMethodEvent::StartPlugin(plugin_rid),
node_id,
arg_data,
response_fn,
));
});
node.add_method("start", vec![], vec![("inst_rid", "", PropertyType::Uint32)], method_fn);
// Link node
let parent_id = scene_graph.lookup_node_id("/plugin").expect("no plugin node attached");
scene_graph.link(node_id, parent_id).unwrap();
Ok(())
}
fn start_plugin(
&mut self,
plugin_rid: ResourceId,
node_id: SceneNodeId,
arg_data: Vec,
) -> Result> {
let plugin = self.plugins.get(plugin_rid).expect("plugin not found");
// Call init()
// Spawn a new thread, allocate it an ID
// Thread waits for events from the scene_graph and calls update() when they occur.
// See src/net.rs:81 for an example
let inst = plugin.start()?;
let inst2 = inst.clone();
let inst_rid = self.insts.alloc(inst);
let _ = thread::spawn(move || {
inst2.lock().unwrap().update(PluginEvent::RecvSignal((vec![], vec![]))).unwrap();
});
let scene_graph = self.scene_graph.lock().unwrap();
let node = scene_graph.get_node(node_id).expect("node not found");
let prop = node.get_property("insts").unwrap();
prop.push_u32(inst_rid)?;
// TODO: when the plugin finishes, the instance ID should be cleared up somehow
// both from the resource manager and from the property
// https://www.chromium.org/developers/design-documents/inter-process-communication/
let mut reply = vec![];
inst_rid.encode(&mut reply).unwrap();
Ok(reply)
}
}