Implement TestClient inventory and appearance commands (#92)
Some checks failed
Native code generation / deterministic (push) Successful in 18m25s
Imaging and meshing gate / native (push) Failing after 4m23s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 12m37s
Skia feature / linux (push) Has been cancelled

This commit is contained in:
2026-08-11 11:14:50 +00:00
parent 9675440210
commit ea10b7672c
7 changed files with 3379 additions and 15 deletions

View File

@@ -1,5 +1,7 @@
//! Native `TestClient` shell, registry, and first-wave command groups.
mod inventory;
use crate::commands::TEST_CLIENT_COMMANDS;
use clap::Parser;
use libremetaverse::packets::PacketType;
@@ -41,6 +43,31 @@ const MAX_LOADED_COMMANDS: usize = 128;
const MAX_ALIAS_DEPTH: usize = 8;
pub const IMPLEMENTED_TEST_CLIENT_COMMANDS: &[&str] = &[
"AppearanceCommand",
"AttachmentsCommand",
"AvatarInfoCommand",
"CloneCommand",
"WearCommand",
"BackupCommand",
"BalanceCommand",
"ChangeDirectoryCommand",
"CreateNotecardCommand",
"DeleteFolderCommand",
"DownloadCommand",
"DumpOutfitCommand",
"EmptyLostAndFound",
"EmptyTrashCommand",
"GiveAllCommand",
"GiveItemCommand",
"InventoryCommand",
"ListContentsCommand",
"ObjectInventoryCommand",
"ScriptCommand",
"TaskRunningCommand",
"UploadImageCommand",
"UploadScriptCommand",
"ViewNotecardCommand",
"XferCommand",
"EchoMasterCommand",
"IMCommand",
"IMGroupCommand",
@@ -79,8 +106,9 @@ pub fn pending_test_client_commands() -> Vec<&'static str> {
version,
about = "Run the native LibreMetaverse multi-client command shell",
long_about = None,
after_help = "Owned commands: @, debug, echomaster, help, im, imgroup, load, login, logpacket, logout, md5, quit, say, setmaster, setmasterkey, shout, showeffects, sleep, waitforlogin, whisper"
after_help = "Owned commands include @, debug, echomaster, help, im, imgroup, load, login, logpacket, logout, md5, quit, say, setmaster, setmasterkey, shout, showeffects, sleep, waitforlogin, whisper, plus the inventory/assets and appearance groups. Run `help` or `help COMMAND` inside the shell for the complete command registry."
)]
#[allow(clippy::struct_excessive_bools)]
struct Cli {
/// First name for one initial account.
#[arg(long)]
@@ -124,6 +152,14 @@ struct Cli {
/// Maximum time allowed for each live login.
#[arg(long, default_value_t = 30, value_name = "SECONDS", value_parser = clap::value_parser!(u64).range(1..=300))]
login_timeout_seconds: u64,
/// Permit commands that mutate live inventory or appearance state.
#[arg(long)]
allow_live_mutations: bool,
/// Permit commands that spend or transfer L$; each command also requires confirmation.
#[arg(long)]
allow_spending: bool,
}
#[derive(Debug)]
@@ -207,8 +243,9 @@ enum ClientEvent {
}
type BackendFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
type InventoryFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
trait ClientBackend: Send + Sync {
trait ClientBackend: Send + Sync + inventory::Backend {
fn id(&self) -> UUID;
fn name(&self) -> String;
fn connected(&self) -> bool;
@@ -242,6 +279,7 @@ struct LiveBackend {
id: UUID,
name: String,
subscriptions: Mutex<Vec<Subscription>>,
inventory_state: Mutex<inventory::State>,
closed: AtomicBool,
}
@@ -251,6 +289,7 @@ impl LiveBackend {
timeout: Duration,
events: mpsc::Sender<ClientEvent>,
dropped: Arc<AtomicUsize>,
account_policy: inventory::MutationPolicy,
cancellation: CancellationToken,
) -> Result<Arc<Self>, ProgramError> {
let mut client = GridClient::new()
@@ -299,6 +338,7 @@ impl LiveBackend {
id,
name,
subscriptions: Mutex::new(Vec::new()),
inventory_state: Mutex::new(inventory::State::new(account_policy)),
closed: AtomicBool::new(false),
});
backend.install(&events, &dropped);
@@ -309,6 +349,7 @@ impl LiveBackend {
action(lock(&self.client).self_())
}
#[allow(clippy::too_many_lines)]
fn install(self: &Arc<Self>, events: &mpsc::Sender<ClientEvent>, dropped: &Arc<AtomicUsize>) {
let mut subscriptions = Vec::new();
let sender = events.clone();
@@ -390,6 +431,18 @@ impl LiveBackend {
}),
false,
));
let appearance_backend = Arc::downgrade(self);
subscriptions.push(self.network.subscribe_packet(
PacketType::AvatarAppearance,
Arc::new(move |event: PacketReceivedEventArgs| {
if let (Some(backend), Some(bytes)) =
(appearance_backend.upgrade(), event.raw_data())
{
inventory::capture_appearance(&backend, &bytes);
}
}),
false,
));
let sender = events.clone();
let dropped_disconnect = Arc::clone(dropped);
subscriptions.push(self.network.subscribe_disconnected(Arc::new(
@@ -570,6 +623,7 @@ struct FakeBackend {
calls: Mutex<Vec<String>>,
people: Arc<Mutex<HashMap<String, UUID>>>,
group_members: Arc<Mutex<HashSet<UUID>>>,
inventory_state: Mutex<inventory::State>,
}
impl FakeBackend {
@@ -578,6 +632,7 @@ impl FakeBackend {
name: String,
people: Arc<Mutex<HashMap<String, UUID>>>,
group_members: Arc<Mutex<HashSet<UUID>>>,
policy: inventory::MutationPolicy,
) -> Self {
Self {
id,
@@ -586,6 +641,7 @@ impl FakeBackend {
calls: Mutex::new(Vec::new()),
people,
group_members,
inventory_state: Mutex::new(inventory::State::new(policy)),
}
}
@@ -704,7 +760,9 @@ impl ProgramOutput {
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
enum CommandCategory {
Appearance,
Communication,
Inventory,
TestClient,
Other,
}
@@ -717,6 +775,7 @@ enum CommandHandler {
Help,
Im,
ImGroup,
Inventory(inventory::Command),
Load,
Login,
LogPacket,
@@ -876,6 +935,7 @@ fn built_in_commands() -> HashMap<String, CommandDefinition> {
framework_commands()
.into_iter()
.chain(communication_commands())
.chain(inventory::commands())
.map(|(name, description, category, handler)| {
(
name.into(),
@@ -1020,6 +1080,7 @@ struct ClientManager {
default_master_name: String,
default_master_key: UUID,
default_group_commands: bool,
default_mutation_policy: inventory::MutationPolicy,
cancellation: CancellationTokenSource,
output: Arc<ProgramOutput>,
events_tx: mpsc::Sender<ClientEvent>,
@@ -1033,6 +1094,7 @@ struct ClientManager {
}
impl ClientManager {
#[allow(clippy::too_many_arguments)]
fn new(
mode: RunMode,
login_uri: Option<String>,
@@ -1040,6 +1102,7 @@ impl ClientManager {
master_name: String,
master_key: UUID,
group_commands: bool,
mutation_policy: inventory::MutationPolicy,
output: Arc<ProgramOutput>,
) -> Self {
let (events_tx, events_rx) = mpsc::channel(EVENT_QUEUE_CAPACITY);
@@ -1055,6 +1118,7 @@ impl ClientManager {
default_master_name: master_name,
default_master_key: master_key,
default_group_commands: group_commands,
default_mutation_policy: mutation_policy,
cancellation: CancellationTokenSource::new(),
output,
events_tx,
@@ -1114,6 +1178,7 @@ impl ClientManager {
name,
Arc::clone(&self.fake_people),
Arc::clone(&self.fake_group_members),
self.default_mutation_policy,
));
self.add_client(ManagedClient {
backend,
@@ -1140,6 +1205,7 @@ impl ClientManager {
self.login_timeout,
self.events_tx.clone(),
Arc::clone(&self.dropped_events),
self.default_mutation_policy,
self.cancellation.token(),
)
.await
@@ -1153,6 +1219,7 @@ impl ClientManager {
display_name,
Arc::clone(&self.fake_people),
Arc::clone(&self.fake_group_members),
self.default_mutation_policy,
))),
Err(_) => Err(ProgramError::Client(
"could not make fake client UUID".into(),
@@ -1297,6 +1364,11 @@ impl ClientManager {
self.output.line(result);
return;
}
CommandHandler::Inventory(inventory::Command::Script) => {
let result = self.execute_script(args).await;
self.output.line(result);
return;
}
CommandHandler::At | CommandHandler::LoadedAlias(_) => return,
_ => {}
}
@@ -1411,6 +1483,25 @@ impl ClientManager {
format!("Logging is set to {level}")
}
async fn execute_script(&mut self, args: &[String]) -> String {
if args.len() != 1 {
return "Usage: script [filename]".into();
}
let lines = match read_bounded_lines(Path::new(&args[0])) {
Ok(lines) => lines,
Err(error) => return error.to_string(),
};
let count = lines.len();
for line in lines {
if !self.running {
break;
}
Box::pin(self.dispatch_line(line.trim(), UUID::zero())).await;
drain_events(self).await;
}
format!("Finished executing {count} commands")
}
fn load_commands(&mut self, args: &[String]) -> String {
if args.len() != 1 {
return "Usage: load [native-command-manifest]".into();
@@ -1605,7 +1696,7 @@ async fn execute_client_command(
client: &mut ManagedClient,
handler: &CommandHandler,
args: &[String],
_from: UUID,
from: UUID,
cancellation: CancellationToken,
) -> String {
match handler {
@@ -1616,6 +1707,9 @@ async fn execute_client_command(
execute_communication_command(client, handler, args, cancellation).await
}
CommandHandler::LogPacket => configure_packet_log(client, args),
CommandHandler::Inventory(command) => {
inventory::execute(client.backend.as_ref(), *command, args, from, cancellation).await
}
CommandHandler::Md5 => {
if args.len() != 1 {
return "Usage: md5 [password]".into();
@@ -1861,6 +1955,8 @@ pub fn main_entry() -> ExitCode {
}
async fn run(cli: Cli) -> Result<(), ProgramError> {
let mutation_policy =
inventory::MutationPolicy::new(cli.allow_live_mutations, cli.allow_spending);
let master_key = match cli.masterkey.as_deref() {
Some(value) => UUID::new_with_string(value.into())
.map_err(|_| ProgramError::Usage("--masterkey must be a UUID"))?,
@@ -1881,6 +1977,7 @@ async fn run(cli: Cli) -> Result<(), ProgramError> {
cli.master.unwrap_or_default(),
master_key,
cli.groupcommands,
mutation_policy,
Arc::clone(&output),
);
run_fake_script(&mut manager, path).await?;
@@ -1913,6 +2010,7 @@ async fn run(cli: Cli) -> Result<(), ProgramError> {
cli.master.unwrap_or_default(),
master_key,
cli.groupcommands,
mutation_policy,
Arc::clone(&output),
);
for account in accounts {
@@ -2085,7 +2183,7 @@ async fn run_interactive(manager: &mut ClientManager) -> Result<(), ProgramError
async fn drain_events(manager: &mut ClientManager) {
while let Ok(event) = manager.events_rx.try_recv() {
if let Some((line, from)) = manager.process_event(event) {
manager.dispatch_line(&line, from).await;
Box::pin(manager.dispatch_line(&line, from)).await;
}
}
}
@@ -2183,6 +2281,10 @@ async fn run_fake_event_directive(
fields: &[&str],
line: usize,
) -> Result<(), ProgramError> {
if let Some(result) = inventory::apply_fake_directive(manager, fields) {
result.map_err(|reason| ProgramError::InvalidInput { line, reason })?;
return Ok(());
}
match fields {
["chat", client, source, name, message] => {
let event = ClientEvent::Chat {
@@ -2254,7 +2356,7 @@ async fn run_fake_event_directive(
_ => {
return invalid(
line,
"unknown fake directive; expected client, person, group-member, chat, im, packet, effect, disconnect, cancel, or shutdown",
"unknown fake directive; see --help and programs/README.md for supported records",
);
}
}

File diff suppressed because it is too large Load Diff