feat: client state (save/restore)

This commit is contained in:
Nova
2023-09-28 09:55:45 -04:00
parent 7095e1df84
commit cb7f3ef4ac
10 changed files with 266 additions and 235 deletions

View File

@@ -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"

View File

@@ -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<FxHashMap<String, String>, std::io::Error> {
.map(|(k, v)| (k.to_string(), v.to_string())),
))
}
pub fn startup_settings(env: &FxHashMap<String, String>) -> Option<StartupSettings> {
pub fn state(env: &FxHashMap<String, String>) -> Option<Arc<ClientState>> {
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<Scenegraph>,
pub root: OnceCell<Arc<Root>>,
pub base_resource_prefixes: Mutex<Vec<PathBuf>>,
pub startup_settings: Option<StartupSettings>,
pub state: Arc<ClientState>,
}
impl Client {
pub fn from_connection(connection: UnixStream) -> Result<Arc<Self>> {
@@ -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<Vec<String>> {
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<PathBuf> {
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<ClientState> {
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<Arc<Node>> {
self.scenegraph

94
src/core/client_state.rs Normal file
View File

@@ -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<FxHashMap<String, Arc<ClientState>>> = Default::default();
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ClientState {
pub cmdline: Option<Vec<String>>,
pub cwd: Option<PathBuf>,
pub data: Option<Vec<u8>>,
pub root: Mat4,
pub spatial_anchors: FxHashMap<String, Mat4>,
}
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<Mat4> {
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<Client>) -> 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<Vec<u8>>,
root: Option<String>,
spatial_anchors: FxHashMap<String, String>,
}

View File

@@ -1,4 +1,5 @@
pub mod client;
pub mod client_state;
pub mod delta;
pub mod destroy_queue;
pub mod eventloop;

View File

@@ -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;
}

View File

@@ -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
}

View File

@@ -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<B: Backend + ?Sized> PanelItem<B> {
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<B: Backend + ?Sized> PanelItem<B> {
.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<B: Backend + ?Sized> PanelItem<B> {
});
let generic_panel_item: Arc<dyn PanelItemTrait> = 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);

View File

@@ -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<u8>,
pub fds: Vec<OwnedFd>,
@@ -97,9 +97,6 @@ pub struct Node {
// Sound
pub sound: OnceCell<Arc<Sound>>,
// Startup
pub startup_settings: OnceCell<Mutex<StartupSettings>>,
}
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<u8>,
// ) -> Result<impl Future<Output = Result<Message>>> {
// 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<Message>,
) -> Result<impl Future<Output = Result<Message>>> {
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 {

View File

@@ -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<Root> = Registry::new();
pub struct Root {
node: Arc<Node>,
pub node: Arc<Node>,
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<Client>,
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<Output = Result<ClientStateInternal>> {
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<Client>,
_message: Message,
response: MethodResponseSender,
) {
response.wrap_sync(move || {
let mut env: FxHashMap<String, String> = 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())
});
}

View File

@@ -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<FxHashMap<String, StartupSettings>> = Default::default();
}
#[derive(Default, Clone)]
pub struct StartupSettings {
pub transform: Mat4,
pub acceptors: FxHashMap<&'static TypeInfo, Weak<ItemAcceptor>>,
}
impl StartupSettings {
pub fn add_to(node: &Arc<Node>) {
let _ = node
.startup_settings
.set(Mutex::new(StartupSettings::default()));
}
fn set_root_flex(node: &Node, calling_client: Arc<Client>, 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<Client>,
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<Client>,
_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::<Vec<_>>(),
)
.finish()
}
}
pub fn create_interface(client: &Arc<Client>) -> 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<Client>,
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<Client>,
_message: Message,
response: MethodResponseSender,
) {
response.wrap_sync(move || {
let mut env: FxHashMap<String, String> = 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())
});
}