fix(ohos): keep nearby deployments ephemeral

This commit is contained in:
FrankHan
2026-08-03 03:41:02 +08:00
parent 5dbc71e58e
commit f7645cb410
2 changed files with 370 additions and 168 deletions
+47 -13
View File
@@ -733,10 +733,7 @@ pub(crate) fn run_network_instance_from_json(cfg_json: &str) -> bool {
return false;
}
let Some(config_control) = nearby_management::runtime_management_config_control(inst_id) else {
ohrs_log_error!("[Rust] nearby management config store is not initialized");
return false;
};
let config_control = nearby_management::runtime_management_config_control(inst_id);
if !nearby_management::ensure_runtime_management_server_started() {
return false;
}
@@ -748,9 +745,6 @@ pub(crate) fn run_network_instance_from_json(cfg_json: &str) -> bool {
}
Err(err) => {
ohrs_log_error!("[Rust] start_kernel failed for {}: {}", inst_id, err);
if INSTANCE_MANAGER.instance_ids().is_empty() {
let _ = nearby_management::stop_runtime_management_server();
}
false
}
}
@@ -863,16 +857,12 @@ pub fn start_kernel(config_id: String) -> bool {
#[napi]
pub fn stop_kernel(config_id: String) -> bool {
let stopped = exports::runtime_api::stop_kernel(
exports::runtime_api::stop_kernel(
config_id,
stop_web_client,
parse_instance_uuid,
maybe_stop_local_socket_server,
);
if stopped && INSTANCE_MANAGER.instance_ids().is_empty() {
let _ = nearby_management::stop_runtime_management_server();
}
stopped
)
}
#[napi]
@@ -977,6 +967,50 @@ pub fn run_network_instance(cfg_json: String) -> bool {
run_network_instance_from_json(&cfg_json)
}
/// Starts the management server in the VPN Extension process even when no
/// network instance is active, allowing a nearby controller to deploy a
/// one-shot config through the canonical Core RPC surface.
#[napi]
pub fn start_nearby_management_host() -> bool {
nearby_management::ensure_runtime_management_server_started()
}
#[napi]
pub fn stop_nearby_management_host() -> bool {
nearby_management::stop_runtime_management_server()
}
#[napi]
pub fn drain_nearby_host_commands() -> Vec<nearby_management::NearbyHostCommand> {
nearby_management::drain_nearby_host_commands()
}
#[napi]
pub fn complete_nearby_host_command(
request_id: String,
success: bool,
error: Option<String>,
) -> bool {
nearby_management::complete_nearby_host_command(request_id, success, error)
}
/// Returns 1 for canonical Core RPC packets, 2 for the OHOS-private settings
/// envelope, and 0 for malformed or unsupported data.
#[napi]
pub fn nearby_management_packet_kind(packet: Uint8Array) -> i32 {
nearby_management::nearby_management_packet_kind(packet)
}
#[napi]
pub fn encode_nearby_ohos_packet(envelope_json: String) -> Option<Uint8Array> {
nearby_management::encode_nearby_ohos_packet(envelope_json)
}
#[napi]
pub fn decode_nearby_ohos_packet(packet: Uint8Array) -> Option<String> {
nearby_management::decode_nearby_ohos_packet(packet)
}
/// Opens one Core RPC endpoint for a HarmonyOS collaboration session.
///
/// The Harmony layer transports the returned native packets verbatim with
@@ -6,7 +6,7 @@
//! exposes the empty `InstanceManager` loaded by `EntryAbility`.
use std::{
collections::HashMap,
collections::{HashMap, VecDeque},
fmt, io,
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
path::{Path, PathBuf},
@@ -16,20 +16,19 @@ use std::{
atomic::{AtomicBool, Ordering},
},
task::{Context, Poll},
time::Duration,
};
use async_trait::async_trait;
use bytes::BytesMut;
use easytier::{
common::config::{ConfigLoader as _, NetworkConfigExt as _, TomlConfigLoader},
instance::factory::NativeInstanceFactory,
common::config::NetworkConfigExt as _, instance::factory::NativeInstanceFactory,
rpc_service::logger::NativeLoggerControl,
};
use easytier_core::{
config::{api::network_config_from_toml, toml::TomlConfig},
management::{
ConfigFileControl, ConfigFilePermission, ConfigFileStorage, InstanceMutationHooks,
LoggerManagementRpc, ProcessManagementRpc, register_instance_management_rpc,
ConfigFileControl, LoggerManagementRpc, ProcessManagementRpc, UnsupportedConfigFileStorage,
register_instance_management_rpc,
},
packet::{PacketType, ZCPacket, ZCPacketType},
rpc::{bidirect::BidirectRpcManager, client::Client, standalone::StandAloneServer},
@@ -64,27 +63,24 @@ use easytier_proto::{
rpc_types::{controller::BaseController, error::Error as RpcError},
};
use futures::{SinkExt, StreamExt};
use napi_derive_ohos::napi;
use napi_ohos::bindgen_prelude::Uint8Array;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use tokio::{
io::{AsyncRead, AsyncWrite, ReadBuf},
net::{UnixListener, UnixStream},
sync::mpsc::{Receiver, Sender, channel},
sync::{
mpsc::{Receiver, Sender, channel},
oneshot,
},
task::JoinHandle,
time::timeout,
};
use url::Url;
use uuid::Uuid;
use crate::{
ASYNC_RUNTIME, INSTANCE_MANAGER,
config::{
repository::{
cache_runtime_config_snapshot, config_root_dir, get_runtime_config_snapshot,
load_config_json, save_config_record,
},
storage::config_meta::get_config_display_name,
},
};
use crate::{ASYNC_RUNTIME, INSTANCE_MANAGER, config::repository::config_root_dir};
const MAX_NEARBY_SESSIONS: usize = 8;
const MAX_SESSION_KEY_LENGTH: usize = 128;
@@ -94,7 +90,50 @@ const MAX_DRAIN_PACKETS: usize = 256;
const PACKET_QUEUE_CAPACITY: usize = 256;
const RPC_PEER_ID: u32 = 1;
const MANAGEMENT_SOCKET_FILE_NAME: &str = "easytier-nearby-management.sock";
const MANAGEMENT_CONFIG_DIR_NAME: &str = ".easytier-nearby-management";
const MAX_PENDING_HOST_COMMANDS: usize = 8;
const HOST_COMMAND_TIMEOUT: Duration = Duration::from_secs(30);
const HOST_COMMAND_START: &str = "start_once";
const HOST_COMMAND_STOP: &str = "stop";
const OHOS_PRIVATE_PACKET_TYPE: u8 = 0xF0;
const OHOS_PRIVATE_SCHEMA_VERSION: u32 = 1;
const MAX_OHOS_PRIVATE_PAYLOAD_BYTES: usize = 16 * 1024;
const MAX_OHOS_PRIVATE_ERROR_LENGTH: usize = 512;
const OHOS_OPERATION_GET_SETTINGS: &str = "get_settings";
const OHOS_OPERATION_UPDATE_SETTING: &str = "update_setting";
#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct NearbyOhosEnvelope {
schema_version: u32,
kind: String,
request_id: String,
operation: String,
payload_json: Option<String>,
ok: Option<bool>,
error: Option<String>,
}
#[napi(object)]
#[derive(Clone)]
pub struct NearbyHostCommand {
pub request_id: String,
pub operation: String,
pub instance_id: String,
pub config_json: Option<String>,
}
struct NearbyHostCommandState {
queued: VecDeque<NearbyHostCommand>,
completions: HashMap<String, oneshot::Sender<Result<(), String>>>,
}
static NEARBY_HOST_COMMANDS: once_cell::sync::Lazy<Mutex<NearbyHostCommandState>> =
once_cell::sync::Lazy::new(|| {
Mutex::new(NearbyHostCommandState {
queued: VecDeque::new(),
completions: HashMap::new(),
})
});
#[derive(Clone, Copy, PartialEq, Eq)]
enum NearbyRole {
@@ -228,91 +267,77 @@ impl Drop for NearbyUnixListener {
}
}
struct NearbyConfigStorage;
async fn dispatch_host_command(
operation: &str,
instance_id: Uuid,
config_json: Option<String>,
) -> Result<(), RpcError> {
let request_id = Uuid::new_v4().to_string();
let command = NearbyHostCommand {
request_id: request_id.clone(),
operation: operation.to_owned(),
instance_id: instance_id.to_string(),
config_json,
};
let (sender, receiver) = oneshot::channel::<Result<(), String>>();
{
let mut state = NEARBY_HOST_COMMANDS
.lock()
.map_err(|_| anyhow::anyhow!("HarmonyOS host command queue is unavailable"))?;
if state.queued.len() >= MAX_PENDING_HOST_COMMANDS
|| state.completions.len() >= MAX_PENDING_HOST_COMMANDS
{
return Err(anyhow::anyhow!("HarmonyOS host command queue is full").into());
}
state.completions.insert(request_id.clone(), sender);
state.queued.push_back(command);
}
#[async_trait]
impl ConfigFileStorage for NearbyConfigStorage {
async fn inspect(&self, path: &Path) -> ConfigFileControl {
match config_id_from_management_path(path) {
Ok(_) => ConfigFileControl::new(
Some(path.to_owned()),
ConfigFilePermission::from(ConfigFilePermission::NO_DELETE),
),
Err(_) => ConfigFileControl::new(
Some(path.to_owned()),
ConfigFilePermission::from(
ConfigFilePermission::READ_ONLY | ConfigFilePermission::NO_DELETE,
),
),
let completion = timeout(HOST_COMMAND_TIMEOUT, receiver).await;
let timed_out = completion.is_err();
if let Ok(mut state) = NEARBY_HOST_COMMANDS.lock() {
state.completions.remove(&request_id);
if timed_out {
state
.queued
.retain(|command| command.request_id != request_id);
}
}
async fn read(&self, path: &Path) -> anyhow::Result<Option<Vec<u8>>> {
let config_id = config_id_from_management_path(path)?;
let Some(raw) = load_config_json(&config_id.to_string()) else {
return Ok(None);
};
let config = serde_json::from_str::<easytier_proto::api::manage::NetworkConfig>(&raw)?;
Ok(Some(config.gen_config()?.dump().into_bytes()))
}
async fn write(&self, path: &Path, contents: &[u8]) -> anyhow::Result<()> {
let config_id = config_id_from_management_path(path)?;
let toml = std::str::from_utf8(contents)?;
let config = TomlConfigLoader::new_from_str(toml)?;
if config.get_id() != config_id {
anyhow::bail!("nearby management cannot change the running instance ID");
}
let network_config = network_config_from_toml(&config);
let display_name = get_config_display_name(&config_id.to_string())
.or_else(|| {
get_runtime_config_snapshot(&config_id.to_string())
.map(|snapshot| snapshot.display_name)
})
.unwrap_or_else(|| config.get_inst_name());
let raw = serde_json::to_string(&network_config)?;
save_config_record(config_id.to_string(), display_name, raw)
.ok_or_else(|| anyhow::anyhow!("failed to persist nearby management config"))?;
Ok(())
}
async fn remove(&self, _path: &Path) -> anyhow::Result<()> {
anyhow::bail!("nearby management never deletes a saved HarmonyOS config")
match completion {
Ok(Ok(Ok(()))) => Ok(()),
Ok(Ok(Err(error))) => Err(anyhow::anyhow!(error).into()),
Ok(Err(_)) => Err(anyhow::anyhow!("HarmonyOS host command was cancelled").into()),
Err(_) => Err(anyhow::anyhow!("HarmonyOS host command timed out").into()),
}
}
struct NearbyMutationHooks;
pub(crate) fn drain_nearby_host_commands() -> Vec<NearbyHostCommand> {
NEARBY_HOST_COMMANDS
.lock()
.map(|mut state| state.queued.drain(..).collect())
.unwrap_or_default()
}
#[async_trait]
impl InstanceMutationHooks for NearbyMutationHooks {
async fn pre_run_network_instance(&self, config: &TomlConfig) -> Result<(), String> {
let instance_ids = INSTANCE_MANAGER.instance_ids();
if instance_ids.len() != 1 || instance_ids[0] != config.get_id() {
return Err(
"nearby management may only overwrite the single running HarmonyOS instance"
.to_owned(),
);
}
pub(crate) fn complete_nearby_host_command(
request_id: String,
success: bool,
error: Option<String>,
) -> bool {
let sender = NEARBY_HOST_COMMANDS
.lock()
.ok()
.and_then(|mut state| state.completions.remove(&request_id));
let Some(sender) = sender else {
return false;
};
let result = if success {
Ok(())
}
async fn post_run_network_instance(&self, instance_id: &Uuid) -> Result<(), String> {
let config = INSTANCE_MANAGER
.config(*instance_id)
.ok_or_else(|| format!("instance {instance_id} is missing after restart"))?;
let display_name = get_config_display_name(&instance_id.to_string())
.or_else(|| {
get_runtime_config_snapshot(&instance_id.to_string())
.map(|snapshot| snapshot.display_name)
})
.unwrap_or_else(|| config.get_inst_name());
cache_runtime_config_snapshot(
instance_id.to_string(),
display_name,
network_config_from_toml(&config),
);
Ok(())
}
} else {
Err(error
.filter(|message| !message.trim().is_empty())
.unwrap_or_else(|| "HarmonyOS host rejected the command".to_owned()))
};
sender.send(result).is_ok()
}
#[derive(Clone)]
@@ -325,43 +350,52 @@ impl NearbyWebClientService {
Self {
inner: ProcessManagementRpc::new(
INSTANCE_MANAGER.clone(),
Arc::new(NearbyMutationHooks),
Arc::new(NearbyConfigStorage),
Arc::new(()),
Arc::new(UnsupportedConfigFileStorage),
),
}
}
fn ensure_safe_overwrite(&self, request: &RunNetworkInstanceRequest) -> Result<(), RpcError> {
if !request.overwrite {
return Err(anyhow::anyhow!("nearby management requires an explicit overwrite").into());
}
let requested_id = request
.inst_id
.clone()
.map(Uuid::from)
.ok_or_else(|| anyhow::anyhow!("nearby management requires an instance ID"))?;
let running = INSTANCE_MANAGER.instance_ids();
if running.len() != 1 || running[0] != requested_id {
fn validate_one_shot_start(
&self,
request: &RunNetworkInstanceRequest,
) -> Result<(Uuid, String), RpcError> {
if request.overwrite {
return Err(anyhow::anyhow!(
"nearby management may only overwrite the single running HarmonyOS instance"
"HarmonyOS nearby deployment is one-shot and never overwrites a running config"
)
.into());
}
let expected_path = management_config_path(requested_id)
.ok_or_else(|| anyhow::anyhow!("HarmonyOS config store is not initialized"))?;
let control = INSTANCE_MANAGER
.config_control(requested_id)
.ok_or_else(|| anyhow::anyhow!("running instance config control is missing"))?;
if control.path.as_deref() != Some(expected_path.as_path())
|| control.is_read_only()
|| !control.is_no_delete()
{
let requested_id = request.inst_id.clone().map(Uuid::from).ok_or_else(|| {
anyhow::anyhow!("HarmonyOS nearby deployment requires an instance ID")
})?;
let config = request
.config
.as_ref()
.ok_or_else(|| anyhow::anyhow!("HarmonyOS nearby deployment requires a config"))?;
config.gen_config()?;
let config_id = config
.instance_id
.as_deref()
.ok_or_else(|| anyhow::anyhow!("HarmonyOS nearby config has no instance ID"))?;
let parsed_config_id = Uuid::parse_str(config_id)
.map_err(|_| anyhow::anyhow!("HarmonyOS nearby config instance ID is invalid"))?;
if parsed_config_id != requested_id {
return Err(anyhow::anyhow!(
"running instance is not owned by the HarmonyOS nearby console"
"HarmonyOS nearby config instance ID does not match the request"
)
.into());
}
Ok(())
if !INSTANCE_MANAGER.instance_ids().is_empty() {
return Err(anyhow::anyhow!(
"HarmonyOS config is running; stop it before deploying another config"
)
.into());
}
let config_json = serde_json::to_string(config).map_err(|error| {
anyhow::anyhow!("failed to encode HarmonyOS one-shot config: {error}")
})?;
Ok((requested_id, config_json))
}
}
@@ -379,11 +413,20 @@ impl WebClientService for NearbyWebClientService {
async fn run_network_instance(
&self,
controller: BaseController,
_controller: BaseController,
request: RunNetworkInstanceRequest,
) -> Result<RunNetworkInstanceResponse, RpcError> {
self.ensure_safe_overwrite(&request)?;
self.inner.run_network_instance(controller, request).await
let (instance_id, config_json) = self.validate_one_shot_start(&request)?;
dispatch_host_command(HOST_COMMAND_START, instance_id, Some(config_json)).await?;
if !INSTANCE_MANAGER.instance_ids().contains(&instance_id) {
return Err(anyhow::anyhow!(
"HarmonyOS host reported success but the instance is not running"
)
.into());
}
Ok(RunNetworkInstanceResponse {
inst_id: Some(instance_id.into()),
})
}
async fn retain_network_instance(
@@ -413,9 +456,38 @@ impl WebClientService for NearbyWebClientService {
async fn delete_network_instance(
&self,
_controller: BaseController,
_request: DeleteNetworkInstanceRequest,
request: DeleteNetworkInstanceRequest,
) -> Result<DeleteNetworkInstanceResponse, RpcError> {
Err(anyhow::anyhow!("nearby management cannot stop or delete a HarmonyOS instance").into())
if request.inst_ids.len() != 1 {
return Err(anyhow::anyhow!(
"HarmonyOS nearby control stops exactly one running instance"
)
.into());
}
let instance_id = Uuid::from(request.inst_ids[0].clone());
let running = INSTANCE_MANAGER.instance_ids();
if running.is_empty() {
return Ok(DeleteNetworkInstanceResponse {
remain_inst_ids: Vec::new(),
});
}
if running.len() != 1 || running[0] != instance_id {
return Err(anyhow::anyhow!(
"requested HarmonyOS instance is not the active controlled instance"
)
.into());
}
dispatch_host_command(HOST_COMMAND_STOP, instance_id, None).await?;
let remaining = INSTANCE_MANAGER.instance_ids();
if remaining.contains(&instance_id) {
return Err(anyhow::anyhow!(
"HarmonyOS host reported success but the instance is still running"
)
.into());
}
Ok(DeleteNetworkInstanceResponse {
remain_inst_ids: remaining.into_iter().map(Into::into).collect(),
})
}
async fn get_network_instance_config(
@@ -447,13 +519,8 @@ struct NearbyManagementHostServer {
static NEARBY_HOST_SERVER: once_cell::sync::Lazy<Mutex<Option<NearbyManagementHostServer>>> =
once_cell::sync::Lazy::new(|| Mutex::new(None));
pub(crate) fn runtime_management_config_control(instance_id: Uuid) -> Option<ConfigFileControl> {
management_config_path(instance_id).map(|path| {
ConfigFileControl::new(
Some(path),
ConfigFilePermission::from(ConfigFilePermission::NO_DELETE),
)
})
pub(crate) fn runtime_management_config_control(_instance_id: Uuid) -> ConfigFileControl {
ConfigFileControl::STATIC_CONFIG
}
pub(crate) fn ensure_runtime_management_server_started() -> bool {
@@ -494,10 +561,17 @@ pub(crate) fn ensure_runtime_management_server_started() -> bool {
}
pub(crate) fn stop_runtime_management_server() -> bool {
NEARBY_HOST_SERVER
let stopped = NEARBY_HOST_SERVER
.lock()
.map(|mut state| state.take().is_some())
.unwrap_or(false)
.unwrap_or(false);
if let Ok(mut commands) = NEARBY_HOST_COMMANDS.lock() {
commands.queued.clear();
for (_, sender) in commands.completions.drain() {
let _ = sender.send(Err("HarmonyOS nearby management host stopped".to_owned()));
}
}
stopped
}
struct NearbyManagementSession {
@@ -659,27 +733,6 @@ fn management_socket_url(path: &Path) -> Url {
.expect("HarmonyOS sandbox path must form a Unix URL")
}
fn management_config_path(instance_id: Uuid) -> Option<PathBuf> {
config_root_dir().map(|root| {
root.join(MANAGEMENT_CONFIG_DIR_NAME)
.join(format!("{instance_id}.toml"))
})
}
fn config_id_from_management_path(path: &Path) -> anyhow::Result<Uuid> {
let file_stem = path
.file_stem()
.and_then(|value| value.to_str())
.ok_or_else(|| anyhow::anyhow!("nearby management config path is invalid"))?;
let instance_id = Uuid::parse_str(file_stem)?;
let expected = management_config_path(instance_id)
.ok_or_else(|| anyhow::anyhow!("HarmonyOS config store is not initialized"))?;
if expected != path {
anyhow::bail!("nearby management config path is outside the managed store");
}
Ok(instance_id)
}
fn valid_session_key(session_key: &str) -> bool {
!session_key.is_empty()
&& session_key.len() <= MAX_SESSION_KEY_LENGTH
@@ -702,6 +755,90 @@ fn valid_rpc_packet_bytes(bytes: &[u8]) -> bool {
)
}
fn validate_ohos_envelope(envelope: &NearbyOhosEnvelope) -> anyhow::Result<()> {
if envelope.schema_version != OHOS_PRIVATE_SCHEMA_VERSION {
anyhow::bail!("unsupported HarmonyOS private packet schema");
}
Uuid::parse_str(&envelope.request_id)
.map_err(|_| anyhow::anyhow!("invalid HarmonyOS private request ID"))?;
if !matches!(
envelope.operation.as_str(),
OHOS_OPERATION_GET_SETTINGS | OHOS_OPERATION_UPDATE_SETTING
) {
anyhow::bail!("unsupported HarmonyOS private operation");
}
if !matches!(envelope.kind.as_str(), "request" | "response") {
anyhow::bail!("invalid HarmonyOS private packet kind");
}
if envelope.kind == "request" && envelope.ok.is_some() {
anyhow::bail!("HarmonyOS private request cannot carry a result state");
}
if envelope.kind == "response" && envelope.ok.is_none() {
anyhow::bail!("HarmonyOS private response must carry a result state");
}
if envelope
.payload_json
.as_ref()
.is_some_and(|payload| payload.len() > MAX_OHOS_PRIVATE_PAYLOAD_BYTES)
{
anyhow::bail!("HarmonyOS private payload is too large");
}
if envelope
.error
.as_ref()
.is_some_and(|error| error.len() > MAX_OHOS_PRIVATE_ERROR_LENGTH)
{
anyhow::bail!("HarmonyOS private error is too large");
}
Ok(())
}
fn decode_ohos_envelope(bytes: &[u8]) -> anyhow::Result<NearbyOhosEnvelope> {
if bytes.is_empty() || bytes.len() > MAX_PACKET_BYTES {
anyhow::bail!("HarmonyOS private packet size is invalid");
}
let packet = ZCPacket::new_from_buf(BytesMut::from(bytes), ZCPacketType::NIC);
let header = packet
.peer_manager_header()
.ok_or_else(|| anyhow::anyhow!("HarmonyOS private packet header is missing"))?;
if header.packet_type != OHOS_PRIVATE_PACKET_TYPE {
anyhow::bail!("packet is not a HarmonyOS private packet");
}
let envelope = serde_json::from_slice::<NearbyOhosEnvelope>(packet.payload())?;
validate_ohos_envelope(&envelope)?;
Ok(envelope)
}
pub(crate) fn nearby_management_packet_kind(packet: Uint8Array) -> i32 {
let bytes = packet.to_vec();
if valid_rpc_packet_bytes(&bytes) {
return 1;
}
if decode_ohos_envelope(&bytes).is_ok() {
return 2;
}
0
}
pub(crate) fn encode_nearby_ohos_packet(envelope_json: String) -> Option<Uint8Array> {
let envelope = serde_json::from_str::<NearbyOhosEnvelope>(&envelope_json).ok()?;
validate_ohos_envelope(&envelope).ok()?;
let payload = serde_json::to_vec(&envelope).ok()?;
if payload.len() > MAX_OHOS_PRIVATE_PAYLOAD_BYTES {
return None;
}
let mut packet = ZCPacket::new_with_payload(&payload);
packet.fill_peer_manager_hdr(RPC_PEER_ID, RPC_PEER_ID, OHOS_PRIVATE_PACKET_TYPE);
Some(Uint8Array::from(
packet.convert_type(ZCPacketType::NIC).into_bytes().to_vec(),
))
}
pub(crate) fn decode_nearby_ohos_packet(packet: Uint8Array) -> Option<String> {
let envelope = decode_ohos_envelope(&packet.to_vec()).ok()?;
serde_json::to_string(&envelope).ok()
}
fn session(session_key: &str) -> Option<Arc<NearbyManagementSession>> {
NEARBY_SESSIONS
.lock()
@@ -959,4 +1096,35 @@ mod tests {
assert!(!valid_session_key("contains space"));
assert!(valid_session_key("42:client"));
}
#[test]
fn ohos_private_settings_packet_round_trips_as_zcpacket() {
let request_id = Uuid::new_v4().to_string();
let raw = json!({
"schemaVersion": OHOS_PRIVATE_SCHEMA_VERSION,
"kind": "request",
"requestId": request_id,
"operation": OHOS_OPERATION_GET_SETTINGS,
})
.to_string();
let encoded = encode_nearby_ohos_packet(raw).expect("private packet must encode");
let bytes = encoded.to_vec();
assert_eq!(
nearby_management_packet_kind(Uint8Array::from(bytes.clone())),
2
);
let decoded =
decode_nearby_ohos_packet(Uint8Array::from(bytes)).expect("private packet must decode");
let envelope: NearbyOhosEnvelope = serde_json::from_str(&decoded).unwrap();
assert_eq!(envelope.request_id, request_id);
assert_eq!(envelope.operation, OHOS_OPERATION_GET_SETTINGS);
}
#[test]
fn one_shot_runtime_has_no_persistent_config_path() {
let control = runtime_management_config_control(Uuid::new_v4());
assert!(control.path.is_none());
assert!(control.is_read_only());
assert!(control.is_no_delete());
}
}