use std::path::PathBuf;
use std::sync::OnceLock;
use tokio::runtime::Runtime;
static RUNTIME: OnceLock<Runtime> = OnceLock::new();
static PROJECT_ROOT: OnceLock<PathBuf> = OnceLock::new();
static DATA_DIR: OnceLock<PathBuf> = OnceLock::new();
fn rt() -> &'static Runtime {
RUNTIME.get_or_init(|| {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("Failed to create tokio runtime")
})
}
fn port_in_use() -> bool {
std::net::TcpStream::connect_timeout(
&"127.0.0.1:17830".parse().unwrap(),
std::time::Duration::from_millis(300),
)
.is_ok()
}
fn resolve_token_path() -> Option<PathBuf> {
DATA_DIR
.get()
.map(|d| d.join("runtime-token"))
.or_else(|| PROJECT_ROOT.get().map(|r| r.join("data").join("runtime-token")))
}
fn ensure_port_free() -> bool {
if !port_in_use() {
return true;
}
let token = resolve_token_path()
.and_then(|p| std::fs::read_to_string(p).ok())
.unwrap_or_default();
let _ = reqwest::blocking::Client::new()
.post("http://127.0.0.1:17830/api/shutdown")
.bearer_auth(token.trim())
.timeout(std::time::Duration::from_secs(3))
.send();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::time::Instant::now() < deadline {
if !port_in_use() {
return true;
}
std::thread::sleep(std::time::Duration::from_millis(250));
}
!port_in_use()
}
pub fn start_core(project_root: String) -> String {
start_core_with_options(project_root, String::new(), String::new())
}
pub fn start_core_with_options(project_root: String, data_dir: String, config_dir: String) -> String {
let root = PathBuf::from(&project_root);
let _ = PROJECT_ROOT.set(root.clone());
let effective_data_dir = if data_dir.is_empty() {
root.join("data")
} else {
PathBuf::from(&data_dir)
};
let _ = std::fs::create_dir_all(&effective_data_dir);
let _ = DATA_DIR.set(effective_data_dir.clone());
let effective_config_dir = if config_dir.is_empty() {
root.join("config")
} else {
PathBuf::from(&config_dir)
};
if let Err(e) = std::env::set_current_dir(&root) {
return format!("failed to set cwd to {}: {}", project_root, e);
}
std::env::set_var("WEFT_FFI_DATA_DIR", effective_data_dir.to_string_lossy().as_ref());
std::env::set_var("WEFT_FFI_CONFIG_DIR", effective_config_dir.to_string_lossy().as_ref());
if !ensure_port_free() {
eprintln!("[weft-ffi] port 17830 still occupied; continuing in FFI-only mode (in-process dispatch unaffected)");
}
let debug_log_path = effective_data_dir.join("ffi-debug.log");
std::thread::spawn(move || {
use std::io::Write;
let mut log = std::fs::OpenOptions::new()
.create(true).write(true).truncate(true)
.open(&debug_log_path).ok();
if let Some(ref mut f) = log {
let _ = writeln!(f, "[ffi] run_server starting, cwd={:?}", std::env::current_dir());
}
rt().block_on(async {
if let Err(e) = crate::runtime::run_server().await {
eprintln!("[weft-ffi] run_server error: {e}");
if let Some(ref mut f) = log {
let _ = writeln!(f, "[ffi] run_server ERROR: {e}");
}
} else {
if let Some(ref mut f) = log {
let _ = writeln!(f, "[ffi] run_server exited normally");
}
}
});
});
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(90);
while std::time::Instant::now() < deadline {
if crate::api::rpc::router_ready() {
return String::new(); }
std::thread::sleep(std::time::Duration::from_millis(200));
}
"core start timed out: router not ready after 90s".to_string()
}
pub fn rpc_call(request_json: String) -> String {
let envelope: crate::api::rpc::RequestEnvelope = match serde_json::from_str(&request_json) {
Ok(e) => e,
Err(e) => {
return serde_json::json!({
"id": "",
"status": 400,
"body": { "error": format!("parse error: {e}") }
}).to_string();
}
};
let token_path = resolve_token_path()
.unwrap_or_else(|| PathBuf::from("./data/runtime-token"));
let token = std::fs::read_to_string(&token_path)
.unwrap_or_default()
.trim()
.to_string();
let response = rt().block_on(async {
crate::api::rpc::dispatch_internal(envelope, &token).await
});
serde_json::to_string(&response).unwrap_or_else(|_| {
r#"{"id":"","status":500,"body":{"error":"serialize failed"}}"#.to_string()
})
}
pub fn stop_core() {
let token_path = resolve_token_path()
.unwrap_or_else(|| PathBuf::from("./data/runtime-token"));
let token = std::fs::read_to_string(&token_path)
.unwrap_or_default()
.trim()
.to_string();
if crate::api::rpc::router_ready() {
let envelope = crate::api::rpc::RequestEnvelope {
id: "ffi-shutdown".to_string(),
method: "POST".to_string(),
path: "/api/shutdown".to_string(),
headers: std::collections::HashMap::new(),
body: serde_json::Value::Null,
};
rt().block_on(async {
crate::api::rpc::dispatch_internal(envelope, &token).await;
});
return;
}
let _ = reqwest::blocking::Client::new()
.post("http://127.0.0.1:17830/api/shutdown")
.bearer_auth(&token)
.send();
}
pub fn is_core_running() -> bool {
if crate::api::rpc::router_ready() {
return true;
}
reqwest::blocking::get("http://127.0.0.1:17830/api/health")
.map(|r| r.status().is_success())
.unwrap_or(false)
}