feat(ohos): 接入 Pro 运行时并自动化 HAR 交付 (#2462)

This commit is contained in:
韩嘉乐
2026-08-01 16:35:14 +08:00
committed by GitHub
parent afbba5d928
commit 40c857748f
4 changed files with 982 additions and 269 deletions
+581 -1
View File
@@ -68,7 +68,7 @@ use kernel_bridge::{
stop_local_socket_server as stop_local_socket_server_inner,
};
use napi_derive_ohos::napi;
use runtime::state::runtime_state::RuntimeAggregateState;
use runtime::state::runtime_state::{RuntimeAggregateState, RuntimeInstanceState};
use std::collections::{HashMap, HashSet};
use std::format;
use std::sync::{Arc, Mutex};
@@ -89,10 +89,13 @@ pub(crate) static INSTANCE_MANAGER: once_cell::sync::Lazy<Arc<NativeInstanceMana
});
static WEB_CLIENTS: once_cell::sync::Lazy<Mutex<HashMap<String, ManagedWebClient>>> =
once_cell::sync::Lazy::new(|| Mutex::new(HashMap::new()));
const PRO_CONFIG_SERVER_CLIENT_ID: &str = "__easytier_pro_config_server_client__";
#[derive(Default)]
struct TrackedWebClientHooks {
instance_ids: Mutex<HashSet<Uuid>>,
network_names_by_instance_id: Mutex<HashMap<Uuid, String>>,
events: Mutex<Vec<serde_json::Value>>,
}
struct ManagedWebClient {
@@ -100,6 +103,13 @@ struct ManagedWebClient {
hooks: Arc<TrackedWebClientHooks>,
}
fn network_name_for_instance(id: &Uuid) -> Option<String> {
INSTANCE_MANAGER
.config(*id)
.map(|config| config.get_network_identity().network_name)
.filter(|name| !name.trim().is_empty())
}
#[async_trait::async_trait]
impl WebClientHooks for TrackedWebClientHooks {
async fn post_run_network_instance(&self, id: &Uuid) -> Result<(), String> {
@@ -107,13 +117,43 @@ impl WebClientHooks for TrackedWebClientHooks {
.lock()
.map_err(|err| err.to_string())?
.insert(*id);
let network_name = network_name_for_instance(id);
if let Some(network_name) = &network_name {
self.network_names_by_instance_id
.lock()
.map_err(|err| err.to_string())?
.insert(*id, network_name.clone());
}
self.events
.lock()
.map_err(|err| err.to_string())?
.push(serde_json::json!({
"event": "run_network_instance",
"success": true,
"instance_id": id.to_string(),
"instance_name": id.to_string(),
"network_name": network_name,
}));
Ok(())
}
async fn post_remove_network_instances(&self, ids: &[Uuid]) -> Result<(), String> {
let mut guard = self.instance_ids.lock().map_err(|err| err.to_string())?;
let mut events = self.events.lock().map_err(|err| err.to_string())?;
let mut network_names_by_instance_id = self
.network_names_by_instance_id
.lock()
.map_err(|err| err.to_string())?;
for id in ids {
guard.remove(id);
let network_name = network_names_by_instance_id.remove(id);
events.push(serde_json::json!({
"event": "delete_network_instance",
"success": true,
"instance_id": id.to_string(),
"instance_name": id.to_string(),
"network_name": network_name,
}));
}
Ok(())
}
@@ -243,6 +283,388 @@ fn run_config_server_instance(config_id: &str, config: &NetworkConfig) -> bool {
}
}
fn run_config_server_client(
url: &str,
hostname: Option<String>,
machine_id: Option<String>,
secure_mode: bool,
) -> bool {
let trimmed_url = url.trim();
if trimmed_url.is_empty() {
ohrs_log_error!("[Rust] config server url missing");
return false;
}
let _ = stop_web_client(PRO_CONFIG_SERVER_CLIENT_ID);
let hooks = Arc::new(TrackedWebClientHooks::default());
if !ensure_local_socket_server_started() {
return false;
}
let machine_id_opts = MachineIdOptions {
explicit_machine_id: machine_id.filter(|value| !value.trim().is_empty()),
state_dir: None,
};
let client = ASYNC_RUNTIME.block_on(run_web_client(
trimmed_url,
machine_id_opts,
hostname.filter(|value| !value.trim().is_empty()),
secure_mode,
INSTANCE_MANAGER.clone(),
Some(hooks.clone()),
));
let client = match client {
Ok(client) => client,
Err(err) => {
ohrs_log_error!("[Rust] start pro config server client failed {}", err);
return false;
}
};
match WEB_CLIENTS.lock() {
Ok(mut guard) => {
guard.insert(
PRO_CONFIG_SERVER_CLIENT_ID.to_string(),
ManagedWebClient {
_client: client,
hooks,
},
);
true
}
Err(err) => {
ohrs_log_error!("[Rust] store pro config server client failed {}", err);
false
}
}
}
fn pro_config_server_client_connected() -> bool {
WEB_CLIENTS
.lock()
.ok()
.and_then(|guard| {
guard
.get(PRO_CONFIG_SERVER_CLIENT_ID)
.map(|managed| managed._client.is_connected())
})
.unwrap_or(false)
}
fn drain_config_server_events_inner() -> Vec<serde_json::Value> {
let Ok(guard) = WEB_CLIENTS.lock() else {
return Vec::new();
};
let Some(managed) = guard.get(PRO_CONFIG_SERVER_CLIENT_ID) else {
return Vec::new();
};
let Ok(mut events) = managed.hooks.events.lock() else {
return Vec::new();
};
events.drain(..).collect()
}
fn stop_runtime_inner() -> bool {
let mut ok = stop_web_client(PRO_CONFIG_SERVER_CLIENT_ID);
let ids = INSTANCE_MANAGER.instance_ids();
if !ids.is_empty() {
ok = ASYNC_RUNTIME
.block_on(INSTANCE_MANAGER.delete_network_instances(ids))
.map(|_| true)
.unwrap_or_else(|err| {
ohrs_log_error!("[Rust] stop runtime instances failed {}", err);
false
})
&& ok;
}
maybe_stop_local_socket_server();
ok
}
fn is_pro_internal_instance(instance: &RuntimeInstanceState) -> bool {
instance.instance_id == PRO_CONFIG_SERVER_CLIENT_ID
|| instance.config_id == PRO_CONFIG_SERVER_CLIENT_ID
|| instance.display_name == PRO_CONFIG_SERVER_CLIENT_ID
}
fn runtime_instance_label(instance: &RuntimeInstanceState) -> String {
let display_name = instance.display_name.trim();
if !display_name.is_empty() && display_name != PRO_CONFIG_SERVER_CLIENT_ID {
return display_name.to_string();
}
let instance_id = instance.instance_id.trim();
if !instance_id.is_empty() {
return instance_id.to_string();
}
instance.config_id.clone()
}
fn runtime_instance_matches(instance: &RuntimeInstanceState, selector: &str) -> bool {
let target = selector.trim();
if target.is_empty() {
return false;
}
instance.instance_id == target
|| instance.config_id == target
|| instance.display_name == target
|| runtime_instance_label(instance) == target
}
fn read_json_string_path<'a>(value: &'a serde_json::Value, path: &[&str]) -> Option<&'a str> {
let mut cursor = value;
for key in path {
cursor = cursor.get(*key)?;
}
cursor.as_str().filter(|value| !value.trim().is_empty())
}
fn selected_instance_from_payload(payload_json: &str) -> Option<String> {
let value = serde_json::from_str::<serde_json::Value>(payload_json).ok()?;
for path in [
&["instance", "instance_selector", "name"][..],
&["instance", "instanceSelector", "name"][..],
&["instance", "instance_selector", "id"][..],
&["instance", "instanceSelector", "id"][..],
&["instance", "name"][..],
&["instance", "id"][..],
&["instance_name"][..],
&["instanceName"][..],
&["instance_id"][..],
&["instanceId"][..],
&["id"][..],
] {
if let Some(value) = read_json_string_path(&value, path) {
return Some(value.to_string());
}
}
None
}
fn find_runtime_instance<'a>(
state: &'a RuntimeAggregateState,
selector: Option<&str>,
) -> Option<&'a RuntimeInstanceState> {
if let Some(selector) = selector
&& let Some(instance) = state.instances.iter().find(|instance| {
!is_pro_internal_instance(instance) && runtime_instance_matches(instance, selector)
})
{
return Some(instance);
}
state
.instances
.iter()
.find(|instance| !is_pro_internal_instance(instance) && instance.running)
}
fn list_instances_json_inner(state: &RuntimeAggregateState) -> String {
let mut instances = serde_json::Map::new();
for instance in state
.instances
.iter()
.filter(|instance| !is_pro_internal_instance(instance) && instance.running)
{
let label = runtime_instance_label(instance);
if !label.trim().is_empty() {
instances.insert(
label,
serde_json::Value::String(instance.instance_id.clone()),
);
}
}
serde_json::Value::Object(instances).to_string()
}
fn list_pro_instances_json_inner(
state: &RuntimeAggregateState,
network_names_by_instance_id: &HashMap<String, String>,
) -> String {
let mut instances = serde_json::Map::new();
for instance in state.instances.iter().filter(|instance| instance.running) {
let Some(network_name) = network_names_by_instance_id.get(&instance.instance_id) else {
continue;
};
let label = if network_name.trim().is_empty() {
instance.instance_id.clone()
} else {
network_name.clone()
};
instances.insert(
label,
serde_json::Value::String(instance.instance_id.clone()),
);
}
serde_json::Value::Object(instances).to_string()
}
fn find_pro_runtime_instance<'a>(
state: &'a RuntimeAggregateState,
network_names_by_instance_id: &HashMap<String, String>,
selector: Option<&str>,
) -> Option<(&'a RuntimeInstanceState, String)> {
let mut tracked_instances = state.instances.iter().filter_map(|instance| {
let network_name = network_names_by_instance_id.get(&instance.instance_id)?;
Some((instance, network_name))
});
if let Some(selector) = selector {
return tracked_instances
.filter(|(instance, _)| instance.running)
.find(|(instance, network_name)| {
selector == network_name.as_str() || runtime_instance_matches(instance, selector)
})
.map(|(instance, network_name)| (instance, network_name.clone()));
}
tracked_instances
.find(|(instance, _)| instance.running)
.map(|(instance, network_name)| (instance, network_name.clone()))
}
fn call_pro_json_rpc_inner(
state: &RuntimeAggregateState,
network_names_by_instance_id: &HashMap<String, String>,
service_name: &str,
method_name: &str,
payload_json: &str,
) -> String {
let selector = selected_instance_from_payload(payload_json);
let Some((instance, network_name)) =
find_pro_runtime_instance(state, network_names_by_instance_id, selector.as_deref())
else {
return "{}".to_string();
};
let method = method_name.trim();
let service = service_name.trim();
let response = match (service, method) {
(_, "show_node_info") => serde_json::json!({
"node_info": instance.my_node_info,
}),
(_, "list_route") => serde_json::json!({
"routes": instance.routes,
}),
(_, "list_peer") => serde_json::json!({
"my_info": instance.my_node_info,
"peer_infos": instance.peers,
}),
(_, "get_stats") => {
let mut rx_bytes = 0_i64;
let mut tx_bytes = 0_i64;
for peer in &instance.peers {
for conn in &peer.conns {
if let Some(stats) = &conn.stats {
rx_bytes = rx_bytes.saturating_add(stats.rx_bytes);
tx_bytes = tx_bytes.saturating_add(stats.tx_bytes);
}
}
}
serde_json::json!({
"metrics": [
{
"name": "traffic_bytes_self_rx",
"labels": { "network_name": network_name },
"value": rx_bytes,
},
{
"name": "traffic_bytes_self_tx",
"labels": { "network_name": network_name },
"value": tx_bytes,
}
]
})
}
_ => serde_json::json!({}),
};
response.to_string()
}
fn pro_runtime_registry_snapshot() -> HashMap<String, String> {
WEB_CLIENTS
.lock()
.ok()
.and_then(|clients| {
clients
.get(PRO_CONFIG_SERVER_CLIENT_ID)
.and_then(|managed| managed.hooks.network_names_by_instance_id.lock().ok())
.map(|registry| {
registry
.iter()
.map(|(instance_id, network_name)| {
(instance_id.to_string(), network_name.clone())
})
.collect()
})
})
.unwrap_or_default()
}
fn call_json_rpc_inner(service_name: &str, method_name: &str, payload_json: &str) -> String {
let state = collect_runtime_state_inner();
let selector = selected_instance_from_payload(payload_json);
let Some(instance) = find_runtime_instance(&state, selector.as_deref()) else {
return "{}".to_string();
};
let method = method_name.trim();
let service = service_name.trim();
let response = match (service, method) {
(_, "show_node_info") => serde_json::json!({
"node_info": instance.my_node_info,
}),
(_, "list_route") => serde_json::json!({
"routes": instance.routes,
}),
(_, "list_peer") => serde_json::json!({
"my_info": instance.my_node_info,
"peer_infos": instance.peers,
}),
(_, "get_stats") => {
let mut rx_bytes = 0_i64;
let mut tx_bytes = 0_i64;
for peer in &instance.peers {
for conn in &peer.conns {
if let Some(stats) = &conn.stats {
rx_bytes = rx_bytes.saturating_add(stats.rx_bytes);
tx_bytes = tx_bytes.saturating_add(stats.tx_bytes);
}
}
}
let network_name = runtime_instance_label(instance);
serde_json::json!({
"metrics": [
{
"name": "traffic_bytes_self_rx",
"labels": { "network_name": network_name },
"value": rx_bytes,
},
{
"name": "traffic_bytes_self_tx",
"labels": { "network_name": network_name },
"value": tx_bytes,
}
]
})
}
_ => serde_json::json!({}),
};
response.to_string()
}
fn resolve_instance_id_from_state(
state: &RuntimeAggregateState,
instance_name: &str,
) -> Option<String> {
let instance = state.instances.iter().find(|instance| {
!is_pro_internal_instance(instance) && runtime_instance_matches(instance, instance_name)
})?;
Some(instance.instance_id.clone())
}
fn resolve_instance_id_inner(instance_name: &str) -> Option<String> {
resolve_instance_id_from_state(&collect_runtime_state_inner(), instance_name)
}
pub(crate) fn build_default_network_config_json() -> Result<String, String> {
let config = NetworkConfig::new_from_config(TomlConfigLoader::default())
.map_err(|e| format!("default_network_config failed {}", e))?;
@@ -440,6 +862,78 @@ pub fn stop_network_instance(config_ids: Vec<String>) -> bool {
exports::runtime_api::stop_network_instance(config_ids, stop_kernel)
}
#[napi]
pub fn start_config_server_client(
url: String,
hostname: Option<String>,
machine_id: Option<String>,
secure_mode: Option<bool>,
) -> bool {
run_config_server_client(&url, hostname, machine_id, secure_mode.unwrap_or(false))
}
#[napi]
pub fn stop_config_server_client() -> bool {
stop_web_client(PRO_CONFIG_SERVER_CLIENT_ID)
}
#[napi]
pub fn is_config_server_client_connected() -> bool {
pro_config_server_client_connected()
}
#[napi]
pub fn stop_runtime() -> bool {
stop_runtime_inner()
}
#[napi]
pub fn drain_config_server_events() -> String {
serde_json::to_string(&drain_config_server_events_inner()).unwrap_or_else(|_| "[]".to_string())
}
#[napi]
pub fn collect_runtime_state_json() -> String {
serde_json::to_string(&collect_runtime_state_inner()).unwrap_or_else(|_| "{}".to_string())
}
#[napi]
pub fn list_instances_json() -> String {
list_instances_json_inner(&collect_runtime_state_inner())
}
#[napi]
pub fn list_pro_instances_json() -> String {
let registry = pro_runtime_registry_snapshot();
list_pro_instances_json_inner(&collect_runtime_state_inner(), &registry)
}
#[napi]
pub fn call_json_rpc(service_name: String, method_name: String, payload_json: String) -> String {
call_json_rpc_inner(&service_name, &method_name, &payload_json)
}
#[napi]
pub fn call_pro_json_rpc(
service_name: String,
method_name: String,
payload_json: String,
) -> String {
let registry = pro_runtime_registry_snapshot();
call_pro_json_rpc_inner(
&collect_runtime_state_inner(),
&registry,
&service_name,
&method_name,
&payload_json,
)
}
#[napi]
pub fn resolve_instance_id(instance_name: String) -> Option<String> {
resolve_instance_id_inner(&instance_name)
}
#[napi]
pub fn easytier_version() -> String {
EASYTIER_VERSION.to_string()
@@ -512,6 +1006,92 @@ mod tests {
.any(|field| field.name == "enabled")
);
}
fn pro_test_state() -> RuntimeAggregateState {
RuntimeAggregateState {
instances: vec![
RuntimeInstanceState {
config_id: "0c4b33ba-4ed5-42d8-9095-21b786c66e94".to_string(),
instance_id: "0c4b33ba-4ed5-42d8-9095-21b786c66e94".to_string(),
display_name: "0c4b33ba-4ed5-42d8-9095-21b786c66e94".to_string(),
running: true,
tun_required: false,
tun_attached: false,
magic_dns_enabled: false,
need_exit_node: false,
error_message: None,
my_node_info: None,
events: vec![],
routes: vec![],
peers: vec![],
},
RuntimeInstanceState {
config_id: "ec7b6a3c-aeae-4c0e-844e-f7ec2dbdc2ce".to_string(),
instance_id: "ec7b6a3c-aeae-4c0e-844e-f7ec2dbdc2ce".to_string(),
display_name: "ec7b6a3c-aeae-4c0e-844e-f7ec2dbdc2ce".to_string(),
running: true,
tun_required: false,
tun_attached: false,
magic_dns_enabled: false,
need_exit_node: false,
error_message: None,
my_node_info: None,
events: vec![],
routes: vec![],
peers: vec![],
},
],
tun: runtime::state::runtime_state::TunAggregateState {
active: false,
attached_instance_ids: vec![],
aggregated_routes: vec![],
dns_servers: vec![],
need_rebuild: false,
},
running_instance_count: 2,
}
}
fn pro_test_registry() -> HashMap<String, String> {
HashMap::from([(
"0c4b33ba-4ed5-42d8-9095-21b786c66e94".to_string(),
"office-network".to_string(),
)])
}
#[test]
fn pro_instance_list_uses_registry_network_name_and_excludes_untracked_instances() {
assert_eq!(
list_pro_instances_json_inner(&pro_test_state(), &pro_test_registry()),
r#"{"office-network":"0c4b33ba-4ed5-42d8-9095-21b786c66e94"}"#,
);
}
#[test]
fn pro_json_rpc_selects_instance_by_network_name_and_labels_traffic() {
let response = call_pro_json_rpc_inner(
&pro_test_state(),
&pro_test_registry(),
"api.instance.StatsRpcService",
"get_stats",
r#"{"instance":{"instance_selector":{"name":"office-network"}}}"#,
);
let response: serde_json::Value = serde_json::from_str(&response).unwrap();
assert_eq!(
response["metrics"][0]["labels"]["network_name"],
"office-network",
);
}
#[test]
fn resolve_instance_id_does_not_fall_back_for_unknown_selector() {
let state = pro_test_state();
assert_eq!(
resolve_instance_id_from_state(&state, "0c4b33ba-4ed5-42d8-9095-21b786c66e94"),
Some("0c4b33ba-4ed5-42d8-9095-21b786c66e94".to_string()),
);
assert_eq!(resolve_instance_id_from_state(&state, "stale-name"), None);
}
}
pub(crate) fn collect_runtime_state_inner() -> RuntimeAggregateState {