mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-08-06 04:29:52 +00:00
feat(ffi): add config server client bindings (#2320)
Add config server client support for the C FFI and Android JNI bindings. Reuse the existing easytier::web_client::run_web_client path and NetworkInstanceManager; OHOS is unchanged. Report successful remote config apply/delete operations through a callback, with one JSON event per affected instance. Keep the config server client and FFI data plane mutually exclusive: once either side is in use, the other side returns an error instead of sharing lifecycle state.
This commit is contained in:
Generated
+5
@@ -2385,6 +2385,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"android_logger",
|
||||
"easytier",
|
||||
"easytier-ffi",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
@@ -2396,13 +2397,17 @@ dependencies = [
|
||||
name = "easytier-ffi"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"dashmap",
|
||||
"easytier",
|
||||
"log",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
||||
@@ -13,4 +13,5 @@ log = "0.4"
|
||||
android_logger = "0.13"
|
||||
serde = { version = "1.0.220", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
easytier = { path = "../../easytier" }
|
||||
easytier = { path = "../../easytier" }
|
||||
easytier-ffi = { path = "../easytier-ffi", default-features = false }
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
fn main() {
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
if !matches!(target_os.as_str(), "android" | "linux") {
|
||||
return;
|
||||
}
|
||||
|
||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let exports = manifest_dir.join("exports.map");
|
||||
println!("cargo:rerun-if-changed={}", exports.display());
|
||||
println!(
|
||||
"cargo:rustc-cdylib-link-arg=-Wl,--version-script={}",
|
||||
exports.display()
|
||||
);
|
||||
println!("cargo:rustc-cdylib-link-arg=-Wl,--exclude-libs,ALL");
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
global:
|
||||
Java_com_easytier_jni_EasyTierJNI_*;
|
||||
local:
|
||||
*;
|
||||
};
|
||||
@@ -1,5 +1,9 @@
|
||||
package com.easytier.jni
|
||||
|
||||
fun interface ConfigServerEventCallback {
|
||||
fun onEvent(eventJson: String)
|
||||
}
|
||||
|
||||
/** EasyTier JNI 接口类 提供 Android 应用调用 EasyTier 网络功能的接口 */
|
||||
object EasyTierJNI {
|
||||
|
||||
@@ -33,6 +37,35 @@ object EasyTierJNI {
|
||||
*/
|
||||
@JvmStatic external fun runNetworkInstance(config: String): Int
|
||||
|
||||
/**
|
||||
* 启动配置服务器客户端
|
||||
* @param url 配置服务器 URL
|
||||
* @param hostname 主机名,传入 null 使用系统主机名
|
||||
* @param machineId 稳定机器 ID,由调用方负责持久化
|
||||
* @param secureMode 是否启用 secure mode
|
||||
* @param callback 远程配置应用/删除事件回调
|
||||
* @return 0 表示成功,-1 表示失败
|
||||
* @throws RuntimeException 当客户端启动失败时抛出异常
|
||||
*/
|
||||
@JvmStatic
|
||||
external fun startConfigServerClient(
|
||||
url: String,
|
||||
hostname: String?,
|
||||
machineId: String,
|
||||
secureMode: Boolean,
|
||||
callback: ConfigServerEventCallback?
|
||||
): Int
|
||||
|
||||
/**
|
||||
* 停止配置服务器客户端
|
||||
* @return 0 表示成功,-1 表示失败
|
||||
* @throws RuntimeException 当客户端停止失败时抛出异常
|
||||
*/
|
||||
@JvmStatic external fun stopConfigServerClient(): Int
|
||||
|
||||
/** 查询配置服务器客户端是否已连接 */
|
||||
@JvmStatic external fun isConfigServerClientConnected(): Boolean
|
||||
|
||||
/**
|
||||
* 保留指定的网络实例,停止其他实例
|
||||
* @param instanceNames 要保留的实例名称数组,传入 null 或空数组将停止所有实例
|
||||
@@ -44,7 +77,7 @@ object EasyTierJNI {
|
||||
/**
|
||||
* 收集网络信息
|
||||
* @param maxLength 最大返回条目数
|
||||
* @return 包含网络信息的字符串数组,每个元素格式为 "key=value"
|
||||
* @return 包含网络信息的 JSON 字符串
|
||||
* @throws RuntimeException 当操作失败时抛出异常
|
||||
*/
|
||||
@JvmStatic external fun collectNetworkInfos(maxLength: Int): String?
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
use std::{
|
||||
ffi::{CStr, c_char, c_void},
|
||||
sync::{Arc, Mutex, MutexGuard},
|
||||
};
|
||||
|
||||
use easytier_ffi::ConfigServerEventCallback;
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{GlobalRef, JObject, JValue};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::error;
|
||||
|
||||
pub(crate) struct JniConfigServerCallback {
|
||||
java_vm: jni::JavaVM,
|
||||
callback: GlobalRef,
|
||||
}
|
||||
|
||||
static CONFIG_SERVER_CALLBACK: Lazy<Mutex<Option<Arc<JniConfigServerCallback>>>> =
|
||||
Lazy::new(|| Mutex::new(None));
|
||||
|
||||
pub(crate) fn lock_callback_storage()
|
||||
-> Result<MutexGuard<'static, Option<Arc<JniConfigServerCallback>>>, String> {
|
||||
CONFIG_SERVER_CALLBACK
|
||||
.lock()
|
||||
.map_err(|e| format!("Failed to lock config server callback: {}", e))
|
||||
}
|
||||
|
||||
pub(crate) fn new_callback(
|
||||
env: &mut JNIEnv,
|
||||
callback: &JObject,
|
||||
) -> Result<Arc<JniConfigServerCallback>, String> {
|
||||
let java_vm = env
|
||||
.get_java_vm()
|
||||
.map_err(|e| format!("Failed to get JavaVM: {:?}", e))?;
|
||||
let callback = env
|
||||
.new_global_ref(callback)
|
||||
.map_err(|e| format!("Failed to create callback global ref: {:?}", e))?;
|
||||
Ok(Arc::new(JniConfigServerCallback { java_vm, callback }))
|
||||
}
|
||||
|
||||
pub(crate) fn callback_fn(
|
||||
callback: &Option<Arc<JniConfigServerCallback>>,
|
||||
) -> ConfigServerEventCallback {
|
||||
callback
|
||||
.as_ref()
|
||||
.map(|_| config_server_event_callback as unsafe extern "C" fn(*const c_char, *mut c_void))
|
||||
}
|
||||
|
||||
pub(crate) fn user_data(callback: &Option<Arc<JniConfigServerCallback>>) -> *mut c_void {
|
||||
callback
|
||||
.as_ref()
|
||||
.map(|callback| Arc::as_ptr(callback) as *mut c_void)
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
impl JniConfigServerCallback {
|
||||
fn clear_pending_exception(
|
||||
env: &mut JNIEnv,
|
||||
context: &str,
|
||||
error: &dyn std::fmt::Debug,
|
||||
) -> String {
|
||||
match env.exception_check() {
|
||||
Ok(true) => {
|
||||
if let Err(clear_err) = env.exception_clear() {
|
||||
return format!(
|
||||
"{}: {:?}; failed to clear pending Java exception: {:?}",
|
||||
context, error, clear_err
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(check_err) => {
|
||||
return format!(
|
||||
"{}: {:?}; failed to check pending Java exception: {:?}",
|
||||
context, error, check_err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
format!("{}: {:?}", context, error)
|
||||
}
|
||||
|
||||
fn on_event(&self, event_json: *const c_char) -> Result<(), String> {
|
||||
let event_json = unsafe { CStr::from_ptr(event_json) }
|
||||
.to_str()
|
||||
.map_err(|e| format!("Invalid config server event JSON: {:?}", e))?;
|
||||
let mut env = self
|
||||
.java_vm
|
||||
.attach_current_thread()
|
||||
.map_err(|e| format!("Failed to attach callback thread: {:?}", e))?;
|
||||
let event_json = env.new_string(event_json).map_err(|e| {
|
||||
Self::clear_pending_exception(&mut env, "Failed to create event string", &e)
|
||||
})?;
|
||||
|
||||
if let Err(e) = env.call_method(
|
||||
self.callback.as_obj(),
|
||||
"onEvent",
|
||||
"(Ljava/lang/String;)V",
|
||||
&[JValue::from(&event_json)],
|
||||
) {
|
||||
return Err(Self::clear_pending_exception(
|
||||
&mut env,
|
||||
"Failed to call config server callback",
|
||||
&e,
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn config_server_event_callback(
|
||||
event_json: *const c_char,
|
||||
user_data: *mut c_void,
|
||||
) {
|
||||
if event_json.is_null() || user_data.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
let callback = unsafe { &*(user_data as *const JniConfigServerCallback) };
|
||||
|
||||
if let Err(error) = callback.on_event(event_json) {
|
||||
error::set_callback_error(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
use std::ptr;
|
||||
|
||||
use easytier_ffi::{
|
||||
in_config_server_callback, is_config_server_client_connected, start_config_server_client,
|
||||
stop_config_server_client,
|
||||
};
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JObject, JString};
|
||||
use jni::sys::{JNI_FALSE, JNI_TRUE, jboolean, jint};
|
||||
|
||||
use crate::{
|
||||
callback, error,
|
||||
strings::{jstring_to_cstring, optional_jstring_to_cstring},
|
||||
};
|
||||
|
||||
pub(crate) fn start_config_server_client_jni(
|
||||
env: &mut JNIEnv,
|
||||
config_server_url: JString,
|
||||
hostname: JString,
|
||||
machine_id: JString,
|
||||
secure_mode: jboolean,
|
||||
callback_obj: JObject,
|
||||
) -> jint {
|
||||
if in_config_server_callback() {
|
||||
error::throw_exception(
|
||||
env,
|
||||
"Cannot start config server client from config server callback",
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
|
||||
let config_server_url = match jstring_to_cstring(env, &config_server_url) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
error::throw_exception(env, &format!("Invalid config server URL: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let hostname = match optional_jstring_to_cstring(env, &hostname) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
error::throw_exception(env, &format!("Invalid hostname: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let machine_id = match jstring_to_cstring(env, &machine_id) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
error::throw_exception(env, &format!("Invalid machine ID: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let callback_ref = if callback_obj.is_null() {
|
||||
None
|
||||
} else {
|
||||
match callback::new_callback(env, &callback_obj) {
|
||||
Ok(state) => Some(state),
|
||||
Err(e) => {
|
||||
error::throw_exception(env, &e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut callback_guard = match callback::lock_callback_storage() {
|
||||
Ok(guard) => guard,
|
||||
Err(e) => {
|
||||
error::throw_exception(env, &e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
if callback_guard.is_none() {
|
||||
error::clear_callback_error();
|
||||
}
|
||||
|
||||
let callback_fn = callback::callback_fn(&callback_ref);
|
||||
let user_data = callback::user_data(&callback_ref);
|
||||
let result = unsafe {
|
||||
start_config_server_client(
|
||||
config_server_url.as_ptr(),
|
||||
hostname
|
||||
.as_ref()
|
||||
.map(|value| value.as_ptr())
|
||||
.unwrap_or(ptr::null()),
|
||||
machine_id.as_ptr(),
|
||||
secure_mode == JNI_TRUE,
|
||||
callback_fn,
|
||||
user_data,
|
||||
)
|
||||
};
|
||||
if result != 0 {
|
||||
if let Some(error_msg) = error::get_last_error() {
|
||||
error::throw_exception(env, &error_msg);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
*callback_guard = callback_ref;
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn stop_config_server_client_jni(mut env: JNIEnv, _class: JClass) -> jint {
|
||||
if in_config_server_callback() {
|
||||
let result = stop_config_server_client();
|
||||
if result != 0
|
||||
&& let Some(error_msg) = error::get_last_error()
|
||||
{
|
||||
error::throw_exception(&mut env, &error_msg);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
let mut callback_guard = match callback::lock_callback_storage() {
|
||||
Ok(guard) => guard,
|
||||
Err(e) => {
|
||||
error::throw_exception(&mut env, &e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let result = stop_config_server_client();
|
||||
if result != 0 {
|
||||
if let Some(error_msg) = error::get_last_error() {
|
||||
error::throw_exception(&mut env, &error_msg);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
*callback_guard = None;
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn is_config_server_client_connected_jni(_env: JNIEnv, _class: JClass) -> jboolean {
|
||||
if is_config_server_client_connected() != 0 {
|
||||
JNI_TRUE
|
||||
} else {
|
||||
JNI_FALSE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
use std::{
|
||||
ffi::{CStr, c_char},
|
||||
ptr,
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use easytier_ffi::{free_string, get_error_msg};
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::JClass;
|
||||
use jni::sys::jstring;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static JNI_CALLBACK_ERROR: Lazy<Mutex<Option<String>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
pub(crate) fn set_callback_error(error: String) {
|
||||
log::error!("{}", error);
|
||||
if let Ok(mut guard) = JNI_CALLBACK_ERROR.lock() {
|
||||
*guard = Some(error);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear_callback_error() {
|
||||
if let Ok(mut guard) = JNI_CALLBACK_ERROR.lock() {
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn take_callback_error() -> Option<String> {
|
||||
JNI_CALLBACK_ERROR
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|mut guard| guard.take())
|
||||
}
|
||||
|
||||
fn get_ffi_last_error() -> Option<String> {
|
||||
unsafe {
|
||||
let mut error_ptr: *const c_char = ptr::null();
|
||||
get_error_msg(&mut error_ptr);
|
||||
if error_ptr.is_null() {
|
||||
None
|
||||
} else {
|
||||
let error_cstr = CStr::from_ptr(error_ptr);
|
||||
let error_str = error_cstr.to_string_lossy().into_owned();
|
||||
free_string(error_ptr);
|
||||
Some(error_str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_last_error() -> Option<String> {
|
||||
match (get_ffi_last_error(), take_callback_error()) {
|
||||
(Some(ffi_error), Some(callback_error)) => Some(format!(
|
||||
"{}; config server callback error: {}",
|
||||
ffi_error, callback_error
|
||||
)),
|
||||
(Some(ffi_error), None) => Some(ffi_error),
|
||||
(None, Some(callback_error)) => Some(callback_error),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn throw_exception(env: &mut JNIEnv, message: &str) {
|
||||
let _ = env.throw_new("java/lang/RuntimeException", message);
|
||||
}
|
||||
|
||||
pub(crate) fn get_last_error_jni(env: JNIEnv, _class: JClass) -> jstring {
|
||||
match get_last_error() {
|
||||
Some(error) => match env.new_string(&error) {
|
||||
Ok(jstr) => jstr.into_raw(),
|
||||
Err(_) => ptr::null_mut(),
|
||||
},
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
@@ -1,319 +1,203 @@
|
||||
use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
|
||||
//! JNI facade for Android callers of EasyTier.
|
||||
//!
|
||||
//! This file intentionally lists every Java-visible native method exported by
|
||||
//! `libeasytier_android_jni.so`. The implementation details live in sibling
|
||||
//! modules so this facade stays readable as an API map.
|
||||
//!
|
||||
//! Network management APIs:
|
||||
//! - `setTunFd(instanceName, fd)`: attach an Android TUN fd to an instance.
|
||||
//! - `parseConfig(config)`: validate TOML config text.
|
||||
//! - `runNetworkInstance(config)`: start a local network instance.
|
||||
//! - `retainNetworkInstance(instanceNames)`: retain named instances and stop the rest.
|
||||
//! - `collectNetworkInfos()`: return running instance info as a JSON string.
|
||||
//!
|
||||
//! Config server client APIs:
|
||||
//! - `startConfigServerClient(url, hostname, machineId, secureMode, callback)`:
|
||||
//! start the managed remote config client.
|
||||
//! - `stopConfigServerClient()`: stop the managed client and release its Java callback.
|
||||
//! - `isConfigServerClientConnected()`: return whether the managed client is connected.
|
||||
//!
|
||||
//! Error API:
|
||||
//! - `getLastError()`: return the latest FFI/JNI error string for the calling thread.
|
||||
|
||||
mod callback;
|
||||
mod config_server_api;
|
||||
mod error;
|
||||
mod logger;
|
||||
mod network_api;
|
||||
mod strings;
|
||||
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JObjectArray, JString};
|
||||
use jni::sys::{jint, jstring};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::ptr;
|
||||
use jni::objects::{JClass, JObject, JObjectArray, JString};
|
||||
use jni::sys::{jboolean, jint, jstring};
|
||||
|
||||
// 定义 KeyValuePair 结构体
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct KeyValuePair {
|
||||
pub key: *const std::ffi::c_char,
|
||||
pub value: *const std::ffi::c_char,
|
||||
}
|
||||
|
||||
// 声明外部 C 函数
|
||||
unsafe extern "C" {
|
||||
fn set_tun_fd(inst_name: *const std::ffi::c_char, fd: std::ffi::c_int) -> std::ffi::c_int;
|
||||
fn get_error_msg(out: *mut *const std::ffi::c_char);
|
||||
fn free_string(s: *const std::ffi::c_char);
|
||||
fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int;
|
||||
fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int;
|
||||
fn retain_network_instance(
|
||||
inst_names: *const *const std::ffi::c_char,
|
||||
length: usize,
|
||||
) -> std::ffi::c_int;
|
||||
fn collect_network_infos(infos: *mut KeyValuePair, max_length: usize) -> std::ffi::c_int;
|
||||
}
|
||||
|
||||
// 初始化 Android 日志
|
||||
static LOGGER_INIT: Lazy<()> = Lazy::new(|| {
|
||||
android_logger::init_once(
|
||||
android_logger::Config::default()
|
||||
.with_max_level(log::LevelFilter::Debug)
|
||||
.with_tag("EasyTier-JNI"),
|
||||
);
|
||||
});
|
||||
|
||||
// 辅助函数:从 Java String 转换为 CString
|
||||
fn jstring_to_cstring(env: &mut JNIEnv, jstr: &JString) -> Result<CString, String> {
|
||||
let java_str = env
|
||||
.get_string(jstr)
|
||||
.map_err(|e| format!("Failed to get string: {:?}", e))?;
|
||||
let rust_str = java_str.to_str().map_err(|_| "Invalid UTF-8".to_string())?;
|
||||
CString::new(rust_str).map_err(|_| "String contains null byte".to_string())
|
||||
}
|
||||
|
||||
// 辅助函数:获取错误消息
|
||||
fn get_last_error() -> Option<String> {
|
||||
unsafe {
|
||||
let mut error_ptr: *const std::ffi::c_char = ptr::null();
|
||||
get_error_msg(&mut error_ptr);
|
||||
if error_ptr.is_null() {
|
||||
None
|
||||
} else {
|
||||
let error_cstr = CStr::from_ptr(error_ptr);
|
||||
let error_str = error_cstr.to_string_lossy().into_owned();
|
||||
free_string(error_ptr);
|
||||
Some(error_str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数:抛出 Java 异常
|
||||
fn throw_exception(env: &mut JNIEnv, message: &str) {
|
||||
let _ = env.throw_new("java/lang/RuntimeException", message);
|
||||
}
|
||||
|
||||
/// 设置 TUN 文件描述符
|
||||
/// Attach a TUN file descriptor to an EasyTier network instance.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.setTunFd(instanceName: String, fd: Int): Int`
|
||||
///
|
||||
/// `instanceName` must name an instance known to the shared FFI instance cache.
|
||||
/// The `fd` must be a valid Android TUN file descriptor. On failure this
|
||||
/// returns `-1` and throws `RuntimeException` with the FFI error message when
|
||||
/// one is available.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_setTunFd(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
inst_name: JString,
|
||||
fd: jint,
|
||||
) -> jint {
|
||||
Lazy::force(&LOGGER_INIT);
|
||||
|
||||
let inst_name_cstr = match jstring_to_cstring(&mut env, &inst_name) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid instance name: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let result = set_tun_fd(inst_name_cstr.as_ptr(), fd);
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
logger::init();
|
||||
network_api::set_tun_fd_jni(env, class, inst_name, fd)
|
||||
}
|
||||
|
||||
/// 解析配置
|
||||
/// Validate a TOML network config string.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.parseConfig(config: String): Int`
|
||||
///
|
||||
/// This only validates the config text; it does not start or mutate any
|
||||
/// instance. On failure this returns `-1` and throws `RuntimeException`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_parseConfig(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
config: JString,
|
||||
) -> jint {
|
||||
Lazy::force(&LOGGER_INIT);
|
||||
|
||||
let config_cstr = match jstring_to_cstring(&mut env, &config) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid config string: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let result = parse_config(config_cstr.as_ptr());
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
logger::init();
|
||||
network_api::parse_config_jni(env, class, config)
|
||||
}
|
||||
|
||||
/// 运行网络实例
|
||||
/// Start one local EasyTier network instance from TOML config text.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.runNetworkInstance(config: String): Int`
|
||||
///
|
||||
/// The instance name in the config must be unique in the FFI instance cache.
|
||||
/// On failure this returns `-1` and throws `RuntimeException`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_runNetworkInstance(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
config: JString,
|
||||
) -> jint {
|
||||
Lazy::force(&LOGGER_INIT);
|
||||
|
||||
let config_cstr = match jstring_to_cstring(&mut env, &config) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid config string: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let result = run_network_instance(config_cstr.as_ptr());
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
logger::init();
|
||||
network_api::run_network_instance_jni(env, class, config)
|
||||
}
|
||||
|
||||
/// 保持网络实例
|
||||
/// Retain the named network instances and stop all other instances.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.retainNetworkInstance(instanceNames: Array<String>?): Int`
|
||||
///
|
||||
/// Passing `null` or an empty array stops all instances. Null elements inside a
|
||||
/// non-empty array are skipped. On failure this returns `-1` and throws
|
||||
/// `RuntimeException`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
instance_names: JObjectArray,
|
||||
) -> jint {
|
||||
Lazy::force(&LOGGER_INIT);
|
||||
|
||||
// 处理 null 数组的情况
|
||||
if instance_names.is_null() {
|
||||
unsafe {
|
||||
let result = retain_network_instance(ptr::null(), 0);
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取数组长度
|
||||
let array_length = match env.get_array_length(&instance_names) {
|
||||
Ok(len) => len as usize,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Failed to get array length: {:?}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
// 如果数组为空,停止所有实例
|
||||
if array_length == 0 {
|
||||
unsafe {
|
||||
let result = retain_network_instance(ptr::null(), 0);
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 转换 Java 字符串数组为 C 字符串数组
|
||||
let mut c_strings = Vec::with_capacity(array_length);
|
||||
let mut c_string_ptrs = Vec::with_capacity(array_length);
|
||||
|
||||
for i in 0..array_length {
|
||||
let java_string = match env.get_object_array_element(&instance_names, i as i32) {
|
||||
Ok(obj) => obj,
|
||||
Err(e) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Failed to get array element {}: {:?}", i, e),
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
if java_string.is_null() {
|
||||
continue; // 跳过 null 元素
|
||||
}
|
||||
|
||||
let jstring = JString::from(java_string);
|
||||
let c_string = match jstring_to_cstring(&mut env, &jstring) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Invalid instance name at index {}: {}", i, e),
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
c_string_ptrs.push(c_string.as_ptr());
|
||||
c_strings.push(c_string); // 保持 CString 的所有权
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let result = retain_network_instance(c_string_ptrs.as_ptr(), c_string_ptrs.len());
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
logger::init();
|
||||
network_api::retain_network_instance_jni(env, class, instance_names)
|
||||
}
|
||||
|
||||
/// 收集网络信息
|
||||
/// Collect running network instance information.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.collectNetworkInfos(maxLength: Int): String?`
|
||||
///
|
||||
/// Returns a JSON string containing `NetworkInstanceRunningInfoMap`, or null if
|
||||
/// collection fails. `maxLength` limits how many FFI entries are collected. On
|
||||
/// failure this throws `RuntimeException` when an error message is available.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_collectNetworkInfos(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
max_length: jint,
|
||||
) -> jstring {
|
||||
Lazy::force(&LOGGER_INIT);
|
||||
|
||||
const MAX_INFOS: usize = 100;
|
||||
let mut infos = vec![
|
||||
KeyValuePair {
|
||||
key: ptr::null(),
|
||||
value: ptr::null(),
|
||||
};
|
||||
MAX_INFOS
|
||||
];
|
||||
|
||||
unsafe {
|
||||
let count = collect_network_infos(infos.as_mut_ptr(), MAX_INFOS);
|
||||
if count < 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let mut ret = NetworkInstanceRunningInfoMap::default();
|
||||
|
||||
// 使用 serde_json 构建 JSON
|
||||
for info in infos.iter().take(count as usize) {
|
||||
let key_ptr = info.key;
|
||||
let val_ptr = info.value;
|
||||
if key_ptr.is_null() || val_ptr.is_null() {
|
||||
break;
|
||||
}
|
||||
|
||||
let key = CStr::from_ptr(key_ptr).to_string_lossy();
|
||||
let val = CStr::from_ptr(val_ptr).to_string_lossy();
|
||||
let value = match serde_json::from_str::<NetworkInstanceRunningInfo>(val.as_ref()) {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
throw_exception(&mut env, "Failed to parse JSON");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
ret.map.insert(key.to_string(), value);
|
||||
}
|
||||
|
||||
let json_str = serde_json::to_string(&ret).unwrap_or_else(|_| "{}".to_string());
|
||||
|
||||
match env.new_string(&json_str) {
|
||||
Ok(jstr) => jstr.into_raw(),
|
||||
Err(_) => {
|
||||
throw_exception(&mut env, "Failed to create JSON string");
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
logger::init();
|
||||
network_api::collect_network_infos_jni(env, class, max_length)
|
||||
}
|
||||
|
||||
/// 获取最后的错误信息
|
||||
/// Return the latest FFI/JNI error string for the calling thread.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.getLastError(): String?`
|
||||
///
|
||||
/// This combines the FFI thread-local error with any pending config-server Java
|
||||
/// callback error. It returns null when no error is available.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_getLastError(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
class: JClass,
|
||||
) -> jstring {
|
||||
match get_last_error() {
|
||||
Some(error) => match env.new_string(&error) {
|
||||
Ok(jstr) => jstr.into_raw(),
|
||||
Err(_) => ptr::null_mut(),
|
||||
},
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
error::get_last_error_jni(env, class)
|
||||
}
|
||||
|
||||
/// Start the managed config-server client.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.startConfigServerClient(url, hostname, machineId, secureMode, callback): Int`
|
||||
///
|
||||
/// JNI only converts Java values and keeps the Java callback alive. The FFI
|
||||
/// layer owns singleton lifecycle, config-server/data-plane mutual exclusion,
|
||||
/// remote instance tracking, and callback event timing. If `callback` is
|
||||
/// non-null, each remote apply/delete event is delivered to
|
||||
/// `ConfigServerEventCallback.onEvent(eventJson)`.
|
||||
///
|
||||
/// On failure this returns `-1` and throws `RuntimeException`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_startConfigServerClient(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
config_server_url: JString,
|
||||
hostname: JString,
|
||||
machine_id: JString,
|
||||
secure_mode: jboolean,
|
||||
callback: JObject,
|
||||
) -> jint {
|
||||
logger::init();
|
||||
config_server_api::start_config_server_client_jni(
|
||||
&mut env,
|
||||
config_server_url,
|
||||
hostname,
|
||||
machine_id,
|
||||
secure_mode,
|
||||
callback,
|
||||
)
|
||||
}
|
||||
|
||||
/// Stop the managed config-server client.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.stopConfigServerClient(): Int`
|
||||
///
|
||||
/// The FFI layer performs the actual stop and managed instance cleanup. JNI
|
||||
/// releases the Java callback reference after FFI stop succeeds. On failure
|
||||
/// this returns `-1` and throws `RuntimeException`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_stopConfigServerClient(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
) -> jint {
|
||||
logger::init();
|
||||
config_server_api::stop_config_server_client_jni(env, class)
|
||||
}
|
||||
|
||||
/// Report whether the managed config-server client is connected.
|
||||
///
|
||||
/// Java signature:
|
||||
/// `EasyTierJNI.isConfigServerClientConnected(): Boolean`
|
||||
///
|
||||
/// Returns `JNI_TRUE` only when the FFI config-server client exists and reports
|
||||
/// connected.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_isConfigServerClientConnected(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
) -> jboolean {
|
||||
logger::init();
|
||||
config_server_api::is_config_server_client_connected_jni(env, class)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static LOGGER_INIT: Lazy<()> = Lazy::new(|| {
|
||||
android_logger::init_once(
|
||||
android_logger::Config::default()
|
||||
.with_max_level(log::LevelFilter::Debug)
|
||||
.with_tag("EasyTier-JNI"),
|
||||
);
|
||||
});
|
||||
|
||||
pub(crate) fn init() {
|
||||
Lazy::force(&LOGGER_INIT);
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
use std::{ffi::CStr, ptr};
|
||||
|
||||
use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
|
||||
use easytier_ffi::{
|
||||
KeyValuePair, collect_network_infos, parse_config, retain_network_instance,
|
||||
run_network_instance, set_tun_fd,
|
||||
};
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JObjectArray, JString};
|
||||
use jni::sys::{jint, jstring};
|
||||
|
||||
use crate::{
|
||||
error::{get_last_error, throw_exception},
|
||||
strings::jstring_to_cstring,
|
||||
};
|
||||
|
||||
pub(crate) fn set_tun_fd_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
inst_name: JString,
|
||||
fd: jint,
|
||||
) -> jint {
|
||||
let inst_name_cstr = match jstring_to_cstring(&mut env, &inst_name) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid instance name: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
unsafe {
|
||||
let result = set_tun_fd(inst_name_cstr.as_ptr(), fd);
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_config_jni(mut env: JNIEnv, _class: JClass, config: JString) -> jint {
|
||||
let config_cstr = match jstring_to_cstring(&mut env, &config) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid config string: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
unsafe {
|
||||
let result = parse_config(config_cstr.as_ptr());
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn run_network_instance_jni(mut env: JNIEnv, _class: JClass, config: JString) -> jint {
|
||||
let config_cstr = match jstring_to_cstring(&mut env, &config) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Invalid config string: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
unsafe {
|
||||
let result = run_network_instance(config_cstr.as_ptr());
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn retain_network_instance_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
instance_names: JObjectArray,
|
||||
) -> jint {
|
||||
if instance_names.is_null() {
|
||||
return retain_all(&mut env);
|
||||
}
|
||||
|
||||
let array_length = match env.get_array_length(&instance_names) {
|
||||
Ok(len) => len as usize,
|
||||
Err(e) => {
|
||||
throw_exception(&mut env, &format!("Failed to get array length: {:?}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
if array_length == 0 {
|
||||
return retain_all(&mut env);
|
||||
}
|
||||
|
||||
let mut c_strings = Vec::with_capacity(array_length);
|
||||
let mut c_string_ptrs = Vec::with_capacity(array_length);
|
||||
|
||||
for i in 0..array_length {
|
||||
let java_string = match env.get_object_array_element(&instance_names, i as i32) {
|
||||
Ok(obj) => obj,
|
||||
Err(e) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Failed to get array element {}: {:?}", i, e),
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
if java_string.is_null() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let jstring = JString::from(java_string);
|
||||
let c_string = match jstring_to_cstring(&mut env, &jstring) {
|
||||
Ok(cstr) => cstr,
|
||||
Err(e) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Invalid instance name at index {}: {}", i, e),
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
c_string_ptrs.push(c_string.as_ptr());
|
||||
c_strings.push(c_string);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let result = retain_network_instance(c_string_ptrs.as_ptr(), c_string_ptrs.len());
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fn retain_all(env: &mut JNIEnv) -> jint {
|
||||
unsafe {
|
||||
let result = retain_network_instance(ptr::null(), 0);
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn collect_network_infos_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
max_length: jint,
|
||||
) -> jstring {
|
||||
let max_length = max_length.max(0) as usize;
|
||||
let mut infos = vec![
|
||||
KeyValuePair {
|
||||
key: ptr::null(),
|
||||
value: ptr::null(),
|
||||
};
|
||||
max_length
|
||||
];
|
||||
|
||||
unsafe {
|
||||
let count = collect_network_infos(infos.as_mut_ptr(), max_length);
|
||||
if count < 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let mut ret = NetworkInstanceRunningInfoMap::default();
|
||||
for info in infos.iter().take(count as usize) {
|
||||
let key_ptr = info.key;
|
||||
let val_ptr = info.value;
|
||||
if key_ptr.is_null() || val_ptr.is_null() {
|
||||
break;
|
||||
}
|
||||
|
||||
let key = CStr::from_ptr(key_ptr).to_string_lossy();
|
||||
let val = CStr::from_ptr(val_ptr).to_string_lossy();
|
||||
let value = match serde_json::from_str::<NetworkInstanceRunningInfo>(val.as_ref()) {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
throw_exception(&mut env, "Failed to parse JSON");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
ret.map.insert(key.to_string(), value);
|
||||
}
|
||||
|
||||
let json_str = serde_json::to_string(&ret).unwrap_or_else(|_| "{}".to_string());
|
||||
match env.new_string(&json_str) {
|
||||
Ok(jstr) => jstr.into_raw(),
|
||||
Err(_) => {
|
||||
throw_exception(&mut env, "Failed to create JSON string");
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
use std::ffi::CString;
|
||||
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::JString;
|
||||
|
||||
pub(crate) fn jstring_to_cstring(env: &mut JNIEnv, jstr: &JString) -> Result<CString, String> {
|
||||
let java_str = env
|
||||
.get_string(jstr)
|
||||
.map_err(|e| format!("Failed to get string: {:?}", e))?;
|
||||
let rust_str = java_str.to_str().map_err(|_| "Invalid UTF-8".to_string())?;
|
||||
CString::new(rust_str).map_err(|_| "String contains null byte".to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn optional_jstring_to_cstring(
|
||||
env: &mut JNIEnv,
|
||||
jstr: &JString,
|
||||
) -> Result<Option<CString>, String> {
|
||||
if jstr.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
jstring_to_cstring(env, jstr).map(Some)
|
||||
}
|
||||
@@ -4,10 +4,11 @@ version = "0.1.0"
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = ["ffi-dataplane"]
|
||||
default = ["c-abi", "ffi-dataplane"]
|
||||
c-abi = []
|
||||
ffi-dataplane = ["easytier/ffi-dataplane"]
|
||||
|
||||
[dependencies]
|
||||
@@ -15,9 +16,13 @@ easytier = { path = "../../easytier" }
|
||||
|
||||
once_cell = "1.18.0"
|
||||
dashmap = "6.0"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "io-util", "time", "sync", "macros"] }
|
||||
async-trait = "0.1"
|
||||
log = "0.4"
|
||||
percent-encoding = "2.3"
|
||||
url = "2"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
uuid = "1.17.0"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "io-util", "time", "sync", "macros"] }
|
||||
tokio-util = "0.7"
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h> // for sleep
|
||||
|
||||
// FFI struct and function declarations
|
||||
typedef struct {
|
||||
const char* key;
|
||||
const char* value;
|
||||
} KeyValuePair;
|
||||
|
||||
typedef void (*config_server_event_callback)(
|
||||
const char* event_json,
|
||||
void* user_data
|
||||
);
|
||||
|
||||
extern int parse_config(const char* cfg_str);
|
||||
extern int run_network_instance(const char* cfg_str);
|
||||
extern void get_error_msg(const char** out);
|
||||
extern void free_string(const char* s);
|
||||
extern int collect_network_infos(KeyValuePair* infos, size_t max_length);
|
||||
extern int start_config_server_client(
|
||||
const char* config_server_url,
|
||||
const char* hostname,
|
||||
const char* machine_id,
|
||||
bool secure_mode,
|
||||
config_server_event_callback callback,
|
||||
void* user_data
|
||||
);
|
||||
extern int stop_config_server_client(void);
|
||||
extern int is_config_server_client_connected(void);
|
||||
|
||||
static void on_config_server_event(const char* event_json, void* user_data) {
|
||||
(void)user_data;
|
||||
printf("config server event: %s\n", event_json);
|
||||
}
|
||||
|
||||
int main() {
|
||||
const char* config = "inst_name = \"test\"\nnetwork = \"test_network\"\n";
|
||||
int ret;
|
||||
|
||||
// 调用 parse_config
|
||||
ret = parse_config(config);
|
||||
if (ret != 0) {
|
||||
const char* err = NULL;
|
||||
get_error_msg(&err);
|
||||
if (err) {
|
||||
printf("parse_config error: %s\n", err);
|
||||
free_string(err);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
printf("parse_config success\n");
|
||||
|
||||
// 调用 run_network_instance
|
||||
ret = run_network_instance(config);
|
||||
if (ret != 0) {
|
||||
const char* err = NULL;
|
||||
get_error_msg(&err);
|
||||
if (err) {
|
||||
printf("run_network_instance error: %s\n", err);
|
||||
free_string(err);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
printf("run_network_instance success\n");
|
||||
|
||||
// 周期性调用 collect_network_infos 并打印
|
||||
const size_t max_infos = 8;
|
||||
KeyValuePair* infos = (KeyValuePair*)malloc(sizeof(KeyValuePair) * max_infos);
|
||||
if (!infos) {
|
||||
fprintf(stderr, "malloc failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; ++i) { // 循环5次作为示例
|
||||
memset(infos, 0, sizeof(KeyValuePair) * max_infos);
|
||||
int count = collect_network_infos(infos, max_infos);
|
||||
if (count < 0) {
|
||||
const char* err = NULL;
|
||||
get_error_msg(&err);
|
||||
if (err) {
|
||||
printf("collect_network_infos error: %s\n", err);
|
||||
free_string(err);
|
||||
}
|
||||
break;
|
||||
}
|
||||
printf("collect_network_infos: %d instance(s)\n", count);
|
||||
for (int j = 0; j < count; ++j) {
|
||||
printf(" [%d] key: %s\n value: %s\n", j, infos[j].key, infos[j].value);
|
||||
free_string(infos[j].key);
|
||||
free_string(infos[j].value);
|
||||
}
|
||||
sleep(1);
|
||||
}
|
||||
free(infos);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
use std::{
|
||||
cell::Cell,
|
||||
collections::HashSet,
|
||||
ffi::{CString, c_char, c_int, c_void},
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use easytier::{
|
||||
common::{
|
||||
MachineIdOptions,
|
||||
config::{ConfigLoader as _, TomlConfigLoader},
|
||||
},
|
||||
tunnel::TunnelScheme,
|
||||
web_client::{WebClient, WebClientHooks, run_web_client},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
data_plane::remove_data_plane_handles_by_instance_ids,
|
||||
error::set_error_msg,
|
||||
state::{
|
||||
ASYNC_RUNTIME, INSTANCE_MANAGER, INSTANCE_MUTATION_LOCK, INSTANCE_NAME_ID_MAP,
|
||||
lock_remote_instance_mutation, remove_instance_name_ids,
|
||||
},
|
||||
strings::{c_str_to_string, optional_c_str_to_string},
|
||||
types::ConfigServerEventCallback,
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
static IN_CONFIG_SERVER_CALLBACK: Cell<bool> = const { Cell::new(false) };
|
||||
}
|
||||
|
||||
static CONFIG_SERVER_CLIENT: once_cell::sync::Lazy<Mutex<Option<ManagedConfigServerClient>>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(None));
|
||||
static CONFIG_SERVER_CLIENT_ACTIVE: once_cell::sync::Lazy<AtomicBool> =
|
||||
once_cell::sync::Lazy::new(|| AtomicBool::new(false));
|
||||
static CONFIG_SERVER_CLIENT_STOPPING: once_cell::sync::Lazy<AtomicBool> =
|
||||
once_cell::sync::Lazy::new(|| AtomicBool::new(false));
|
||||
static LAST_CONFIG_SERVER_CALLBACK_ERROR: once_cell::sync::Lazy<Mutex<Option<String>>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(None));
|
||||
|
||||
pub(crate) struct ConfigServerCallbackScope;
|
||||
|
||||
impl ConfigServerCallbackScope {
|
||||
pub(crate) fn enter() -> Self {
|
||||
IN_CONFIG_SERVER_CALLBACK.with(|in_callback| in_callback.set(true));
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ConfigServerCallbackScope {
|
||||
fn drop(&mut self) {
|
||||
IN_CONFIG_SERVER_CALLBACK.with(|in_callback| in_callback.set(false));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn in_config_server_callback() -> bool {
|
||||
IN_CONFIG_SERVER_CALLBACK.with(Cell::get)
|
||||
}
|
||||
|
||||
fn config_server_machine_id_options(machine_id: String) -> MachineIdOptions {
|
||||
MachineIdOptions {
|
||||
explicit_machine_id: Some(machine_id),
|
||||
state_dir: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_config_server_client_options(
|
||||
config_server_url_s: &str,
|
||||
machine_id: &str,
|
||||
) -> Result<(), String> {
|
||||
if machine_id.trim().is_empty() {
|
||||
return Err("machine_id is empty".to_string());
|
||||
}
|
||||
|
||||
let config_server_url = match url::Url::parse(config_server_url_s) {
|
||||
Ok(url) => url,
|
||||
Err(_) => format!(
|
||||
"udp://config-server.easytier.cn:22020/{}",
|
||||
config_server_url_s
|
||||
)
|
||||
.parse()
|
||||
.map_err(|err| format!("failed to parse config server URL: {}", err))?,
|
||||
};
|
||||
|
||||
TunnelScheme::try_from(&config_server_url).map_err(|_| {
|
||||
format!(
|
||||
"unsupported config server scheme: {}",
|
||||
config_server_url.scheme()
|
||||
)
|
||||
})?;
|
||||
|
||||
let token = config_server_url
|
||||
.path_segments()
|
||||
.and_then(|mut segments| segments.next_back())
|
||||
.map(|segment| percent_encoding::percent_decode_str(segment).decode_utf8())
|
||||
.transpose()
|
||||
.map_err(|err| format!("failed to decode config server token: {}", err))?
|
||||
.map(|token| token.to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
if token.is_empty() {
|
||||
return Err("empty token".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct ManagedConfigServerClient {
|
||||
client: WebClient,
|
||||
hooks: Arc<ManagedConfigServerClientHooks>,
|
||||
}
|
||||
|
||||
pub(crate) struct ManagedConfigServerClientHooks {
|
||||
pub(crate) instance_ids: Mutex<HashSet<Uuid>>,
|
||||
callback_delivery: Mutex<()>,
|
||||
stopping: AtomicBool,
|
||||
callback: ConfigServerEventCallback,
|
||||
user_data: usize,
|
||||
}
|
||||
|
||||
impl ManagedConfigServerClientHooks {
|
||||
pub(crate) fn new(callback: ConfigServerEventCallback, user_data: *mut c_void) -> Self {
|
||||
Self {
|
||||
instance_ids: Mutex::new(HashSet::new()),
|
||||
callback_delivery: Mutex::new(()),
|
||||
stopping: AtomicBool::new(false),
|
||||
callback,
|
||||
user_data: user_data as usize,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn tracked_instance_ids(&self) -> Vec<Uuid> {
|
||||
self.instance_ids
|
||||
.lock()
|
||||
.map(|guard| guard.iter().copied().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn remove_tracked_instance_ids(&self, ids: &[Uuid]) -> Result<Vec<Uuid>, String> {
|
||||
let mut guard = self.instance_ids.lock().map_err(|err| err.to_string())?;
|
||||
Ok(ids
|
||||
.iter()
|
||||
.filter_map(|id| guard.remove(id).then_some(*id))
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn validate_instance_name(&self, inst_name: &str, inst_id: Uuid) -> Result<(), String> {
|
||||
if let Some(existing_id) = INSTANCE_NAME_ID_MAP.get(inst_name).map(|id| *id)
|
||||
&& existing_id != inst_id
|
||||
{
|
||||
return Err(format!("instance name {} already exists", inst_name));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn commit_instance_name(&self, inst_name: String, inst_id: Uuid) -> Result<(), String> {
|
||||
INSTANCE_NAME_ID_MAP.retain(|_, existing_id| *existing_id != inst_id);
|
||||
self.validate_instance_name(&inst_name, inst_id)?;
|
||||
INSTANCE_NAME_ID_MAP.insert(inst_name, inst_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn start_stopping(&self) -> Vec<Uuid> {
|
||||
let _delivery_guard = if in_config_server_callback() {
|
||||
None
|
||||
} else {
|
||||
self.callback_delivery.lock().ok()
|
||||
};
|
||||
let mut guard = match self.instance_ids.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
self.stopping.store(true, Ordering::Release);
|
||||
guard.drain().collect()
|
||||
}
|
||||
|
||||
pub(crate) fn note_callback_error(&self, error: String) {
|
||||
log::warn!("config server event callback failed: {}", error);
|
||||
if let Ok(mut guard) = LAST_CONFIG_SERVER_CALLBACK_ERROR.lock() {
|
||||
*guard = Some(error);
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_event_with_delivery_locked(
|
||||
&self,
|
||||
event: &str,
|
||||
instance_id: Uuid,
|
||||
) -> Result<(), String> {
|
||||
if self.stopping.load(Ordering::Acquire) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(callback) = self.callback else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let event_json = serde_json::json!({
|
||||
"event": event,
|
||||
"success": true,
|
||||
"instance_id": instance_id.to_string(),
|
||||
"error": null,
|
||||
})
|
||||
.to_string();
|
||||
let event_json = CString::new(event_json).map_err(|err| err.to_string())?;
|
||||
let _callback_scope = ConfigServerCallbackScope::enter();
|
||||
unsafe {
|
||||
callback(event_json.as_ptr(), self.user_data as *mut c_void);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_event(&self, event: &str, instance_id: Uuid) -> Result<(), String> {
|
||||
let _delivery_guard = self
|
||||
.callback_delivery
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?;
|
||||
self.emit_event_with_delivery_locked(event, instance_id)
|
||||
}
|
||||
|
||||
fn wait_for_callback_delivery(&self) {
|
||||
if in_config_server_callback() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(guard) = self.callback_delivery.lock() {
|
||||
drop(guard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl WebClientHooks for ManagedConfigServerClientHooks {
|
||||
fn manages_remote_config_instances(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn pre_run_network_instance(&self, cfg: &TomlConfigLoader) -> Result<(), String> {
|
||||
if self.stopping.load(Ordering::Acquire) {
|
||||
return Err("config server client is stopping".to_string());
|
||||
}
|
||||
|
||||
let inst_name = cfg.get_inst_name();
|
||||
let inst_id = cfg.get_id();
|
||||
|
||||
self.validate_instance_name(&inst_name, inst_id)
|
||||
}
|
||||
|
||||
async fn post_run_network_instance(&self, id: &Uuid) -> Result<(), String> {
|
||||
let _delivery_guard = self
|
||||
.callback_delivery
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?;
|
||||
let Some(inst_name) = INSTANCE_MANAGER.get_instance_name(id) else {
|
||||
if !self.stopping.load(Ordering::Acquire) {
|
||||
return Err(format!("instance {} not found after start", id));
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
{
|
||||
let _mutation_guard = INSTANCE_MUTATION_LOCK
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?;
|
||||
if INSTANCE_MANAGER.get_instance_name(id).is_none() {
|
||||
if !self.stopping.load(Ordering::Acquire) {
|
||||
return Err(format!("instance {} not found after start", id));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let should_delete = {
|
||||
let mut guard = self.instance_ids.lock().map_err(|err| err.to_string())?;
|
||||
if self.stopping.load(Ordering::Acquire) {
|
||||
true
|
||||
} else {
|
||||
guard.insert(*id);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if should_delete {
|
||||
if let Err(err) = INSTANCE_MANAGER.delete_network_instance(vec![*id]) {
|
||||
return Err(err.to_string());
|
||||
}
|
||||
remove_instance_name_ids(&[*id]);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.stopping.load(Ordering::Acquire) {
|
||||
self.remove_tracked_instance_ids(&[*id])?;
|
||||
remove_instance_name_ids(&[*id]);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Err(err) = self.commit_instance_name(inst_name.clone(), *id) {
|
||||
self.remove_tracked_instance_ids(&[*id])?;
|
||||
if let Err(delete_err) = INSTANCE_MANAGER.delete_network_instance(vec![*id]) {
|
||||
return Err(format!(
|
||||
"{}; failed to delete duplicate instance: {}",
|
||||
err, delete_err
|
||||
));
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
if self.stopping.load(Ordering::Acquire) {
|
||||
self.remove_tracked_instance_ids(&[*id])?;
|
||||
remove_instance_name_ids(&[*id]);
|
||||
return Ok(());
|
||||
}
|
||||
if INSTANCE_MANAGER.get_instance_name(id).is_none() {
|
||||
self.remove_tracked_instance_ids(&[*id])?;
|
||||
remove_instance_name_ids(&[*id]);
|
||||
return Err(format!(
|
||||
"instance {} was removed before post-run completed",
|
||||
id
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
remove_data_plane_handles_by_instance_ids(&[*id]);
|
||||
|
||||
if let Err(err) = self.emit_event_with_delivery_locked("run_network_instance", *id) {
|
||||
self.note_callback_error(err);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn post_remove_network_instances(&self, ids: &[Uuid]) -> Result<(), String> {
|
||||
let removed_ids = {
|
||||
let _mutation_guard = INSTANCE_MUTATION_LOCK
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?;
|
||||
let removed_ids = self.remove_tracked_instance_ids(ids)?;
|
||||
remove_instance_name_ids(ids);
|
||||
remove_data_plane_handles_by_instance_ids(&removed_ids);
|
||||
removed_ids
|
||||
};
|
||||
|
||||
for id in removed_ids {
|
||||
if let Err(err) = self.emit_event("delete_network_instance", id) {
|
||||
self.note_callback_error(err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove_config_server_tracked_instance_ids(ids: &[Uuid]) {
|
||||
if ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(guard) = CONFIG_SERVER_CLIENT.lock()
|
||||
&& let Some(managed) = guard.as_ref()
|
||||
&& let Err(err) = managed.hooks.remove_tracked_instance_ids(ids)
|
||||
{
|
||||
log::warn!("failed to remove config server tracked ids: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn wait_for_config_server_delivery() {
|
||||
let hooks = CONFIG_SERVER_CLIENT
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| guard.as_ref().map(|managed| managed.hooks.clone()));
|
||||
if let Some(hooks) = hooks {
|
||||
hooks.wait_for_callback_delivery();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn last_callback_error() -> Option<String> {
|
||||
LAST_CONFIG_SERVER_CALLBACK_ERROR
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| guard.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn clear_last_callback_error() {
|
||||
if let Ok(mut guard) = LAST_CONFIG_SERVER_CALLBACK_ERROR.lock() {
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn is_config_server_active_or_stopping() -> bool {
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.load(Ordering::Acquire)
|
||||
|| CONFIG_SERVER_CLIENT_STOPPING.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn set_active_for_test(active: bool) {
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(active, Ordering::Release);
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Start the config server client.
|
||||
///
|
||||
/// `config_server_url` must be a valid null-terminated UTF-8 string.
|
||||
/// `hostname` may be null; if non-null it must be a valid null-terminated UTF-8 string.
|
||||
/// `machine_id` must be a valid null-terminated UTF-8 string.
|
||||
/// `event_json` passed to `callback` is valid only during that callback invocation.
|
||||
pub(crate) unsafe fn start_config_server_client(
|
||||
config_server_url: *const c_char,
|
||||
hostname: *const c_char,
|
||||
machine_id: *const c_char,
|
||||
secure_mode: bool,
|
||||
callback: ConfigServerEventCallback,
|
||||
user_data: *mut c_void,
|
||||
) -> c_int {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot start config server client from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let config_server_url = match unsafe { c_str_to_string(config_server_url, "config_server_url") }
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let hostname = match unsafe { optional_c_str_to_string(hostname, "hostname") } {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let machine_id = match unsafe { c_str_to_string(machine_id, "machine_id") } {
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
Ok(value) => value,
|
||||
};
|
||||
if let Err(err) = validate_config_server_client_options(&config_server_url, &machine_id) {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
let mut guard = match CONFIG_SERVER_CLIENT.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to lock config server client: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
if guard.is_some() {
|
||||
set_error_msg("config server client already exists");
|
||||
return -1;
|
||||
}
|
||||
if CONFIG_SERVER_CLIENT_STOPPING.load(Ordering::Acquire) {
|
||||
set_error_msg("config server client is stopping");
|
||||
return -1;
|
||||
}
|
||||
clear_last_callback_error();
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
let _data_plane_usage_guard = match crate::data_plane::lock_for_config_server_start() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&err);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(true, Ordering::Release);
|
||||
let hooks = Arc::new(ManagedConfigServerClientHooks::new(callback, user_data));
|
||||
let client = match ASYNC_RUNTIME.block_on(run_web_client(
|
||||
&config_server_url,
|
||||
config_server_machine_id_options(machine_id),
|
||||
hostname,
|
||||
secure_mode,
|
||||
INSTANCE_MANAGER.clone(),
|
||||
Some(hooks.clone()),
|
||||
)) {
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||
set_error_msg(&format!("failed to start config server client: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
*guard = Some(ManagedConfigServerClient { client, hooks });
|
||||
0
|
||||
}
|
||||
|
||||
pub(crate) fn stop_config_server_client() -> c_int {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot stop config server client from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let mut guard = match CONFIG_SERVER_CLIENT.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to lock config server client: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(managed) = guard.as_ref() else {
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||
return 0;
|
||||
};
|
||||
if CONFIG_SERVER_CLIENT_STOPPING.swap(true, Ordering::AcqRel) {
|
||||
set_error_msg("config server client is stopping");
|
||||
return -1;
|
||||
}
|
||||
let hooks = managed.hooks.clone();
|
||||
let managed = guard.take().expect("config server client exists");
|
||||
drop(guard);
|
||||
|
||||
let _remote_mutation_guard = lock_remote_instance_mutation();
|
||||
let tracked_ids = hooks.start_stopping();
|
||||
drop(managed);
|
||||
|
||||
let _mutation_guard = match INSTANCE_MUTATION_LOCK.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
hooks.wait_for_callback_delivery();
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||
CONFIG_SERVER_CLIENT_STOPPING.store(false, Ordering::Release);
|
||||
set_error_msg(&format!("failed to lock instance mutation: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let delete_result = INSTANCE_MANAGER.delete_network_instance(tracked_ids.clone());
|
||||
if delete_result.is_ok() {
|
||||
remove_instance_name_ids(&tracked_ids);
|
||||
remove_data_plane_handles_by_instance_ids(&tracked_ids);
|
||||
}
|
||||
drop(_mutation_guard);
|
||||
hooks.wait_for_callback_delivery();
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||
CONFIG_SERVER_CLIENT_STOPPING.store(false, Ordering::Release);
|
||||
|
||||
if let Err(err) = delete_result {
|
||||
set_error_msg(&format!(
|
||||
"failed to delete config server instances: {}",
|
||||
err
|
||||
));
|
||||
return -1;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
pub(crate) fn is_config_server_client_connected() -> c_int {
|
||||
CONFIG_SERVER_CLIENT
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| guard.as_ref().map(|managed| managed.client.is_connected()))
|
||||
.map(i32::from)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
@@ -0,0 +1,854 @@
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use std::{
|
||||
future::Future,
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::{
|
||||
Arc, RwLock,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use dashmap::DashMap;
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use easytier::launcher::{DataPlaneTcpListener, DataPlaneTcpStream, DataPlaneUdpSocket};
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf};
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use tokio_util::sync::CancellationToken;
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use crate::{
|
||||
config_server::{in_config_server_callback, is_config_server_active_or_stopping},
|
||||
error::{free_string, set_error_msg},
|
||||
state::{INSTANCE_MANAGER, INSTANCE_NAME_ID_MAP},
|
||||
};
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
static NEXT_DATA_PLANE_HANDLE: AtomicU64 = AtomicU64::new(1);
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
static DATA_PLANE_HANDLES: once_cell::sync::Lazy<DashMap<u64, DataPlaneHandle>> =
|
||||
once_cell::sync::Lazy::new(DashMap::new);
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
static DATA_PLANE_USAGE_LOCK: once_cell::sync::Lazy<RwLock<()>> =
|
||||
once_cell::sync::Lazy::new(|| RwLock::new(()));
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
struct DataPlaneHandle {
|
||||
instance_id: uuid::Uuid,
|
||||
runtime: tokio::runtime::Handle,
|
||||
// Cancelled by close() to wake any in-flight op on this handle.
|
||||
close_token: CancellationToken,
|
||||
resource: DataPlaneResource,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
struct TcpHalves {
|
||||
read: tokio::sync::Mutex<ReadHalf<DataPlaneTcpStream>>,
|
||||
write: tokio::sync::Mutex<WriteHalf<DataPlaneTcpStream>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
enum DataPlaneResource {
|
||||
Tcp(Arc<TcpHalves>),
|
||||
TcpListener(Arc<tokio::sync::Mutex<DataPlaneTcpListener>>),
|
||||
Udp(Arc<DataPlaneUdpSocket>),
|
||||
}
|
||||
|
||||
// Several helper functions for FFI data plane operations to facilitate logic reuse.
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
fn next_handle() -> u64 {
|
||||
NEXT_DATA_PLANE_HANDLE.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
fn timeout_duration(timeout_ms: u64) -> Duration {
|
||||
Duration::from_millis(timeout_ms)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
unsafe fn cstr_to_string(ptr: *const std::ffi::c_char, name: &str) -> Option<String> {
|
||||
if ptr.is_null() {
|
||||
set_error_msg(&format!("{} is null", name));
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
unsafe { std::ffi::CStr::from_ptr(ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
fn get_instance_id(inst_name: &str) -> Option<uuid::Uuid> {
|
||||
INSTANCE_NAME_ID_MAP.get(inst_name).map(|id| *id.value())
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
fn parse_socket_addr(host: &str, port: u16) -> Option<SocketAddr> {
|
||||
let ip = match host.parse::<IpAddr>() {
|
||||
Ok(ip) => ip,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to parse ip address: {}", e));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(SocketAddr::new(ip, port))
|
||||
}
|
||||
|
||||
/// Encode an IP address for FFI return. Returns `*mut c_char` to match
|
||||
/// `CString::into_raw`; caller releases it via `free_string`.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
fn into_ffi_ip_cstring(ip: IpAddr) -> Option<*mut std::ffi::c_char> {
|
||||
match std::ffi::CString::new(ip.to_string()) {
|
||||
Ok(s) => Some(s.into_raw()),
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to encode ip: {}", e));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
fn get_runtime_handle(
|
||||
inst_id: &uuid::Uuid,
|
||||
deadline: std::time::Instant,
|
||||
) -> Option<tokio::runtime::Handle> {
|
||||
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
|
||||
let Some(rt) = INSTANCE_MANAGER.data_plane_wait_runtime_handle(inst_id, remaining) else {
|
||||
set_error_msg("instance runtime is not ready");
|
||||
return None;
|
||||
};
|
||||
Some(rt)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
fn insert_tcp_stream_handle(
|
||||
instance_id: uuid::Uuid,
|
||||
runtime: tokio::runtime::Handle,
|
||||
stream: DataPlaneTcpStream,
|
||||
) -> u64 {
|
||||
let (rd, wr) = tokio::io::split(stream);
|
||||
let handle = next_handle();
|
||||
DATA_PLANE_HANDLES.insert(
|
||||
handle,
|
||||
DataPlaneHandle {
|
||||
instance_id,
|
||||
runtime,
|
||||
close_token: CancellationToken::new(),
|
||||
resource: DataPlaneResource::Tcp(Arc::new(TcpHalves {
|
||||
read: tokio::sync::Mutex::new(rd),
|
||||
write: tokio::sync::Mutex::new(wr),
|
||||
})),
|
||||
},
|
||||
);
|
||||
handle
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
fn get_tcp_stream(
|
||||
handle: u64,
|
||||
) -> Option<(Arc<TcpHalves>, tokio::runtime::Handle, CancellationToken)> {
|
||||
let Some(h) = DATA_PLANE_HANDLES.get(&handle) else {
|
||||
set_error_msg("tcp stream handle not found");
|
||||
return None;
|
||||
};
|
||||
match &h.resource {
|
||||
DataPlaneResource::Tcp(halves) => {
|
||||
Some((halves.clone(), h.runtime.clone(), h.close_token.clone()))
|
||||
}
|
||||
DataPlaneResource::TcpListener(_) | DataPlaneResource::Udp(_) => {
|
||||
set_error_msg("handle is not a tcp stream");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
fn get_tcp_listener(
|
||||
handle: u64,
|
||||
) -> Option<(
|
||||
Arc<tokio::sync::Mutex<DataPlaneTcpListener>>,
|
||||
tokio::runtime::Handle,
|
||||
CancellationToken,
|
||||
uuid::Uuid,
|
||||
)> {
|
||||
let Some(h) = DATA_PLANE_HANDLES.get(&handle) else {
|
||||
set_error_msg("tcp listener handle not found");
|
||||
return None;
|
||||
};
|
||||
match &h.resource {
|
||||
DataPlaneResource::TcpListener(listener) => Some((
|
||||
listener.clone(),
|
||||
h.runtime.clone(),
|
||||
h.close_token.clone(),
|
||||
h.instance_id,
|
||||
)),
|
||||
DataPlaneResource::Tcp(_) | DataPlaneResource::Udp(_) => {
|
||||
set_error_msg("handle is not a tcp listener");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
fn get_udp_socket(
|
||||
handle: u64,
|
||||
) -> Option<(
|
||||
Arc<DataPlaneUdpSocket>,
|
||||
tokio::runtime::Handle,
|
||||
CancellationToken,
|
||||
)> {
|
||||
let Some(h) = DATA_PLANE_HANDLES.get(&handle) else {
|
||||
set_error_msg("udp socket handle not found");
|
||||
return None;
|
||||
};
|
||||
match &h.resource {
|
||||
DataPlaneResource::Udp(socket) => {
|
||||
Some((socket.clone(), h.runtime.clone(), h.close_token.clone()))
|
||||
}
|
||||
DataPlaneResource::Tcp(_) | DataPlaneResource::TcpListener(_) => {
|
||||
set_error_msg("handle is not a udp socket");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn remove_data_plane_handles_by_instance_ids(ids: &[Uuid]) {
|
||||
if ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
DATA_PLANE_HANDLES.retain(|_, handle| {
|
||||
if ids.contains(&handle.instance_id) {
|
||||
handle.close_token.cancel();
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ffi-dataplane"))]
|
||||
pub(crate) fn remove_data_plane_handles_by_instance_ids(_ids: &[uuid::Uuid]) {}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
fn data_plane_rejected() -> bool {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot use data plane from config server callback");
|
||||
true
|
||||
} else if is_config_server_active_or_stopping() {
|
||||
set_error_msg("cannot use data plane while config server client is active");
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
fn enter_data_plane_operation() -> Option<std::sync::RwLockReadGuard<'static, ()>> {
|
||||
if data_plane_rejected() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let guard = match DATA_PLANE_USAGE_LOCK.read() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to lock data plane usage: {}", err));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if data_plane_rejected() {
|
||||
return None;
|
||||
}
|
||||
Some(guard)
|
||||
}
|
||||
|
||||
/// Run an IO op on the resource's owning runtime, supporting
|
||||
/// timeout and cancellation.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
async fn run_with_cancel<T, F>(
|
||||
close_token: &CancellationToken,
|
||||
timeout_ms: u64,
|
||||
error_prefix: &str,
|
||||
op: F,
|
||||
) -> Option<Result<T, std::io::Error>>
|
||||
where
|
||||
F: Future<Output = Result<T, std::io::Error>>,
|
||||
{
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = close_token.cancelled() => {
|
||||
set_error_msg(&format!("{}: handle closed", error_prefix));
|
||||
None
|
||||
}
|
||||
res = tokio::time::timeout(timeout_duration(timeout_ms), op) => match res {
|
||||
Ok(r) => Some(r),
|
||||
Err(_) => {
|
||||
set_error_msg(&format!("{} timed out", error_prefix));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn lock_for_config_server_start()
|
||||
-> Result<std::sync::RwLockWriteGuard<'static, ()>, String> {
|
||||
let guard = DATA_PLANE_USAGE_LOCK
|
||||
.write()
|
||||
.map_err(|err| format!("failed to lock data plane usage: {}", err))?;
|
||||
if !DATA_PLANE_HANDLES.is_empty() {
|
||||
return Err("cannot start config server client while data plane is in use".to_string());
|
||||
}
|
||||
Ok(guard)
|
||||
}
|
||||
/// # Safety
|
||||
/// Open a TCP stream through an EasyTier instance data plane. Returns 0 on
|
||||
/// failure. On success, writes the local socket address chosen for this
|
||||
/// connection into `out_local_ip` (a heap-allocated C string the caller must
|
||||
/// release via `free_string`) and `out_local_port`. Both out pointers must be
|
||||
/// non-null.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_tcp_connect(
|
||||
inst_name: *const std::ffi::c_char,
|
||||
dst_ip: *const std::ffi::c_char,
|
||||
dst_port: std::ffi::c_ushort,
|
||||
timeout_ms: u64,
|
||||
out_local_ip: *mut *const std::ffi::c_char,
|
||||
out_local_port: *mut std::ffi::c_ushort,
|
||||
) -> u64 {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return 0,
|
||||
};
|
||||
if out_local_ip.is_null() || out_local_port.is_null() {
|
||||
set_error_msg("output pointer is null");
|
||||
return 0;
|
||||
}
|
||||
let Some(inst_name) = (unsafe { cstr_to_string(inst_name, "inst_name") }) else {
|
||||
return 0;
|
||||
};
|
||||
let Some(dst_ip) = (unsafe { cstr_to_string(dst_ip, "dst_ip") }) else {
|
||||
return 0;
|
||||
};
|
||||
let Some(inst_id) = get_instance_id(&inst_name) else {
|
||||
set_error_msg("instance not found");
|
||||
return 0;
|
||||
};
|
||||
let Some(dst_addr) = parse_socket_addr(&dst_ip, dst_port) else {
|
||||
return 0;
|
||||
};
|
||||
let deadline = std::time::Instant::now() + timeout_duration(timeout_ms);
|
||||
let Some(runtime) = get_runtime_handle(&inst_id, deadline) else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
|
||||
let result =
|
||||
runtime.block_on(INSTANCE_MANAGER.data_plane_tcp_connect(&inst_id, dst_addr, remaining));
|
||||
match result {
|
||||
Ok(stream) => {
|
||||
let local_addr = stream.local_addr();
|
||||
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
|
||||
return 0;
|
||||
};
|
||||
let handle = insert_tcp_stream_handle(inst_id, runtime, stream);
|
||||
unsafe {
|
||||
*out_local_ip = local_ip as *const std::ffi::c_char;
|
||||
*out_local_port = local_addr.port();
|
||||
}
|
||||
handle
|
||||
}
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to connect tcp data plane: {}", e));
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Bind a TCP listener through an EasyTier instance data plane. Returns 0 on
|
||||
/// failure. The local address actually bound is written into `out_local_ip` /
|
||||
/// `out_local_port`; the caller must release `*out_local_ip` via `free_string`.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_tcp_bind(
|
||||
inst_name: *const std::ffi::c_char,
|
||||
local_port: std::ffi::c_ushort,
|
||||
timeout_ms: u64,
|
||||
out_local_ip: *mut *const std::ffi::c_char,
|
||||
out_local_port: *mut std::ffi::c_ushort,
|
||||
) -> u64 {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return 0,
|
||||
};
|
||||
if out_local_ip.is_null() || out_local_port.is_null() {
|
||||
set_error_msg("output pointer is null");
|
||||
return 0;
|
||||
}
|
||||
let Some(inst_name) = (unsafe { cstr_to_string(inst_name, "inst_name") }) else {
|
||||
return 0;
|
||||
};
|
||||
let Some(inst_id) = get_instance_id(&inst_name) else {
|
||||
set_error_msg("instance not found");
|
||||
return 0;
|
||||
};
|
||||
let deadline = std::time::Instant::now() + timeout_duration(timeout_ms);
|
||||
let Some(runtime) = get_runtime_handle(&inst_id, deadline) else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
|
||||
let result =
|
||||
runtime.block_on(INSTANCE_MANAGER.data_plane_tcp_bind(&inst_id, local_port, remaining));
|
||||
match result {
|
||||
Ok(listener) => {
|
||||
let local_addr = listener.local_addr();
|
||||
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
|
||||
return 0;
|
||||
};
|
||||
let handle = next_handle();
|
||||
DATA_PLANE_HANDLES.insert(
|
||||
handle,
|
||||
DataPlaneHandle {
|
||||
instance_id: inst_id,
|
||||
runtime,
|
||||
close_token: CancellationToken::new(),
|
||||
resource: DataPlaneResource::TcpListener(Arc::new(tokio::sync::Mutex::new(
|
||||
listener,
|
||||
))),
|
||||
},
|
||||
);
|
||||
unsafe {
|
||||
*out_local_ip = local_ip as *const std::ffi::c_char;
|
||||
*out_local_port = local_addr.port();
|
||||
}
|
||||
handle
|
||||
}
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to bind tcp data plane: {}", e));
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Accept one connection from a TCP data-plane listener. Returns a TCP stream
|
||||
/// handle, or 0 on failure. Local and peer addresses are written into out
|
||||
/// parameters; returned IP strings must be released via `free_string`.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_tcp_accept(
|
||||
handle: u64,
|
||||
timeout_ms: u64,
|
||||
out_local_ip: *mut *const std::ffi::c_char,
|
||||
out_local_port: *mut std::ffi::c_ushort,
|
||||
out_peer_ip: *mut *const std::ffi::c_char,
|
||||
out_peer_port: *mut std::ffi::c_ushort,
|
||||
) -> u64 {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return 0,
|
||||
};
|
||||
if out_local_ip.is_null()
|
||||
|| out_local_port.is_null()
|
||||
|| out_peer_ip.is_null()
|
||||
|| out_peer_port.is_null()
|
||||
{
|
||||
set_error_msg("output pointer is null");
|
||||
return 0;
|
||||
}
|
||||
let Some((listener, runtime, close_token, instance_id)) = get_tcp_listener(handle) else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let ret = runtime.block_on(async move {
|
||||
let mut listener = listener.lock().await;
|
||||
run_with_cancel(
|
||||
&close_token,
|
||||
timeout_ms,
|
||||
"tcp data plane accept",
|
||||
listener.accept(),
|
||||
)
|
||||
.await
|
||||
});
|
||||
|
||||
match ret {
|
||||
Some(Ok((stream, peer_addr))) => {
|
||||
let local_addr = stream.local_addr();
|
||||
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
|
||||
return 0;
|
||||
};
|
||||
let Some(peer_ip) = into_ffi_ip_cstring(peer_addr.ip()) else {
|
||||
free_string(local_ip);
|
||||
return 0;
|
||||
};
|
||||
let stream_handle = insert_tcp_stream_handle(instance_id, runtime, stream);
|
||||
unsafe {
|
||||
*out_local_ip = local_ip as *const std::ffi::c_char;
|
||||
*out_local_port = local_addr.port();
|
||||
*out_peer_ip = peer_ip as *const std::ffi::c_char;
|
||||
*out_peer_port = peer_addr.port();
|
||||
}
|
||||
stream_handle
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
set_error_msg(&format!("failed to accept tcp data plane: {}", e));
|
||||
0
|
||||
}
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Read from a TCP data-plane stream.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_tcp_read(
|
||||
handle: u64,
|
||||
buf: *mut std::ffi::c_uchar,
|
||||
len: u32,
|
||||
timeout_ms: u64,
|
||||
) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
if buf.is_null() {
|
||||
set_error_msg("buf is null");
|
||||
return -1;
|
||||
}
|
||||
let Some((halves, runtime, close_token)) = get_tcp_stream(handle) else {
|
||||
return -1;
|
||||
};
|
||||
// Safety: caller-owned buffer outlives this blocking call.
|
||||
let buf = unsafe { std::slice::from_raw_parts_mut(buf, len as usize) };
|
||||
runtime.block_on(async move {
|
||||
let mut rd = halves.read.lock().await;
|
||||
match run_with_cancel(
|
||||
&close_token,
|
||||
timeout_ms,
|
||||
"failed to read tcp data plane",
|
||||
rd.read(buf),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Some(Ok(n)) => n as std::ffi::c_int,
|
||||
Some(Err(e)) => {
|
||||
set_error_msg(&format!("failed to read tcp data plane: {}", e));
|
||||
-1
|
||||
}
|
||||
None => -1,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Write to a TCP data-plane stream.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_tcp_write(
|
||||
handle: u64,
|
||||
buf: *const std::ffi::c_uchar,
|
||||
len: u32,
|
||||
timeout_ms: u64,
|
||||
) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
if buf.is_null() {
|
||||
set_error_msg("buf is null");
|
||||
return -1;
|
||||
}
|
||||
let Some((halves, runtime, close_token)) = get_tcp_stream(handle) else {
|
||||
return -1;
|
||||
};
|
||||
let total = len as usize;
|
||||
// Safety: caller-owned buffer outlives this blocking call.
|
||||
let buf = unsafe { std::slice::from_raw_parts(buf, total) };
|
||||
runtime.block_on(async move {
|
||||
let mut wr = halves.write.lock().await;
|
||||
// Use `write_all` to honor `net.Conn::Write` semantics on the Go side
|
||||
// (must write everything or return an error); single `write()` can
|
||||
// silently short-write and corrupt streams that the caller assumes are
|
||||
// fully written.
|
||||
match run_with_cancel(
|
||||
&close_token,
|
||||
timeout_ms,
|
||||
"failed to write tcp data plane",
|
||||
wr.write_all(buf),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Some(Ok(())) => total as std::ffi::c_int,
|
||||
Some(Err(e)) => {
|
||||
set_error_msg(&format!("failed to write tcp data plane: {}", e));
|
||||
-1
|
||||
}
|
||||
None => -1,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn data_plane_tcp_close(handle: u64) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
let Some((_, h)) = DATA_PLANE_HANDLES.remove_if(&handle, |_, e| {
|
||||
matches!(e.resource, DataPlaneResource::Tcp(_))
|
||||
}) else {
|
||||
set_error_msg(if DATA_PLANE_HANDLES.contains_key(&handle) {
|
||||
"handle is not a tcp stream"
|
||||
} else {
|
||||
"tcp stream handle not found"
|
||||
});
|
||||
return -1;
|
||||
};
|
||||
h.close_token.cancel();
|
||||
if let DataPlaneResource::Tcp(halves) = h.resource {
|
||||
// Best-effort half-close; if write half is in use, the in-flight call
|
||||
// observes the cancel token and releases the lock shortly after.
|
||||
h.runtime.spawn(async move {
|
||||
if let Ok(mut wr) = halves.write.try_lock() {
|
||||
let _ = wr.shutdown().await;
|
||||
}
|
||||
});
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn data_plane_tcp_listener_close(handle: u64) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
let Some((_, h)) = DATA_PLANE_HANDLES.remove_if(&handle, |_, e| {
|
||||
matches!(e.resource, DataPlaneResource::TcpListener(_))
|
||||
}) else {
|
||||
set_error_msg(if DATA_PLANE_HANDLES.contains_key(&handle) {
|
||||
"handle is not a tcp listener"
|
||||
} else {
|
||||
"tcp listener handle not found"
|
||||
});
|
||||
return -1;
|
||||
};
|
||||
h.close_token.cancel();
|
||||
0
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Bind a UDP socket through an EasyTier instance data plane. Returns 0 on
|
||||
/// failure. The local address actually bound (which may differ from the
|
||||
/// requested port when `local_port == 0`) is written into `out_local_ip` /
|
||||
/// `out_local_port`; the caller must release `*out_local_ip` via `free_string`.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_udp_bind(
|
||||
inst_name: *const std::ffi::c_char,
|
||||
local_port: std::ffi::c_ushort,
|
||||
timeout_ms: u64,
|
||||
out_local_ip: *mut *const std::ffi::c_char,
|
||||
out_local_port: *mut std::ffi::c_ushort,
|
||||
) -> u64 {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return 0,
|
||||
};
|
||||
if out_local_ip.is_null() || out_local_port.is_null() {
|
||||
set_error_msg("output pointer is null");
|
||||
return 0;
|
||||
}
|
||||
let Some(inst_name) = (unsafe { cstr_to_string(inst_name, "inst_name") }) else {
|
||||
return 0;
|
||||
};
|
||||
let Some(inst_id) = get_instance_id(&inst_name) else {
|
||||
set_error_msg("instance not found");
|
||||
return 0;
|
||||
};
|
||||
let deadline = std::time::Instant::now() + timeout_duration(timeout_ms);
|
||||
let Some(runtime) = get_runtime_handle(&inst_id, deadline) else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
|
||||
let result =
|
||||
runtime.block_on(INSTANCE_MANAGER.data_plane_udp_bind(&inst_id, local_port, remaining));
|
||||
match result {
|
||||
Ok(socket) => {
|
||||
let local_addr = socket.local_addr();
|
||||
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
|
||||
return 0;
|
||||
};
|
||||
let handle = next_handle();
|
||||
DATA_PLANE_HANDLES.insert(
|
||||
handle,
|
||||
DataPlaneHandle {
|
||||
instance_id: inst_id,
|
||||
runtime,
|
||||
close_token: CancellationToken::new(),
|
||||
resource: DataPlaneResource::Udp(Arc::new(socket)),
|
||||
},
|
||||
);
|
||||
unsafe {
|
||||
*out_local_ip = local_ip as *const std::ffi::c_char;
|
||||
*out_local_port = local_addr.port();
|
||||
}
|
||||
handle
|
||||
}
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to bind udp data plane: {}", e));
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Send a datagram through a UDP data-plane socket.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_udp_send_to(
|
||||
handle: u64,
|
||||
dst_ip: *const std::ffi::c_char,
|
||||
dst_port: std::ffi::c_ushort,
|
||||
buf: *const std::ffi::c_uchar,
|
||||
len: u32,
|
||||
timeout_ms: u64,
|
||||
) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
if buf.is_null() {
|
||||
set_error_msg("buf is null");
|
||||
return -1;
|
||||
}
|
||||
let Some(dst_ip) = (unsafe { cstr_to_string(dst_ip, "dst_ip") }) else {
|
||||
return -1;
|
||||
};
|
||||
let Some(dst_addr) = parse_socket_addr(&dst_ip, dst_port) else {
|
||||
return -1;
|
||||
};
|
||||
let Some((socket, runtime, close_token)) = get_udp_socket(handle) else {
|
||||
return -1;
|
||||
};
|
||||
let total = len as usize;
|
||||
// Safety: caller-owned buffer outlives this blocking call.
|
||||
let buf = unsafe { std::slice::from_raw_parts(buf, total) };
|
||||
runtime.block_on(async move {
|
||||
match run_with_cancel(
|
||||
&close_token,
|
||||
timeout_ms,
|
||||
"failed to send udp data plane",
|
||||
socket.send_to(buf, dst_addr),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Some(Ok(n)) => n as std::ffi::c_int,
|
||||
Some(Err(e)) => {
|
||||
set_error_msg(&format!("failed to send udp data plane: {}", e));
|
||||
-1
|
||||
}
|
||||
None => -1,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Receive a datagram from a UDP data-plane socket.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_udp_recv_from(
|
||||
handle: u64,
|
||||
buf: *mut std::ffi::c_uchar,
|
||||
len: u32,
|
||||
out_ip: *mut *const std::ffi::c_char,
|
||||
out_port: *mut std::ffi::c_ushort,
|
||||
timeout_ms: u64,
|
||||
) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
if buf.is_null() || out_ip.is_null() || out_port.is_null() {
|
||||
set_error_msg("output pointer is null");
|
||||
return -1;
|
||||
}
|
||||
let Some((socket, runtime, close_token)) = get_udp_socket(handle) else {
|
||||
return -1;
|
||||
};
|
||||
let total = len as usize;
|
||||
// Safety: caller-owned buffer outlives this blocking call.
|
||||
let buf = unsafe { std::slice::from_raw_parts_mut(buf, total) };
|
||||
let ret = runtime.block_on(run_with_cancel(
|
||||
&close_token,
|
||||
timeout_ms,
|
||||
"udp data plane receive",
|
||||
socket.recv_from(buf),
|
||||
));
|
||||
|
||||
match ret {
|
||||
Some(Ok((n, addr))) => {
|
||||
// The returned ip pointer must be released by the caller via
|
||||
// `free_string` (which calls `CString::from_raw`, matching
|
||||
// `CString::into_raw` here).
|
||||
let Some(ip_cstr) = into_ffi_ip_cstring(addr.ip()) else {
|
||||
return -1;
|
||||
};
|
||||
unsafe {
|
||||
*out_ip = ip_cstr as *const std::ffi::c_char;
|
||||
*out_port = addr.port() as std::ffi::c_ushort;
|
||||
}
|
||||
n as std::ffi::c_int
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
set_error_msg(&format!("failed to receive udp data plane: {}", e));
|
||||
-1
|
||||
}
|
||||
None => -1,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn data_plane_udp_close(handle: u64) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
let Some((_, h)) = DATA_PLANE_HANDLES.remove_if(&handle, |_, e| {
|
||||
matches!(e.resource, DataPlaneResource::Udp(_))
|
||||
}) else {
|
||||
set_error_msg(if DATA_PLANE_HANDLES.contains_key(&handle) {
|
||||
"handle is not a udp socket"
|
||||
} else {
|
||||
"udp socket handle not found"
|
||||
});
|
||||
return -1;
|
||||
};
|
||||
h.close_token.cancel();
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "ffi-dataplane"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::{sync::mpsc, time::Duration};
|
||||
|
||||
#[test]
|
||||
fn config_server_start_waits_for_data_plane_operation() {
|
||||
let read_guard = DATA_PLANE_USAGE_LOCK.read().unwrap();
|
||||
let (done_tx, done_rx) = mpsc::channel();
|
||||
let waiter = std::thread::spawn(move || {
|
||||
let _write_guard = lock_for_config_server_start().unwrap();
|
||||
done_tx.send(()).unwrap();
|
||||
});
|
||||
|
||||
assert!(done_rx.recv_timeout(Duration::from_millis(100)).is_err());
|
||||
drop(read_guard);
|
||||
done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
|
||||
waiter.join().unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
ffi::{CString, c_char},
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
// # Thread Safety
|
||||
// set_error_msg and get_error_msg must be called on the same thread to
|
||||
// get correct error. And since `Handle::block_on` polls the top-level
|
||||
// future on the calling thread, set_error_msg always runs on the same
|
||||
// thread as the corresponding get_error_msg.
|
||||
static ERROR_MSG: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
pub(crate) fn set_error_msg(msg: &str) {
|
||||
ERROR_MSG.with(|cell| {
|
||||
let mut buf = cell.borrow_mut();
|
||||
buf.clear();
|
||||
buf.extend_from_slice(msg.as_bytes());
|
||||
});
|
||||
}
|
||||
|
||||
fn thread_local_error_msg() -> Option<String> {
|
||||
ERROR_MSG.with(|cell| {
|
||||
let buf = cell.borrow();
|
||||
if buf.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(String::from_utf8_lossy(&buf).into_owned())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn get_error_msg(out: *mut *const c_char) {
|
||||
let msg = match (
|
||||
thread_local_error_msg(),
|
||||
crate::config_server::last_callback_error(),
|
||||
) {
|
||||
(Some(error), Some(callback_error)) => Some(format!(
|
||||
"{}; config server callback error: {}",
|
||||
error, callback_error
|
||||
)),
|
||||
(Some(error), None) => Some(error),
|
||||
(None, Some(callback_error)) => {
|
||||
Some(format!("config server callback error: {}", callback_error))
|
||||
}
|
||||
(None, None) => None,
|
||||
};
|
||||
let cstr = msg.and_then(|msg| CString::new(msg).ok());
|
||||
unsafe {
|
||||
*out = match cstr {
|
||||
Some(s) => s.into_raw() as *const c_char,
|
||||
None => std::ptr::null(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn free_string(s: *const c_char) {
|
||||
if s.is_null() {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
let _ = CString::from_raw(s as *mut c_char);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use easytier::common::config::{ConfigFileControl, ConfigLoader as _, TomlConfigLoader};
|
||||
|
||||
use crate::{
|
||||
config_server::{
|
||||
in_config_server_callback, remove_config_server_tracked_instance_ids,
|
||||
wait_for_config_server_delivery,
|
||||
},
|
||||
data_plane::remove_data_plane_handles_by_instance_ids,
|
||||
error::set_error_msg,
|
||||
state::{
|
||||
INSTANCE_MANAGER, INSTANCE_MUTATION_LOCK, INSTANCE_NAME_ID_MAP, instance_name_exists,
|
||||
lock_remote_instance_mutation,
|
||||
},
|
||||
types::KeyValuePair,
|
||||
};
|
||||
|
||||
/// # Safety
|
||||
/// Set the tun fd
|
||||
pub(crate) unsafe fn set_tun_fd(inst_name: *const c_char, fd: c_int) -> c_int {
|
||||
let inst_name = unsafe {
|
||||
assert!(!inst_name.is_null());
|
||||
std::ffi::CStr::from_ptr(inst_name)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
};
|
||||
if !INSTANCE_NAME_ID_MAP.contains_key(&inst_name) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let inst_id = *INSTANCE_NAME_ID_MAP
|
||||
.get(&inst_name)
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.value();
|
||||
|
||||
match INSTANCE_MANAGER.set_tun_fd(&inst_id, fd) {
|
||||
Ok(_) => 0,
|
||||
Err(_) => -1,
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Parse the config
|
||||
pub(crate) unsafe fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
|
||||
let cfg_str = unsafe {
|
||||
assert!(!cfg_str.is_null());
|
||||
std::ffi::CStr::from_ptr(cfg_str)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
};
|
||||
|
||||
if let Err(e) = TomlConfigLoader::new_from_str(&cfg_str) {
|
||||
set_error_msg(&format!("failed to parse config: {:?}", e));
|
||||
return -1;
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Run the network instance
|
||||
pub(crate) unsafe fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot run network instance from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let cfg_str = unsafe {
|
||||
assert!(!cfg_str.is_null());
|
||||
std::ffi::CStr::from_ptr(cfg_str)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
};
|
||||
let cfg = match TomlConfigLoader::new_from_str(&cfg_str) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to parse config: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let inst_name = cfg.get_inst_name();
|
||||
|
||||
wait_for_config_server_delivery();
|
||||
let _remote_mutation_guard = lock_remote_instance_mutation();
|
||||
let _mutation_guard = match INSTANCE_MUTATION_LOCK.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to lock instance mutation: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
if instance_name_exists(&inst_name) {
|
||||
set_error_msg("instance already exists");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let instance_id =
|
||||
match INSTANCE_MANAGER.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG) {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to start instance: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
INSTANCE_NAME_ID_MAP.insert(inst_name, instance_id);
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Retain the network instance
|
||||
pub(crate) unsafe fn retain_network_instance(
|
||||
inst_names: *const *const std::ffi::c_char,
|
||||
length: usize,
|
||||
) -> std::ffi::c_int {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot retain network instances from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
wait_for_config_server_delivery();
|
||||
let _remote_mutation_guard = lock_remote_instance_mutation();
|
||||
let _mutation_guard = match INSTANCE_MUTATION_LOCK.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to lock instance mutation: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
if length == 0 {
|
||||
let removed_ids = INSTANCE_MANAGER.list_network_instance_ids();
|
||||
if let Err(e) = INSTANCE_MANAGER.delete_network_instance(removed_ids.clone()) {
|
||||
set_error_msg(&format!("failed to delete instances: {}", e));
|
||||
return -1;
|
||||
}
|
||||
remove_config_server_tracked_instance_ids(&removed_ids);
|
||||
remove_data_plane_handles_by_instance_ids(&removed_ids);
|
||||
INSTANCE_NAME_ID_MAP.clear();
|
||||
return 0;
|
||||
}
|
||||
|
||||
let inst_names = unsafe {
|
||||
assert!(!inst_names.is_null());
|
||||
std::slice::from_raw_parts(inst_names, length)
|
||||
.iter()
|
||||
.map(|&name| {
|
||||
assert!(!name.is_null());
|
||||
std::ffi::CStr::from_ptr(name)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let removed_ids = INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.into_iter()
|
||||
.filter(|id| {
|
||||
INSTANCE_MANAGER
|
||||
.get_instance_name(id)
|
||||
.is_none_or(|name| !inst_names.contains(&name))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Err(e) = INSTANCE_MANAGER.delete_network_instance(removed_ids.clone()) {
|
||||
set_error_msg(&format!("failed to delete instances: {}", e));
|
||||
return -1;
|
||||
}
|
||||
|
||||
remove_config_server_tracked_instance_ids(&removed_ids);
|
||||
remove_data_plane_handles_by_instance_ids(&removed_ids);
|
||||
INSTANCE_NAME_ID_MAP.retain(|k, _| inst_names.contains(k));
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Collect the network infos
|
||||
pub(crate) unsafe fn collect_network_infos(
|
||||
infos: *mut KeyValuePair,
|
||||
max_length: usize,
|
||||
) -> std::ffi::c_int {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot collect network infos from config server callback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if max_length == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let infos = unsafe {
|
||||
assert!(!infos.is_null());
|
||||
std::slice::from_raw_parts_mut(infos, max_length)
|
||||
};
|
||||
|
||||
let collected_infos = match INSTANCE_MANAGER.collect_network_infos_sync() {
|
||||
Ok(infos) => infos,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to collect network infos: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let mut index = 0;
|
||||
for (instance_id, value) in collected_infos.iter() {
|
||||
if index >= max_length {
|
||||
break;
|
||||
}
|
||||
let Some(key) = INSTANCE_MANAGER.get_instance_name(instance_id) else {
|
||||
continue;
|
||||
};
|
||||
// convert value to json string
|
||||
let value = match serde_json::to_string(&value) {
|
||||
Ok(value) => value,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to serialize instance info: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
infos[index] = KeyValuePair {
|
||||
key: std::ffi::CString::new(key).unwrap().into_raw(),
|
||||
value: std::ffi::CString::new(value).unwrap().into_raw(),
|
||||
};
|
||||
index += 1;
|
||||
}
|
||||
|
||||
index as std::ffi::c_int
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use easytier::instance_manager::NetworkInstanceManager;
|
||||
use tokio::runtime::{Builder, Runtime};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) static INSTANCE_NAME_ID_MAP: once_cell::sync::Lazy<DashMap<String, Uuid>> =
|
||||
once_cell::sync::Lazy::new(DashMap::new);
|
||||
pub(crate) static INSTANCE_MANAGER: once_cell::sync::Lazy<Arc<NetworkInstanceManager>> =
|
||||
once_cell::sync::Lazy::new(|| Arc::new(NetworkInstanceManager::new()));
|
||||
pub(crate) static ASYNC_RUNTIME: once_cell::sync::Lazy<Runtime> =
|
||||
once_cell::sync::Lazy::new(|| {
|
||||
Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("tokio runtime for easytier-ffi")
|
||||
});
|
||||
pub(crate) static INSTANCE_MUTATION_LOCK: once_cell::sync::Lazy<Mutex<()>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(()));
|
||||
|
||||
pub(crate) fn remove_instance_name_ids(ids: &[Uuid]) {
|
||||
if ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
INSTANCE_NAME_ID_MAP.retain(|_, instance_id| !ids.contains(instance_id));
|
||||
}
|
||||
|
||||
pub(crate) fn lock_remote_instance_mutation() -> tokio::sync::OwnedMutexGuard<()> {
|
||||
INSTANCE_MANAGER
|
||||
.remote_mutation_lock()
|
||||
.blocking_lock_owned()
|
||||
}
|
||||
|
||||
pub(crate) fn instance_name_exists(inst_name: &str) -> bool {
|
||||
find_instance_id_by_name(inst_name).is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn find_instance_id_by_name(inst_name: &str) -> Option<Uuid> {
|
||||
INSTANCE_NAME_ID_MAP
|
||||
.get(inst_name)
|
||||
.map(|id| *id)
|
||||
.or_else(|| {
|
||||
INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.into_iter()
|
||||
.find(|id| {
|
||||
INSTANCE_MANAGER
|
||||
.get_instance_name(id)
|
||||
.is_some_and(|name| name == inst_name)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
use std::ffi::{CStr, c_char};
|
||||
|
||||
pub(crate) unsafe fn c_str_to_string(ptr: *const c_char, name: &str) -> Result<String, String> {
|
||||
if ptr.is_null() {
|
||||
return Err(format!("{} is null", name));
|
||||
}
|
||||
|
||||
unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map(|value| value.to_string())
|
||||
.map_err(|err| format!("{} is not valid UTF-8: {}", name, err))
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn optional_c_str_to_string(
|
||||
ptr: *const c_char,
|
||||
name: &str,
|
||||
) -> Result<Option<String>, String> {
|
||||
if ptr.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
unsafe { c_str_to_string(ptr, name) }.map(Some)
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
use crate::{
|
||||
config_server::{
|
||||
ConfigServerCallbackScope, ManagedConfigServerClientHooks, set_active_for_test,
|
||||
},
|
||||
state::{
|
||||
INSTANCE_MANAGER, INSTANCE_NAME_ID_MAP, find_instance_id_by_name,
|
||||
lock_remote_instance_mutation, remove_instance_name_ids,
|
||||
},
|
||||
*,
|
||||
};
|
||||
use easytier::{
|
||||
common::config::{ConfigFileControl, ConfigLoader as _, TomlConfigLoader},
|
||||
web_client::WebClientHooks,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
ffi::{CStr, CString, c_char, c_void},
|
||||
sync::{Mutex, mpsc},
|
||||
time::Duration,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test]
|
||||
fn test_parse_config() {
|
||||
let cfg_str = r#"
|
||||
inst_name = "test"
|
||||
network = "test_network"
|
||||
"#;
|
||||
let cstr = std::ffi::CString::new(cfg_str).unwrap();
|
||||
unsafe {
|
||||
assert_eq!(parse_config(cstr.as_ptr()), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_network_instance() {
|
||||
let cfg_str = r#"
|
||||
inst_name = "test"
|
||||
network = "test_network"
|
||||
"#;
|
||||
let cstr = std::ffi::CString::new(cfg_str).unwrap();
|
||||
unsafe {
|
||||
assert_eq!(run_network_instance(cstr.as_ptr()), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_error_msg_returns_config_server_callback_error() {
|
||||
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
|
||||
let callback_error = format!("callback delivery failed {}", Uuid::new_v4());
|
||||
crate::config_server::clear_last_callback_error();
|
||||
hooks.note_callback_error(callback_error.clone());
|
||||
|
||||
unsafe {
|
||||
let mut error_ptr: *const c_char = std::ptr::null();
|
||||
get_error_msg(&mut error_ptr);
|
||||
assert!(!error_ptr.is_null());
|
||||
let error_msg = CStr::from_ptr(error_ptr).to_string_lossy().into_owned();
|
||||
free_string(error_ptr);
|
||||
assert!(error_msg.contains(&callback_error));
|
||||
}
|
||||
|
||||
crate::config_server::clear_last_callback_error();
|
||||
}
|
||||
|
||||
unsafe extern "C" fn record_config_server_event(event_json: *const c_char, user_data: *mut c_void) {
|
||||
let events = unsafe { &*(user_data as *const Mutex<Vec<String>>) };
|
||||
events.lock().unwrap().push(
|
||||
unsafe { CStr::from_ptr(event_json) }
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_emit_run_event() {
|
||||
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
let hooks = ManagedConfigServerClientHooks::new(
|
||||
Some(record_config_server_event),
|
||||
&events as *const _ as *mut c_void,
|
||||
);
|
||||
let instance_id = Uuid::new_v4();
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(instance_id);
|
||||
let inst_name = format!("test-{}", instance_id);
|
||||
cfg.set_inst_name(inst_name.clone());
|
||||
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
|
||||
hooks.post_run_network_instance(&instance_id).await.unwrap();
|
||||
|
||||
let duplicate_cfg = TomlConfigLoader::default();
|
||||
duplicate_cfg.set_inst_name(inst_name);
|
||||
duplicate_cfg.set_id(Uuid::new_v4());
|
||||
assert!(
|
||||
hooks
|
||||
.pre_run_network_instance(&duplicate_cfg)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
assert_eq!(hooks.tracked_instance_ids(), vec![instance_id]);
|
||||
let events = events.lock().unwrap();
|
||||
assert_eq!(events.len(), 1);
|
||||
let event: Value = serde_json::from_str(&events[0]).unwrap();
|
||||
assert_eq!(event["event"], "run_network_instance");
|
||||
assert_eq!(event["success"], true);
|
||||
assert_eq!(event["instance_id"], instance_id.to_string());
|
||||
assert!(event["error"].is_null());
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id])
|
||||
.unwrap();
|
||||
remove_instance_name_ids(&[instance_id]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_emit_delete_events_for_tracked_instances() {
|
||||
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
let hooks = ManagedConfigServerClientHooks::new(
|
||||
Some(record_config_server_event),
|
||||
&events as *const _ as *mut c_void,
|
||||
);
|
||||
let instance_id_1 = Uuid::new_v4();
|
||||
let instance_id_2 = Uuid::new_v4();
|
||||
let unknown_instance_id = Uuid::new_v4();
|
||||
for id in [instance_id_1, instance_id_2] {
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(id);
|
||||
cfg.set_inst_name(format!("test-{}", id));
|
||||
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
hooks
|
||||
.post_run_network_instance(&instance_id_1)
|
||||
.await
|
||||
.unwrap();
|
||||
hooks
|
||||
.post_run_network_instance(&instance_id_2)
|
||||
.await
|
||||
.unwrap();
|
||||
events.lock().unwrap().clear();
|
||||
|
||||
hooks
|
||||
.post_remove_network_instances(&[instance_id_1, unknown_instance_id, instance_id_2])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(hooks.tracked_instance_ids().is_empty());
|
||||
let events = events.lock().unwrap();
|
||||
assert_eq!(events.len(), 2);
|
||||
let event_ids = events
|
||||
.iter()
|
||||
.map(|event| {
|
||||
let event: Value = serde_json::from_str(event).unwrap();
|
||||
assert_eq!(event["event"], "delete_network_instance");
|
||||
assert_eq!(event["success"], true);
|
||||
assert!(event["error"].is_null());
|
||||
event["instance_id"].as_str().unwrap().to_string()
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
assert_eq!(
|
||||
event_ids,
|
||||
HashSet::from([instance_id_1.to_string(), instance_id_2.to_string()])
|
||||
);
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id_1, instance_id_2])
|
||||
.unwrap();
|
||||
remove_instance_name_ids(&[instance_id_1, instance_id_2]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_remove_untracked_name_mapping_without_event() {
|
||||
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
let hooks = ManagedConfigServerClientHooks::new(
|
||||
Some(record_config_server_event),
|
||||
&events as *const _ as *mut c_void,
|
||||
);
|
||||
let local_id = Uuid::new_v4();
|
||||
let inst_name = format!("local-{}", local_id);
|
||||
INSTANCE_NAME_ID_MAP.insert(inst_name.clone(), local_id);
|
||||
|
||||
hooks
|
||||
.post_remove_network_instances(&[local_id])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(INSTANCE_NAME_ID_MAP.get(&inst_name).is_none());
|
||||
assert!(events.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_reject_duplicate_instance_name() {
|
||||
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
|
||||
let inst_name = format!("test-{}", Uuid::new_v4());
|
||||
let existing_id = Uuid::new_v4();
|
||||
let new_id = Uuid::new_v4();
|
||||
INSTANCE_NAME_ID_MAP.insert(inst_name.clone(), existing_id);
|
||||
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_inst_name(inst_name.clone());
|
||||
cfg.set_id(new_id);
|
||||
|
||||
assert!(hooks.pre_run_network_instance(&cfg).await.is_err());
|
||||
assert_eq!(*INSTANCE_NAME_ID_MAP.get(&inst_name).unwrap(), existing_id);
|
||||
INSTANCE_NAME_ID_MAP.remove(&inst_name);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_remove_overwritten_id_before_duplicate_name_error() {
|
||||
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
let hooks = ManagedConfigServerClientHooks::new(
|
||||
Some(record_config_server_event),
|
||||
&events as *const _ as *mut c_void,
|
||||
);
|
||||
let old_name = format!("old-{}", Uuid::new_v4());
|
||||
let duplicate_name = format!("duplicate-{}", Uuid::new_v4());
|
||||
let overwritten_id = Uuid::new_v4();
|
||||
let duplicate_id = Uuid::new_v4();
|
||||
hooks.instance_ids.lock().unwrap().insert(overwritten_id);
|
||||
INSTANCE_NAME_ID_MAP.insert(old_name.clone(), overwritten_id);
|
||||
INSTANCE_NAME_ID_MAP.insert(duplicate_name.clone(), duplicate_id);
|
||||
|
||||
hooks
|
||||
.post_remove_network_instances(&[overwritten_id])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_inst_name(duplicate_name.clone());
|
||||
cfg.set_id(overwritten_id);
|
||||
|
||||
assert!(hooks.pre_run_network_instance(&cfg).await.is_err());
|
||||
assert!(hooks.tracked_instance_ids().is_empty());
|
||||
assert!(INSTANCE_NAME_ID_MAP.get(&old_name).is_none());
|
||||
assert_eq!(
|
||||
*INSTANCE_NAME_ID_MAP.get(&duplicate_name).unwrap(),
|
||||
duplicate_id
|
||||
);
|
||||
assert_eq!(events.lock().unwrap().len(), 1);
|
||||
INSTANCE_NAME_ID_MAP.remove(&duplicate_name);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_remove_tracked_state_before_overwrite_retry() {
|
||||
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
|
||||
let inst_name = format!("test-{}", Uuid::new_v4());
|
||||
let instance_id = Uuid::new_v4();
|
||||
hooks.instance_ids.lock().unwrap().insert(instance_id);
|
||||
INSTANCE_NAME_ID_MAP.insert(inst_name.clone(), instance_id);
|
||||
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_inst_name(inst_name.clone());
|
||||
cfg.set_id(instance_id);
|
||||
|
||||
hooks
|
||||
.post_remove_network_instances(&[instance_id])
|
||||
.await
|
||||
.unwrap();
|
||||
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||
|
||||
assert!(hooks.tracked_instance_ids().is_empty());
|
||||
assert!(INSTANCE_NAME_ID_MAP.get(&inst_name).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_reject_post_run_after_external_delete() {
|
||||
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
|
||||
let instance_id = Uuid::new_v4();
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(instance_id);
|
||||
cfg.set_inst_name(format!("test-{}", instance_id));
|
||||
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id])
|
||||
.unwrap();
|
||||
|
||||
assert!(hooks.post_run_network_instance(&instance_id).await.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_instance_id_by_name_resolves_uncommitted_manager_instance_name() {
|
||||
let instance_id = Uuid::new_v4();
|
||||
let inst_name = format!("test-{}", instance_id);
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(instance_id);
|
||||
cfg.set_inst_name(inst_name.clone());
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(find_instance_id_by_name(&inst_name), Some(instance_id));
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id])
|
||||
.unwrap();
|
||||
remove_instance_name_ids(&[instance_id]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffi_remote_mutation_lock_uses_manager_lock() {
|
||||
let manager_guard = INSTANCE_MANAGER
|
||||
.remote_mutation_lock()
|
||||
.blocking_lock_owned();
|
||||
let (done_tx, done_rx) = mpsc::channel();
|
||||
let waiter = std::thread::spawn(move || {
|
||||
let _ffi_guard = lock_remote_instance_mutation();
|
||||
done_tx.send(()).unwrap();
|
||||
});
|
||||
|
||||
assert!(done_rx.recv_timeout(Duration::from_millis(100)).is_err());
|
||||
drop(manager_guard);
|
||||
done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
|
||||
waiter.join().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_suppress_late_run_events_while_stopping() {
|
||||
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
let hooks = ManagedConfigServerClientHooks::new(
|
||||
Some(record_config_server_event),
|
||||
&events as *const _ as *mut c_void,
|
||||
);
|
||||
hooks.start_stopping();
|
||||
|
||||
hooks
|
||||
.post_run_network_instance(&Uuid::new_v4())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(hooks.tracked_instance_ids().is_empty());
|
||||
assert!(events.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_server_callback_context_rejects_nested_blocking_ffi_calls() {
|
||||
let _callback_scope = ConfigServerCallbackScope::enter();
|
||||
assert_eq!(is_config_server_client_connected(), 0);
|
||||
assert_eq!(
|
||||
unsafe { collect_network_infos(std::ptr::null_mut(), 0) },
|
||||
-1
|
||||
);
|
||||
let cfg = CString::new("inst_name = \"callback-test\"\nlisteners = []").unwrap();
|
||||
assert_eq!(unsafe { run_network_instance(cfg.as_ptr()) }, -1);
|
||||
assert_eq!(unsafe { retain_network_instance(std::ptr::null(), 0) }, -1);
|
||||
let url = CString::new("ring://test/token").unwrap();
|
||||
let machine_id = CString::new("test-machine").unwrap();
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
start_config_server_client(
|
||||
url.as_ptr(),
|
||||
std::ptr::null(),
|
||||
machine_id.as_ptr(),
|
||||
false,
|
||||
None,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
assert_eq!(stop_config_server_client(), -1);
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
{
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
data_plane_tcp_connect(
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
0,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
data_plane_tcp_bind(
|
||||
std::ptr::null(),
|
||||
0,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
data_plane_tcp_accept(
|
||||
0,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_tcp_read(0, std::ptr::null_mut(), 0, 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_tcp_write(0, std::ptr::null(), 0, 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(data_plane_tcp_close(0), -1);
|
||||
assert_eq!(data_plane_tcp_listener_close(0), -1);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
data_plane_udp_bind(
|
||||
std::ptr::null(),
|
||||
0,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_udp_send_to(0, std::ptr::null(), 0, std::ptr::null(), 0, 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
data_plane_udp_recv_from(
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
assert_eq!(data_plane_udp_close(0), -1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[test]
|
||||
fn active_config_server_rejects_data_plane() {
|
||||
set_active_for_test(true);
|
||||
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
data_plane_tcp_connect(
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
0,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_tcp_read(0, std::ptr::null_mut(), 0, 0) },
|
||||
-1
|
||||
);
|
||||
|
||||
set_active_for_test(false);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
use std::ffi::{c_char, c_void};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct KeyValuePair {
|
||||
pub key: *const c_char,
|
||||
pub value: *const c_char,
|
||||
}
|
||||
|
||||
pub type ConfigServerEventCallback = Option<unsafe extern "C" fn(*const c_char, *mut c_void)>;
|
||||
@@ -34,6 +34,7 @@ pub struct NetworkInstanceManager {
|
||||
instance_error_messages: Arc<DashMap<uuid::Uuid, String>>,
|
||||
config_dir: Option<PathBuf>,
|
||||
guard_counter: Arc<()>,
|
||||
remote_mutation_lock: Arc<tokio::sync::Mutex<()>>,
|
||||
}
|
||||
|
||||
impl Default for NetworkInstanceManager {
|
||||
@@ -51,6 +52,7 @@ impl NetworkInstanceManager {
|
||||
instance_error_messages: Arc::new(DashMap::new()),
|
||||
config_dir: None,
|
||||
guard_counter: Arc::new(()),
|
||||
remote_mutation_lock: Arc::new(tokio::sync::Mutex::new(())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +61,10 @@ impl NetworkInstanceManager {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn remote_mutation_lock(&self) -> Arc<tokio::sync::Mutex<()>> {
|
||||
self.remote_mutation_lock.clone()
|
||||
}
|
||||
|
||||
fn start_instance_task(&self, instance_id: uuid::Uuid) -> Result<(), anyhow::Error> {
|
||||
if tokio::runtime::Handle::try_current().is_err() {
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -272,6 +278,12 @@ impl NetworkInstanceManager {
|
||||
.map(|instance| instance.value().get_config_file_control().clone())
|
||||
}
|
||||
|
||||
pub fn get_instance_config(&self, instance_id: &uuid::Uuid) -> Option<TomlConfigLoader> {
|
||||
self.instance_map
|
||||
.get(instance_id)
|
||||
.map(|instance| instance.value().get_config())
|
||||
}
|
||||
|
||||
pub fn get_instance_network_config_source(
|
||||
&self,
|
||||
instance_id: &uuid::Uuid,
|
||||
|
||||
@@ -500,6 +500,10 @@ impl NetworkInstance {
|
||||
&self.config_file_control
|
||||
}
|
||||
|
||||
pub fn get_config(&self) -> TomlConfigLoader {
|
||||
self.config.clone()
|
||||
}
|
||||
|
||||
pub fn get_network_config_source(&self) -> ConfigSource {
|
||||
self.config.get_network_config_source()
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,10 @@ use uuid::Uuid;
|
||||
|
||||
#[async_trait]
|
||||
pub trait WebClientHooks: Send + Sync {
|
||||
fn manages_remote_config_instances(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn pre_run_network_instance(&self, _cfg: &TomlConfigLoader) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user