feat: Add parallel HTTP server E2E tests and enhance plugin system
- Add e2e_plugin_net_additional.rs with parallel server tests - Fix test to properly handle request objects (no double accept) - Add comprehensive net-plugin documentation - Implement debug tracing for method calls - Enhance plugin lifecycle documentation - Improve error handling in plugin loader - Add leak tracking infrastructure (for future use) - Update language spec with latest plugin features This enhances test coverage for concurrent HTTP servers and improves the overall plugin system documentation and debugging capabilities. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -61,6 +61,9 @@ pub struct MethodDefinition {
|
||||
/// Optional argument declarations (v2.1+)
|
||||
#[serde(default)]
|
||||
pub args: Option<Vec<ArgDecl>>,
|
||||
/// Optional: wrap return and errors into ResultBox when true
|
||||
#[serde(default)]
|
||||
pub returns_result: bool,
|
||||
}
|
||||
|
||||
/// Method argument declaration (v2.1+)
|
||||
|
||||
38
src/debug/leak_tracker.rs
Normal file
38
src/debug/leak_tracker.rs
Normal file
@ -0,0 +1,38 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
static ENABLED: Lazy<bool> = Lazy::new(|| std::env::var("NYASH_LEAK_LOG").unwrap_or_default() == "1");
|
||||
static LEAKS: Lazy<Mutex<HashMap<(String, u32), &'static str>>> = Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
pub fn init() {
|
||||
let _ = &*REPORTER; // ensure reporter is constructed
|
||||
}
|
||||
|
||||
pub fn register_plugin(box_type: &str, instance_id: u32) {
|
||||
if !*ENABLED { return; }
|
||||
let mut m = LEAKS.lock().unwrap();
|
||||
m.insert((box_type.to_string(), instance_id), "plugin");
|
||||
}
|
||||
|
||||
pub fn finalize_plugin(box_type: &str, instance_id: u32) {
|
||||
if !*ENABLED { return; }
|
||||
let mut m = LEAKS.lock().unwrap();
|
||||
m.remove(&(box_type.to_string(), instance_id));
|
||||
}
|
||||
|
||||
struct Reporter;
|
||||
impl Drop for Reporter {
|
||||
fn drop(&mut self) {
|
||||
if !*ENABLED { return; }
|
||||
let m = LEAKS.lock().unwrap();
|
||||
if m.is_empty() { return; }
|
||||
eprintln!("[leak] Detected {} non-finalized plugin boxes:", m.len());
|
||||
for ((ty, id), _) in m.iter() {
|
||||
eprintln!(" - {}(id={}) not finalized (missing fini or scope)", ty, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static REPORTER: Lazy<Reporter> = Lazy::new(|| Reporter);
|
||||
|
||||
@ -228,6 +228,9 @@ pub struct NyashInterpreter {
|
||||
/// 共有ランタイム(Boxレジストリ等)
|
||||
#[allow(dead_code)]
|
||||
pub(super) runtime: NyashRuntime,
|
||||
|
||||
/// 現在の文脈で式結果が破棄されるか(must_use警告用)
|
||||
pub(super) discard_context: bool,
|
||||
}
|
||||
|
||||
impl NyashInterpreter {
|
||||
@ -259,6 +262,7 @@ impl NyashInterpreter {
|
||||
invalidated_ids: Arc::new(Mutex::new(HashSet::new())),
|
||||
stdlib: None, // 遅延初期化
|
||||
runtime,
|
||||
discard_context: false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -292,6 +296,7 @@ impl NyashInterpreter {
|
||||
invalidated_ids: Arc::new(Mutex::new(HashSet::new())),
|
||||
stdlib: None,
|
||||
runtime,
|
||||
discard_context: false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -321,6 +326,7 @@ impl NyashInterpreter {
|
||||
invalidated_ids: Arc::new(Mutex::new(HashSet::new())),
|
||||
stdlib: None, // 遅延初期化
|
||||
runtime,
|
||||
discard_context: false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -351,6 +357,7 @@ impl NyashInterpreter {
|
||||
invalidated_ids: Arc::new(Mutex::new(HashSet::new())),
|
||||
stdlib: None,
|
||||
runtime,
|
||||
discard_context: false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -379,8 +386,12 @@ impl NyashInterpreter {
|
||||
ASTNode::Program { statements, .. } => {
|
||||
let mut result: Box<dyn NyashBox> = Box::new(VoidBox::new());
|
||||
|
||||
for statement in statements.iter() {
|
||||
let last = statements.len().saturating_sub(1);
|
||||
for (i, statement) in statements.iter().enumerate() {
|
||||
let prev = self.discard_context;
|
||||
self.discard_context = i != last; // 最終文以外は値が破棄される
|
||||
result = self.execute_statement(statement)?;
|
||||
self.discard_context = prev;
|
||||
|
||||
// 制御フローチェック
|
||||
match &self.control_flow {
|
||||
|
||||
@ -996,6 +996,10 @@ impl NyashInterpreter {
|
||||
method: &str,
|
||||
arguments: &[ASTNode],
|
||||
) -> Result<Box<dyn NyashBox>, RuntimeError> {
|
||||
// Guard: use-after-fini is a runtime error (明示ライフサイクル)
|
||||
if plugin_box.is_finalized() {
|
||||
return Err(RuntimeError::RuntimeFailure { message: format!("Use after fini: {}", plugin_box.box_type) });
|
||||
}
|
||||
eprintln!("🔍 execute_plugin_box_v2_method called: {}.{}", plugin_box.box_type, method);
|
||||
let mut arg_values: Vec<Box<dyn NyashBox>> = Vec::new();
|
||||
for arg in arguments {
|
||||
|
||||
@ -20,6 +20,26 @@ macro_rules! debug_trace {
|
||||
}
|
||||
|
||||
impl NyashInterpreter {
|
||||
fn warn_if_must_use(&self, value: &Box<dyn NyashBox>) {
|
||||
if std::env::var("NYASH_LINT_MUSTUSE").unwrap_or_default() != "1" { return; }
|
||||
if !self.discard_context { return; }
|
||||
// 重資源のヒューリスティクス: プラグインBox、またはHTTP/Socket/File系の型名
|
||||
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
|
||||
{
|
||||
if value.as_any().downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>().is_some() {
|
||||
eprintln!("[lint:must_use] Discarded resource value (plugin box). Consider assigning it or calling fini().");
|
||||
return;
|
||||
}
|
||||
}
|
||||
let ty = value.type_name();
|
||||
let heavy = matches!(ty,
|
||||
"FileBox" | "SocketBox" | "SocketServerBox" | "SocketClientBox" | "SocketConnBox" |
|
||||
"HTTPServerBox" | "HTTPRequestBox" | "HTTPResponseBox" | "HttpClientBox"
|
||||
);
|
||||
if heavy {
|
||||
eprintln!("[lint:must_use] Discarded {} value. Consider assigning it or calling fini().", ty);
|
||||
}
|
||||
}
|
||||
/// 文を実行 - Core statement execution engine
|
||||
pub(super) fn execute_statement(&mut self, statement: &ASTNode) -> Result<Box<dyn NyashBox>, RuntimeError> {
|
||||
match statement {
|
||||
@ -180,8 +200,12 @@ impl NyashInterpreter {
|
||||
Ok(Box::new(VoidBox::new()))
|
||||
}
|
||||
|
||||
// 式文
|
||||
_ => self.execute_expression(statement),
|
||||
// 式文(結果は多くの場合破棄されるため、must_use警告を出力)
|
||||
_ => {
|
||||
let v = self.execute_expression(statement)?;
|
||||
self.warn_if_must_use(&v);
|
||||
Ok(v)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
34
src/runtime/leak_tracker.rs
Normal file
34
src/runtime/leak_tracker.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
static ENABLED: Lazy<bool> = Lazy::new(|| std::env::var("NYASH_LEAK_LOG").unwrap_or_default() == "1");
|
||||
static LEAKS: Lazy<Mutex<HashMap<(String, u32), &'static str>>> = Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
pub fn init() { let _ = &*REPORTER; }
|
||||
|
||||
pub fn register_plugin(box_type: &str, instance_id: u32) {
|
||||
if !*ENABLED { return; }
|
||||
LEAKS.lock().unwrap().insert((box_type.to_string(), instance_id), "plugin");
|
||||
}
|
||||
|
||||
pub fn finalize_plugin(box_type: &str, instance_id: u32) {
|
||||
if !*ENABLED { return; }
|
||||
LEAKS.lock().unwrap().remove(&(box_type.to_string(), instance_id));
|
||||
}
|
||||
|
||||
struct Reporter;
|
||||
impl Drop for Reporter {
|
||||
fn drop(&mut self) {
|
||||
if !*ENABLED { return; }
|
||||
let m = LEAKS.lock().unwrap();
|
||||
if m.is_empty() { return; }
|
||||
eprintln!("[leak] Detected {} non-finalized plugin boxes:", m.len());
|
||||
for ((ty, id), _) in m.iter() {
|
||||
eprintln!(" - {}(id={}) not finalized (missing fini or scope)", ty, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static REPORTER: Lazy<Reporter> = Lazy::new(|| Reporter);
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
pub mod plugin_config;
|
||||
pub mod box_registry;
|
||||
pub mod plugin_loader_v2;
|
||||
pub mod leak_tracker;
|
||||
pub mod unified_registry;
|
||||
pub mod nyash_runtime;
|
||||
// pub mod plugin_box; // legacy - 古いPluginBox
|
||||
|
||||
@ -14,6 +14,7 @@ mod enabled {
|
||||
// use std::ffi::c_void; // unused
|
||||
use std::any::Any;
|
||||
use once_cell::sync::Lazy;
|
||||
use crate::runtime::leak_tracker;
|
||||
|
||||
/// Loaded plugin information
|
||||
pub struct LoadedPluginV2 {
|
||||
@ -71,6 +72,7 @@ mod enabled {
|
||||
pub fn finalize_now(&self) {
|
||||
if let Some(fini_id) = self.fini_method_id {
|
||||
if !self.finalized.swap(true, std::sync::atomic::Ordering::SeqCst) {
|
||||
leak_tracker::finalize_plugin("PluginBox", self.instance_id);
|
||||
let tlv_args: [u8; 4] = [1, 0, 0, 0];
|
||||
let mut out: [u8; 4] = [0; 4];
|
||||
let mut out_len: usize = out.len();
|
||||
@ -196,6 +198,7 @@ mod enabled {
|
||||
impl PluginBoxV2 {
|
||||
pub fn instance_id(&self) -> u32 { self.inner.instance_id }
|
||||
pub fn finalize_now(&self) { self.inner.finalize_now() }
|
||||
pub fn is_finalized(&self) -> bool { self.inner.finalized.load(std::sync::atomic::Ordering::SeqCst) }
|
||||
}
|
||||
|
||||
/// Plugin loader v2
|
||||
@ -376,6 +379,7 @@ impl PluginBoxV2 {
|
||||
let toml_value: toml::Value = toml::from_str(&toml_content).map_err(|_| BidError::PluginError)?;
|
||||
let box_conf = config.get_box_config(lib_name, box_type, &toml_value).ok_or(BidError::InvalidType)?;
|
||||
let type_id = box_conf.type_id;
|
||||
let returns_result = box_conf.methods.get(method_name).map(|m| m.returns_result).unwrap_or(false);
|
||||
eprintln!("[PluginLoaderV2] Invoke {}.{}: resolving and encoding args (argc={})", box_type, method_name, args.len());
|
||||
// TLV args: encode using BID-1 style (u16 ver, u16 argc, then entries)
|
||||
let tlv_args = {
|
||||
@ -497,15 +501,29 @@ impl PluginBoxV2 {
|
||||
if rc != 0 {
|
||||
let be = BidError::from_raw(rc);
|
||||
eprintln!("[PluginLoaderV2] invoke rc={} ({}) for {}.{}", rc, be.message(), box_type, method_name);
|
||||
if returns_result {
|
||||
let err = crate::exception_box::ErrorBox::new(&format!("{} (code: {})", be.message(), rc));
|
||||
return Ok(Some(Box::new(crate::boxes::result::NyashResultBox::new_err(Box::new(err)))));
|
||||
}
|
||||
return Err(be);
|
||||
}
|
||||
// Decode: BID-1 style header + first entry
|
||||
let result = if out_len == 0 { None } else {
|
||||
let result = if out_len == 0 {
|
||||
if returns_result {
|
||||
Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(Box::new(crate::box_trait::VoidBox::new()))) as Box<dyn NyashBox>)
|
||||
} else { None }
|
||||
} else {
|
||||
let data = &out[..out_len];
|
||||
if data.len() < 4 { return Ok(None); }
|
||||
let _ver = u16::from_le_bytes([data[0], data[1]]);
|
||||
let argc = u16::from_le_bytes([data[2], data[3]]);
|
||||
if argc == 0 { return Ok(None); }
|
||||
if argc == 0 {
|
||||
if returns_result {
|
||||
return Ok(Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(Box::new(crate::box_trait::VoidBox::new())))));
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
if data.len() < 8 { return Ok(None); }
|
||||
let tag = data[4];
|
||||
let _rsv = data[5];
|
||||
@ -536,7 +554,12 @@ impl PluginBoxV2 {
|
||||
finalized: std::sync::atomic::AtomicBool::new(false),
|
||||
}),
|
||||
};
|
||||
return Ok(Some(Box::new(pbox) as Box<dyn NyashBox>));
|
||||
let val: Box<dyn NyashBox> = Box::new(pbox);
|
||||
if returns_result {
|
||||
return Ok(Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(val))));
|
||||
} else {
|
||||
return Ok(Some(val));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -544,13 +567,17 @@ impl PluginBoxV2 {
|
||||
}
|
||||
2 if size == 4 => { // I32
|
||||
let mut b = [0u8;4]; b.copy_from_slice(payload);
|
||||
Some(Box::new(IntegerBox::new(i32::from_le_bytes(b) as i64)) as Box<dyn NyashBox>)
|
||||
let val: Box<dyn NyashBox> = Box::new(IntegerBox::new(i32::from_le_bytes(b) as i64));
|
||||
if returns_result { Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(val)) as Box<dyn NyashBox>) } else { Some(val) }
|
||||
}
|
||||
6 | 7 => { // String/Bytes
|
||||
let s = String::from_utf8_lossy(payload).to_string();
|
||||
Some(Box::new(StringBox::new(s)) as Box<dyn NyashBox>)
|
||||
let val: Box<dyn NyashBox> = Box::new(StringBox::new(s));
|
||||
if returns_result { Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(val)) as Box<dyn NyashBox>) } else { Some(val) }
|
||||
}
|
||||
9 => None, // Void
|
||||
9 => {
|
||||
if returns_result { Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(Box::new(crate::box_trait::VoidBox::new()))) as Box<dyn NyashBox>) } else { None }
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
@ -723,7 +750,7 @@ impl PluginBoxV2 {
|
||||
};
|
||||
|
||||
eprintln!("🎉 birth() success: {} instance_id={}", box_type, instance_id);
|
||||
|
||||
|
||||
// Create v2 plugin box wrapper with actual instance_id
|
||||
let plugin_box = PluginBoxV2 {
|
||||
box_type: box_type.to_string(),
|
||||
@ -735,6 +762,8 @@ impl PluginBoxV2 {
|
||||
finalized: std::sync::atomic::AtomicBool::new(false),
|
||||
}),
|
||||
};
|
||||
leak_tracker::init();
|
||||
leak_tracker::register_plugin(&plugin_box.box_type, instance_id);
|
||||
|
||||
Ok(Box::new(plugin_box))
|
||||
}
|
||||
|
||||
@ -39,9 +39,10 @@ impl ScopeTracker {
|
||||
let _ = instance.fini();
|
||||
continue;
|
||||
}
|
||||
// PluginBoxV2: do not auto-finalize (shared handle may be referenced elsewhere)
|
||||
// PluginBoxV2: 明示ライフサイクルに合わせ、スコープ終了時にfini(自己責任運用)
|
||||
#[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
|
||||
if arc_box.as_any().downcast_ref::<PluginBoxV2>().is_some() {
|
||||
if let Some(p) = arc_box.as_any().downcast_ref::<PluginBoxV2>() {
|
||||
p.finalize_now();
|
||||
continue;
|
||||
}
|
||||
// Builtin and others: no-op for now
|
||||
|
||||
Reference in New Issue
Block a user