diff --git a/Cargo.toml b/Cargo.toml index b0454bc..85abbbb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ once_cell = "1.17.1" parking_lot = "0.12.1" portable-atomic = { version = "1.2.0", features = ["float", "std"] } rustc-hash = "1.1.0" -tokio = { version = "1.27.0", features = ["rt-multi-thread", "signal"] } +tokio = { version = "1.27.0", features = ["rt-multi-thread", "signal", "time"] } send_wrapper = "0.6.0" prisma = "0.1.1" stardust-xr = "0.14.1" diff --git a/src/core/client.rs b/src/core/client.rs index a71d0b6..137d103 100644 --- a/src/core/client.rs +++ b/src/core/client.rs @@ -1,20 +1,20 @@ -use super::scenegraph::Scenegraph; +use super::{ + client_state::{ClientState, CLIENT_STATES}, + scenegraph::Scenegraph, +}; use crate::{ core::{registry::OwnedRegistry, task}, - nodes::{ - audio, data, drawable, fields, hmd, input, items, - root::Root, - spatial, - startup::{self, StartupSettings, STARTUP_SETTINGS}, - Node, - }, + nodes::{audio, data, drawable, fields, hmd, input, items, root::Root, spatial, Node}, }; use color_eyre::eyre::{eyre, Result}; use lazy_static::lazy_static; use once_cell::sync::OnceCell; use parking_lot::Mutex; use rustc_hash::FxHashMap; -use stardust_xr::messenger::{self, MessageSenderHandle}; +use stardust_xr::{ + messenger::{self, MessageSenderHandle}, + schemas::flex::serialize, +}; use std::{fs, iter::FromIterator, path::PathBuf, sync::Arc}; use tokio::{net::UnixStream, task::JoinHandle}; use tracing::info; @@ -34,7 +34,7 @@ lazy_static! { scenegraph: Default::default(), root: OnceCell::new(), base_resource_prefixes: Default::default(), - startup_settings: None, + state: Arc::new(ClientState::default()), }); } @@ -46,9 +46,9 @@ pub fn get_env(pid: i32) -> Result, std::io::Error> { .map(|(k, v)| (k.to_string(), v.to_string())), )) } -pub fn startup_settings(env: &FxHashMap) -> Option { +pub fn state(env: &FxHashMap) -> Option> { let token = env.get("STARDUST_STARTUP_TOKEN")?; - STARTUP_SETTINGS.lock().get(token).cloned() + CLIENT_STATES.lock().get(token).cloned() } pub struct Client { @@ -63,7 +63,7 @@ pub struct Client { pub scenegraph: Arc, pub root: OnceCell>, pub base_resource_prefixes: Mutex>, - pub startup_settings: Option, + pub state: Arc, } impl Client { pub fn from_connection(connection: UnixStream) -> Result> { @@ -80,7 +80,10 @@ impl Client { let (mut messenger_tx, mut messenger_rx) = messenger::create(connection); let scenegraph = Arc::new(Scenegraph::default()); - let startup_settings = env.as_ref().and_then(startup_settings); + let state = env + .as_ref() + .and_then(state) + .unwrap_or_else(|| Arc::new(ClientState::default())); let client = CLIENTS.add(Client { pid, @@ -95,7 +98,7 @@ impl Client { scenegraph: scenegraph.clone(), root: OnceCell::new(), base_resource_prefixes: Default::default(), - startup_settings, + state, }); let _ = client.scenegraph.client.set(Arc::downgrade(&client)); let _ = client.root.set(Root::create(&client)?); @@ -107,7 +110,13 @@ impl Client { data::create_interface(&client)?; items::create_interface(&client)?; input::create_interface(&client)?; - startup::create_interface(&client)?; + + client + .root + .get() + .unwrap() + .node + .send_remote_signal("restore_state", serialize(client.state.apply_to(&client))?)?; let pid_printable = pid .map(|pid| pid.to_string()) @@ -164,6 +173,27 @@ impl Client { Ok(client) } + pub fn get_cmdline(&self) -> Option> { + let pid = self.pid?; + let exe_proc_path = format!("/proc/{pid}/exe"); + let cmdline_proc_path = format!("/proc/{pid}/cmdline"); + let exe = std::fs::read_link(exe_proc_path).ok()?; + let cmdline = std::fs::read_to_string(cmdline_proc_path).ok()?; + let mut cmdline_split: Vec<_> = cmdline.split('\0').map(ToString::to_string).collect(); + cmdline_split.pop(); + *cmdline_split.get_mut(0).unwrap() = exe.to_str()?.to_string(); + Some(cmdline_split) + } + pub fn get_cwd(&self) -> Option { + let pid = self.pid?; + let cwd_proc_path = format!("/proc/{pid}/cwd"); + std::fs::read_link(cwd_proc_path).ok() + } + pub async fn save_state(&self) -> Option { + let internal = self.root.get()?.save_state().await.ok()?; + Some(dbg!(ClientState::from_deserialized(self, internal))) + } + #[inline] pub fn get_node(&self, name: &'static str, path: &str) -> Result> { self.scenegraph diff --git a/src/core/client_state.rs b/src/core/client_state.rs new file mode 100644 index 0000000..4d7a823 --- /dev/null +++ b/src/core/client_state.rs @@ -0,0 +1,94 @@ +use super::client::Client; +use crate::nodes::{spatial::Spatial, Node}; +use glam::Mat4; +use parking_lot::Mutex; +use rustc_hash::FxHashMap; +use serde::{Deserialize, Serialize}; +use std::{path::PathBuf, sync::Arc}; + +lazy_static::lazy_static! { + pub static ref CLIENT_STATES: Mutex>> = Default::default(); +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ClientState { + pub cmdline: Option>, + pub cwd: Option, + pub data: Option>, + pub root: Mat4, + pub spatial_anchors: FxHashMap, +} +impl ClientState { + pub fn from_deserialized(client: &Client, state: ClientStateInternal) -> Self { + ClientState { + cmdline: client.get_cmdline(), + cwd: client.get_cwd(), + data: state.data, + root: Self::spatial_transform(client, &state.root.unwrap_or_default()) + .unwrap_or_default(), + spatial_anchors: state + .spatial_anchors + .into_iter() + .filter_map(|(k, v)| Some((k, Self::spatial_transform(client, &v)?))) + .collect(), + } + } + fn spatial_transform(client: &Client, path: &str) -> Option { + let node = client.scenegraph.get_node(path)?; + let spatial = node.spatial.get()?; + Some(spatial.global_transform()) + } + + pub fn token(self) -> String { + let token = nanoid::nanoid!(); + CLIENT_STATES.lock().insert(token.clone(), Arc::new(self)); + token + } + pub fn to_file(self) { + let state_dir = directories::ProjectDirs::from("", "", "stardust") + .unwrap() + .state_dir() + .unwrap(); + // std::fs::File::create(state_dir.join("")) + } + pub fn apply_to(&self, client: &Arc) -> ClientStateInternal { + if let Some(root) = client.root.get() { + root.set_transform(self.root) + } + ClientStateInternal { + data: self.data.clone(), + root: Some("/".to_string()), + spatial_anchors: self + .spatial_anchors + .iter() + .map(|(k, v)| { + (k.clone(), { + let node = Node::create(client, "/spatial/anchor", k, true) + .add_to_scenegraph() + .unwrap(); + Spatial::add_to(&node, None, *v, false).unwrap(); + k.clone() + }) + }) + .collect(), + } + } +} +impl Default for ClientState { + fn default() -> Self { + Self { + cmdline: None, + cwd: None, + data: None, + root: Mat4::IDENTITY, + spatial_anchors: Default::default(), + } + } +} + +#[derive(Default, Serialize, Deserialize)] +pub struct ClientStateInternal { + data: Option>, + root: Option, + spatial_anchors: FxHashMap, +} diff --git a/src/core/mod.rs b/src/core/mod.rs index f8ccf38..e2910d1 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,4 +1,5 @@ pub mod client; +pub mod client_state; pub mod delta; pub mod destroy_queue; pub mod eventloop; diff --git a/src/main.rs b/src/main.rs index 8eb6fcb..0272db3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,8 @@ mod objects; #[cfg(feature = "wayland")] mod wayland; +use crate::core::client::CLIENTS; +use crate::core::client_state::ClientState; use crate::core::destroy_queue; use crate::nodes::items::camera; use crate::nodes::{audio, drawable, hmd, input}; @@ -27,6 +29,7 @@ use stereokit::{ TextureFormat, TextureType, }; use stereokit::{DisplayBlend, Sk}; +use tokio::task::LocalSet; use tokio::{runtime::Handle, sync::oneshot}; use tracing::metadata::LevelFilter; use tracing::{debug_span, error, info}; @@ -353,6 +356,7 @@ async fn event_loop( _ = stop_rx => (), }; } + save_clients().await; info!("Cleanly shut down event loop"); @@ -362,3 +366,17 @@ async fn event_loop( Ok(()) } + +async fn save_clients() { + let local_set = LocalSet::new(); + for client in CLIENTS.get_vec() { + local_set.spawn_local(async move { + tokio::select! { + biased; + s = client.save_state() => {s.map(ClientState::to_file);}, + _ = tokio::time::sleep(Duration::from_millis(100)) => (), + } + }); + } + local_set.await; +} diff --git a/src/nodes/items/mod.rs b/src/nodes/items/mod.rs index 51a0e66..75b9657 100644 --- a/src/nodes/items/mod.rs +++ b/src/nodes/items/mod.rs @@ -109,15 +109,15 @@ impl Item { } let _ = node.item.set(item.clone()); - if let Some(auto_acceptor) = node.get_client().and_then(|client| { - client - .startup_settings - .as_ref() - .and_then(|settings| settings.acceptors.get(type_info)) - .and_then(|acceptor| acceptor.upgrade()) - }) { - capture(&item, &auto_acceptor); - } + // if let Some(auto_acceptor) = node.get_client().and_then(|client| { + // client + // .state + // .as_ref() + // .and_then(|settings| settings.acceptors.get(type_info)) + // .and_then(|acceptor| acceptor.upgrade()) + // }) { + // capture(&item, &auto_acceptor); + // } item } diff --git a/src/nodes/items/panel.rs b/src/nodes/items/panel.rs index 4c0908c..0679cc7 100644 --- a/src/nodes/items/panel.rs +++ b/src/nodes/items/panel.rs @@ -1,11 +1,11 @@ use crate::{ core::{ - client::{get_env, startup_settings, Client, INTERNAL_CLIENT}, + client::{get_env, state, Client, INTERNAL_CLIENT}, registry::Registry, }, nodes::{ drawable::{model::ModelPart, Drawable}, - items::{self, Item, ItemType, TypeInfo}, + items::{Item, ItemType, TypeInfo}, spatial::Spatial, Message, Node, }, @@ -223,7 +223,7 @@ impl PanelItem { let startup_settings = pid .and_then(|pid| get_env(pid).ok()) - .and_then(|env| startup_settings(&env)); + .and_then(|env| state(&env)); let uid = nanoid!(); let node = Node::create(&INTERNAL_CLIENT, "/item/panel/item", &uid, true) @@ -231,7 +231,7 @@ impl PanelItem { .unwrap(); let spatial = Spatial::add_to(&node, None, Mat4::IDENTITY, false).unwrap(); if let Some(startup_settings) = &startup_settings { - spatial.set_local_transform(startup_settings.transform); + spatial.set_local_transform(startup_settings.root); } let panel_item = Arc::new(PanelItem { @@ -241,23 +241,13 @@ impl PanelItem { }); let generic_panel_item: Arc = panel_item.clone(); - let item = Item::add_to( + Item::add_to( &node, uid, &ITEM_TYPE_INFO_PANEL, ItemType::Panel(generic_panel_item), ); - if let Some(startup_settings) = &startup_settings { - if let Some(acceptor) = startup_settings - .acceptors - .get(&*ITEM_TYPE_INFO_PANEL) - .and_then(|acc| acc.upgrade()) - { - items::capture(&item, &acceptor); - } - } - node.add_local_signal("apply_surface_material", Self::apply_surface_material_flex); node.add_local_signal("close_toplevel", Self::close_toplevel_flex); node.add_local_signal("auto_size_toplevel", Self::auto_size_toplevel_flex); diff --git a/src/nodes/mod.rs b/src/nodes/mod.rs index ebe66bc..910b5ad 100644 --- a/src/nodes/mod.rs +++ b/src/nodes/mod.rs @@ -8,7 +8,6 @@ pub mod input; pub mod items; pub mod root; pub mod spatial; -pub mod startup; use color_eyre::eyre::{eyre, Result}; use nanoid::nanoid; @@ -20,6 +19,7 @@ use stardust_xr::messenger::MessageSenderHandle; use stardust_xr::scenegraph::ScenegraphError; use stardust_xr::schemas::flex::deserialize; use std::fmt::Debug; +use std::future::Future; use std::os::fd::OwnedFd; use std::sync::{Arc, Weak}; use std::vec::Vec; @@ -38,8 +38,8 @@ use self::input::{InputHandler, InputMethod}; use self::items::{Item, ItemAcceptor, ItemUI}; use self::spatial::zone::Zone; use self::spatial::Spatial; -use self::startup::StartupSettings; +#[derive(Default)] pub struct Message { pub data: Vec, pub fds: Vec, @@ -97,9 +97,6 @@ pub struct Node { // Sound pub sound: OnceCell>, - - // Startup - pub startup_settings: OnceCell>, } impl Node { @@ -143,7 +140,6 @@ impl Node { item_acceptor: OnceCell::new(), item_ui: OnceCell::new(), sound: OnceCell::new(), - startup_settings: OnceCell::new(), }; node.add_local_signal("set_enabled", Node::set_enabled_flex); node.add_local_signal("destroy", Node::destroy_flex); @@ -302,20 +298,30 @@ impl Node { Ok(()) } // #[instrument(level = "debug", skip_all)] - // pub fn execute_remote_method( - // &self, - // method: &str, - // data: Vec, - // ) -> Result>> { - // let message_sender_handle = self - // .message_sender_handle - // .as_ref() - // .ok_or(eyre!("Messenger does not exist for this node"))?; + pub fn execute_remote_method( + &self, + method: &str, + message: impl Into, + ) -> Result>> { + let message = message.into(); + let message_sender_handle = self + .message_sender_handle + .as_ref() + .ok_or(eyre!("Messenger does not exist for this node"))?; - // let future = message_sender_handle.method(self.path.as_str(), method, &data)?; + let future = + message_sender_handle.method(self.path.as_str(), method, &message.data, message.fds)?; - // Ok(async { future.await.map_err(|e| eyre!(e)) }) - // } + Ok(async { + match future.await { + Ok(m) => { + let (data, fds) = m.into_components(); + Ok(Message { data, fds }) + } + Err(e) => Err(eyre!(e)), + } + }) + } } impl Debug for Node { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/src/nodes/root.rs b/src/nodes/root.rs index 2477bb4..03412ae 100644 --- a/src/nodes/root.rs +++ b/src/nodes/root.rs @@ -1,19 +1,25 @@ use super::spatial::Spatial; use super::{Message, Node}; use crate::core::client::Client; +use crate::core::client_state::{ClientState, ClientStateInternal}; use crate::core::registry::Registry; +use crate::core::scenegraph::MethodResponseSender; +use crate::wayland::WAYLAND_DISPLAY; +use crate::STARDUST_INSTANCE; use color_eyre::eyre::Result; use glam::Mat4; +use rustc_hash::FxHashMap; use stardust_xr::schemas::flex::{deserialize, serialize}; use tracing::instrument; +use std::future::Future; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; static ROOT_REGISTRY: Registry = Registry::new(); pub struct Root { - node: Arc, + pub node: Arc, send_frame_event: AtomicBool, } impl Root { @@ -21,17 +27,13 @@ impl Root { let node = Node::create(client, "", "", false); node.add_local_signal("subscribe_frame", Root::subscribe_frame_flex); node.add_local_signal("set_base_prefixes", Root::set_base_prefixes_flex); - let node = node.add_to_scenegraph()?; - let _ = Spatial::add_to( - &node, - None, - client - .startup_settings - .as_ref() - .map(|settings| settings.transform) - .unwrap_or(Mat4::IDENTITY), - false, + node.add_local_method("state_token", Root::state_token_flex); + node.add_local_method( + "get_connection_environment", + get_connection_environment_flex, ); + let node = node.add_to_scenegraph()?; + let _ = Spatial::add_to(&node, None, client.state.root, false); Ok(ROOT_REGISTRY.add(Root { node, @@ -72,6 +74,31 @@ impl Root { *calling_client.base_resource_prefixes.lock() = deserialize(message.as_ref())?; Ok(()) } + + fn state_token_flex( + _node: &Node, + calling_client: Arc, + message: Message, + response: MethodResponseSender, + ) { + response.wrap_sync(|| { + let state: ClientStateInternal = deserialize(message.as_ref())?; + let token = ClientState::from_deserialized(&calling_client, state).token(); + Ok(serialize(token)?.into()) + }) + } + + pub fn set_transform(&self, transform: Mat4) { + let spatial = self.node.spatial.get().unwrap(); + spatial.set_spatial_parent(None).unwrap(); + spatial.set_local_transform(transform); + } + pub fn save_state(&self) -> impl Future> { + let future = self + .node + .execute_remote_method("save_state", Message::default()); + async move { Ok(deserialize(&future?.await?.data)?) } + } } impl Drop for Root { @@ -79,3 +106,33 @@ impl Drop for Root { ROOT_REGISTRY.remove(self); } } + +macro_rules! var_env_insert { + ($env:ident, $name:ident) => { + $env.insert(stringify!($name).to_string(), $name.get().unwrap().clone()); + }; +} +pub fn get_connection_environment_flex( + _node: &Node, + _calling_client: Arc, + _message: Message, + response: MethodResponseSender, +) { + response.wrap_sync(move || { + let mut env: FxHashMap = FxHashMap::default(); + var_env_insert!(env, STARDUST_INSTANCE); + #[cfg(feature = "wayland")] + { + var_env_insert!(env, WAYLAND_DISPLAY); + #[cfg(feature = "xwayland")] + var_env_insert!(env, DISPLAY); + env.insert("GDK_BACKEND".to_string(), "wayland".to_string()); + env.insert("QT_QPA_PLATFORM".to_string(), "wayland".to_string()); + env.insert("MOZ_ENABLE_WAYLAND".to_string(), "1".to_string()); + env.insert("CLUTTER_BACKEND".to_string(), "wayland".to_string()); + env.insert("SDL_VIDEODRIVER".to_string(), "wayland".to_string()); + } + + Ok(serialize(env)?.into()) + }); +} diff --git a/src/nodes/startup.rs b/src/nodes/startup.rs deleted file mode 100644 index 0275539..0000000 --- a/src/nodes/startup.rs +++ /dev/null @@ -1,165 +0,0 @@ -#[cfg(feature = "xwayland")] -use crate::wayland::xwayland::DISPLAY; -use crate::{ - core::{client::Client, scenegraph::MethodResponseSender}, - wayland::WAYLAND_DISPLAY, - STARDUST_INSTANCE, -}; - -use super::{ - items::{ItemAcceptor, TypeInfo}, - spatial::find_spatial, - Message, Node, -}; -use color_eyre::eyre::Result; -use glam::Mat4; -use parking_lot::Mutex; -use rustc_hash::FxHashMap; -use stardust_xr::schemas::flex::{deserialize, serialize}; -use std::{ - fmt::Debug, - sync::{Arc, Weak}, -}; - -lazy_static::lazy_static! { - pub static ref STARTUP_SETTINGS: Mutex> = Default::default(); -} - -#[derive(Default, Clone)] -pub struct StartupSettings { - pub transform: Mat4, - pub acceptors: FxHashMap<&'static TypeInfo, Weak>, -} -impl StartupSettings { - pub fn add_to(node: &Arc) { - let _ = node - .startup_settings - .set(Mutex::new(StartupSettings::default())); - } - - fn set_root_flex(node: &Node, calling_client: Arc, message: Message) -> Result<()> { - let spatial = find_spatial( - &calling_client, - "Root spatial", - deserialize(message.as_ref())?, - )?; - node.startup_settings.get().unwrap().lock().transform = spatial.global_transform(); - - Ok(()) - } - - fn add_automatic_acceptor_flex( - node: &Node, - calling_client: Arc, - message: Message, - ) -> Result<()> { - let acceptor_node = - calling_client.get_node("Item acceptor", deserialize(message.as_ref())?)?; - let acceptor = - acceptor_node.get_aspect("Item acceptor", "item acceptor", |n| &n.item_acceptor)?; - let mut startup_settings = node.startup_settings.get().unwrap().lock(); - startup_settings - .acceptors - .insert(acceptor.type_info, Arc::downgrade(acceptor)); - - Ok(()) - } - - fn generate_startup_token_flex( - node: &Node, - _calling_client: Arc, - _message: Message, - response: MethodResponseSender, - ) { - response.wrap_sync(move || { - let id = nanoid::nanoid!(); - let data = serialize(&id)?; - STARTUP_SETTINGS - .lock() - .insert(id, node.startup_settings.get().unwrap().lock().clone()); - Ok(data.into()) - }); - } -} -impl Debug for StartupSettings { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("StartupSettings") - .field("transform", &self.transform) - .field( - "acceptors", - &self - .acceptors - .iter() - .map(|(k, _)| k.type_name) - .collect::>(), - ) - .finish() - } -} - -pub fn create_interface(client: &Arc) -> Result<()> { - let node = Node::create(client, "", "startup", false); - node.add_local_signal("create_startup_settings", create_startup_settings_flex); - node.add_local_method( - "get_connection_environment", - get_connection_environment_flex, - ); - node.add_to_scenegraph().map(|_| ()) -} - -pub fn create_startup_settings_flex( - _node: &Node, - calling_client: Arc, - message: Message, -) -> Result<()> { - let node = Node::create( - &calling_client, - "/startup/settings", - deserialize(message.as_ref())?, - true, - ) - .add_to_scenegraph()?; - StartupSettings::add_to(&node); - - node.add_local_signal("set_root", StartupSettings::set_root_flex); - node.add_local_signal( - "add_automatic_acceptor", - StartupSettings::add_automatic_acceptor_flex, - ); - node.add_local_method( - "generate_startup_token", - StartupSettings::generate_startup_token_flex, - ); - - Ok(()) -} - -macro_rules! var_env_insert { - ($env:ident, $name:ident) => { - $env.insert(stringify!($name).to_string(), $name.get().unwrap().clone()); - }; -} -pub fn get_connection_environment_flex( - _node: &Node, - _calling_client: Arc, - _message: Message, - response: MethodResponseSender, -) { - response.wrap_sync(move || { - let mut env: FxHashMap = FxHashMap::default(); - var_env_insert!(env, STARDUST_INSTANCE); - #[cfg(feature = "wayland")] - { - var_env_insert!(env, WAYLAND_DISPLAY); - #[cfg(feature = "xwayland")] - var_env_insert!(env, DISPLAY); - env.insert("GDK_BACKEND".to_string(), "wayland".to_string()); - env.insert("QT_QPA_PLATFORM".to_string(), "wayland".to_string()); - env.insert("MOZ_ENABLE_WAYLAND".to_string(), "1".to_string()); - env.insert("CLUTTER_BACKEND".to_string(), "wayland".to_string()); - env.insert("SDL_VIDEODRIVER".to_string(), "wayland".to_string()); - } - - Ok(serialize(env)?.into()) - }); -}