use log::{debug, error, warn};
use parking_lot::ReentrantMutex;
use std::ffi::{CStr, OsString};
use std::fmt::Debug;
use std::path::PathBuf;
use std::process::Child;
use std::time::Duration;
use std::{ffi::CString, path::Path, sync::Arc};
pub use crate::{
asset::AssetLibrary,
errors::*,
ffi::{
CompositorOptions, CookOptions, ImageFileFormat, SessionInfo, SessionSyncInfo,
ThriftServerOptions, TimelineOptions, Viewport, enums::*,
},
node::{HoudiniNode, ManagerNode, ManagerType, NodeHandle, NodeType, Transform},
parameter::Parameter,
stringhandle::StringArray,
};
pub type SessionState = State;
use crate::ffi::ImageInfo;
use crate::stringhandle::StringHandle;
use crate::{ffi::raw, utils};
pub struct NodeBuilder<'s> {
session: &'s Session,
name: String,
label: Option<String>,
parent: Option<NodeHandle>,
cook: bool,
}
impl NodeBuilder<'_> {
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn with_parent<H: AsRef<NodeHandle>>(mut self, parent: H) -> Self {
self.parent.replace(*parent.as_ref());
self
}
pub fn cook(mut self, cook: bool) -> Self {
self.cook = cook;
self
}
pub fn create(self) -> Result<HoudiniNode> {
let NodeBuilder {
session,
name,
label,
parent,
cook,
} = self;
session.create_node_with(&name, parent, label.as_deref(), cook)
}
}
impl PartialEq for raw::HAPI_Session {
fn eq(&self, other: &Self) -> bool {
self.type_ == other.type_ && self.id == other.id
}
}
pub trait EnvVariable {
type Type: ?Sized + ToOwned + Debug;
fn get_value(session: &Session, key: impl AsRef<str>)
-> Result<<Self::Type as ToOwned>::Owned>;
fn set_value(session: &Session, key: impl AsRef<str>, val: &Self::Type) -> Result<()>;
}
impl EnvVariable for str {
type Type = str;
fn get_value(session: &Session, key: impl AsRef<str>) -> Result<String> {
let key = CString::new(key.as_ref())?;
let handle = crate::ffi::get_server_env_str(session, &key)?;
crate::stringhandle::get_string(handle, session)
}
fn set_value(session: &Session, key: impl AsRef<str>, val: &Self::Type) -> Result<()> {
let key = CString::new(key.as_ref())?;
let val = CString::new(val)?;
crate::ffi::set_server_env_str(session, &key, &val)
}
}
impl EnvVariable for Path {
type Type = Self;
fn get_value(session: &Session, key: impl AsRef<str>) -> Result<PathBuf> {
let key = CString::new(key.as_ref())?;
crate::stringhandle::get_string(crate::ffi::get_server_env_str(session, &key)?, session)
.map(PathBuf::from)
}
fn set_value(session: &Session, key: impl AsRef<str>, val: &Self::Type) -> Result<()> {
let key = CString::new(key.as_ref())?;
let val = utils::path_to_cstring(val)?;
crate::ffi::set_server_env_str(session, &key, &val)
}
}
impl EnvVariable for i32 {
type Type = Self;
fn get_value(session: &Session, key: impl AsRef<str>) -> Result<Self::Type> {
let key = CString::new(key.as_ref())?;
crate::ffi::get_server_env_int(session, &key)
}
fn set_value(session: &Session, key: impl AsRef<str>, val: &Self::Type) -> Result<()> {
let key = CString::new(key.as_ref())?;
crate::ffi::set_server_env_int(session, &key, *val)
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum CookResult {
Succeeded,
CookErrors(String),
FatalErrors(String),
}
impl CookResult {
pub fn message(&self) -> Option<&str> {
match self {
Self::Succeeded => None,
Self::CookErrors(msg) => Some(msg.as_str()),
Self::FatalErrors(msg) => Some(msg.as_str()),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ConnectionType {
ThriftPipe(OsString),
ThriftSocket(std::net::SocketAddrV4),
SharedMemory(String),
InProcess,
Custom,
}
#[derive(Debug)]
pub(crate) struct SessionInner {
pub(crate) handle: raw::HAPI_Session,
pub(crate) options: SessionOptions,
pub(crate) connection: ConnectionType,
pub(crate) pid: Option<u32>,
pub(crate) lock: ReentrantMutex<()>,
}
#[derive(Debug, Clone)]
pub struct Session {
pub(crate) inner: Arc<SessionInner>,
}
impl PartialEq for Session {
fn eq(&self, other: &Self) -> bool {
self.inner.handle.id == other.inner.handle.id
&& self.inner.handle.type_ == other.inner.handle.type_
}
}
impl Session {
fn new(
handle: raw::HAPI_Session,
connection: ConnectionType,
options: SessionOptions,
pid: Option<u32>,
) -> Session {
Session {
inner: Arc::new(SessionInner {
handle,
options,
connection,
lock: ReentrantMutex::new(()),
pid,
}),
}
}
pub fn session_type(&self) -> SessionType {
self.inner.handle.type_
}
pub fn connection_type(&self) -> &ConnectionType {
&self.inner.connection
}
pub fn server_pid(&self) -> Option<u32> {
self.inner.pid
}
#[inline(always)]
pub(crate) fn ptr(&self) -> *const raw::HAPI_Session {
&(self.inner.handle) as *const _
}
pub fn set_server_var<T: EnvVariable + ?Sized>(
&self,
key: &str,
value: &T::Type,
) -> Result<()> {
debug_assert!(self.is_valid());
debug!("Setting server variable {key}={value:?}");
T::set_value(self, key, value)
}
pub fn get_server_var<T: EnvVariable + ?Sized>(
&self,
key: &str,
) -> Result<<T::Type as ToOwned>::Owned> {
debug_assert!(self.is_valid());
T::get_value(self, key)
}
pub fn get_server_variables(&self) -> Result<StringArray> {
debug_assert!(self.is_valid());
let count = crate::ffi::get_server_env_var_count(self)?;
let handles = crate::ffi::get_server_env_var_list(self, count)?;
crate::stringhandle::get_string_array(&handles, self).context("Calling get_string_array")
}
pub fn get_string(&self, handle: StringHandle) -> Result<String> {
crate::stringhandle::get_string(handle, self)
}
pub fn get_string_batch(&self, handles: &[StringHandle]) -> Result<StringArray> {
crate::stringhandle::get_string_array(handles, self)
}
pub fn initialize(&self) -> Result<()> {
debug!("Initializing session");
debug_assert!(self.is_valid());
let res = crate::ffi::initialize_session(self, &self.inner.options);
match res {
Ok(_) => Ok(()),
Err(HapiError {
kind: Kind::Hapi(HapiResult::AlreadyInitialized),
..
}) => {
warn!("Session already initialized, skipping");
Ok(())
}
Err(e) => Err(e),
}
}
pub fn cleanup(&self) -> Result<()> {
debug!("Cleaning session");
debug_assert!(self.is_valid());
crate::ffi::cleanup_session(self)
}
pub fn is_initialized(&self) -> bool {
debug_assert!(self.is_valid());
crate::ffi::is_session_initialized(self)
}
pub fn create_input_node(
&self,
name: &str,
parent: Option<NodeHandle>,
) -> Result<crate::geometry::Geometry> {
debug!("Creating input node: {}", name);
debug_assert!(self.is_valid());
let name = CString::new(name)?;
let id = crate::ffi::create_input_node(self, &name, parent)?;
let node = HoudiniNode::new(self.clone(), NodeHandle(id), None)?;
let info = crate::geometry::GeoInfo::from_node(&node)?;
Ok(crate::geometry::Geometry { node, info })
}
pub fn create_input_curve_node(
&self,
name: &str,
parent: Option<NodeHandle>,
) -> Result<crate::geometry::Geometry> {
debug!("Creating input curve node: {}", name);
debug_assert!(self.is_valid());
let name = CString::new(name)?;
let id = crate::ffi::create_input_curve_node(self, &name, parent)?;
let node = HoudiniNode::new(self.clone(), NodeHandle(id), None)?;
let info = crate::geometry::GeoInfo::from_node(&node)?;
Ok(crate::geometry::Geometry { node, info })
}
pub fn create_node(&self, name: impl AsRef<str>) -> Result<HoudiniNode> {
self.create_node_with(name.as_ref(), None, None, false)
}
pub fn node_builder(&self, node_name: impl Into<String>) -> NodeBuilder<'_> {
NodeBuilder {
session: self,
name: node_name.into(),
label: None,
parent: None,
cook: false,
}
}
pub(crate) fn create_node_with<P>(
&self,
name: &str,
parent: P,
label: Option<&str>,
cook: bool,
) -> Result<HoudiniNode>
where
P: Into<Option<NodeHandle>>,
{
let parent = parent.into();
debug!("Creating node instance: {}", name);
debug_assert!(self.is_valid());
debug_assert!(
parent.is_some() || name.contains('/'),
"Node name must be fully qualified if parent is not specified"
);
debug_assert!(
!(parent.is_some() && name.contains('/')),
"Cannot use fully qualified node name with parent"
);
let name = CString::new(name)?;
let label = label.map(CString::new).transpose()?;
let node_id = crate::ffi::create_node(&name, label.as_deref(), self, parent, cook)?;
if self.inner.options.threaded {
use std::borrow::Cow;
if let CookResult::FatalErrors(message) = self.cook()? {
return Err(HapiError::new(
Kind::Hapi(HapiResult::Failure),
Some(Cow::Owned(format!(
"Could not create node {:?}",
name.to_string_lossy()
))),
Some(Cow::Owned(message)),
));
}
}
HoudiniNode::new(self.clone(), NodeHandle(node_id), None)
}
pub fn delete_node<H: Into<NodeHandle>>(&self, node: H) -> Result<()> {
crate::ffi::delete_node(node.into(), self)
}
pub fn get_node_from_path(
&self,
path: impl AsRef<str>,
parent: impl Into<Option<NodeHandle>>,
) -> Result<Option<HoudiniNode>> {
debug_assert!(self.is_valid());
debug!("Searching node at path: {}", path.as_ref());
let path = CString::new(path.as_ref())?;
match crate::ffi::get_node_from_path(self, parent.into(), &path) {
Ok(handle) => Ok(NodeHandle(handle).to_node(self).ok()),
Err(HapiError {
kind: Kind::Hapi(HapiResult::InvalidArgument),
..
}) => Ok(None),
Err(e) => Err(e),
}
}
pub fn find_parameter_from_path(
&self,
path: impl AsRef<str>,
start: impl Into<Option<NodeHandle>>,
) -> Result<Option<Parameter>> {
debug_assert!(self.is_valid());
debug!("Searching parameter at path: {}", path.as_ref());
let Some((path, parm)) = path.as_ref().rsplit_once('/') else {
return Ok(None);
};
let Some(node) = self.get_node_from_path(path, start)? else {
debug!("Node {} not found", path);
return Ok(None);
};
Ok(node.parameter(parm).ok())
}
pub fn get_manager_node(&self, manager: ManagerType) -> Result<ManagerNode> {
debug_assert!(self.is_valid());
debug!("Getting Manager node of type: {:?}", manager);
let node_type = NodeType::from(manager);
let handle = crate::ffi::get_manager_node(self, node_type)?;
Ok(ManagerNode {
session: self.clone(),
handle: NodeHandle(handle),
node_type: manager,
})
}
pub fn get_composed_object_transform(
&self,
parent: impl AsRef<NodeHandle>,
rst_order: RSTOrder,
) -> Result<Vec<Transform>> {
debug_assert!(self.is_valid());
crate::ffi::get_composed_object_transforms(self, *parent.as_ref(), rst_order)
.map(|transforms| transforms.into_iter().map(Transform).collect())
}
pub fn save_hip(&self, path: impl AsRef<Path>, lock_nodes: bool) -> Result<()> {
debug!("Saving hip file: {:?}", path.as_ref());
debug_assert!(self.is_valid());
let path = utils::path_to_cstring(path)?;
crate::ffi::save_hip(self, &path, lock_nodes)
}
pub fn load_hip(&self, path: impl AsRef<Path>, cook: bool) -> Result<()> {
debug!("Loading hip file: {:?}", path.as_ref());
debug_assert!(self.is_valid());
let path = utils::path_to_cstring(path)?;
crate::ffi::load_hip(self, &path, cook)
}
pub fn merge_hip(&self, name: &str, cook: bool) -> Result<i32> {
debug!("Merging hip file: {}", name);
debug_assert!(self.is_valid());
let name = CString::new(name)?;
crate::ffi::merge_hip(self, &name, cook)
}
pub fn get_hip_file_nodes(&self, hip_id: i32) -> Result<Vec<NodeHandle>> {
crate::ffi::get_hipfile_node_ids(self, hip_id)
.map(|handles| handles.into_iter().map(NodeHandle).collect())
}
pub fn load_asset_file(&self, file: impl AsRef<Path>) -> Result<AssetLibrary> {
debug_assert!(self.is_valid());
AssetLibrary::from_file(self.clone(), file)
}
pub fn get_loaded_asset_libraries(&self) -> Result<Vec<AssetLibrary>> {
debug_assert!(self.is_valid());
crate::ffi::get_asset_library_ids(self)?
.into_iter()
.map(|library_id| {
crate::ffi::get_asset_library_file_path(self, library_id).map(|lib_file| {
AssetLibrary {
lib_id: library_id,
session: self.clone(),
file: Some(PathBuf::from(lib_file)),
}
})
})
.collect::<Result<Vec<_>>>()
}
pub fn interrupt(&self) -> Result<()> {
debug_assert!(self.is_valid());
crate::ffi::interrupt(self)
}
#[doc(hidden)]
#[allow(unused)]
pub(crate) fn get_call_result_status(&self) -> Result<HapiResult> {
debug_assert!(self.is_valid());
let status = crate::ffi::get_status_code(self, StatusType::CallResult)?;
Ok(unsafe { std::mem::transmute::<i32, HapiResult>(status) })
}
pub fn get_cook_state_status(&self) -> Result<SessionState> {
debug_assert!(self.is_valid());
crate::ffi::get_cook_state_status(self)
}
pub fn is_cooking(&self) -> Result<bool> {
debug_assert!(self.is_valid());
Ok(matches!(
self.get_cook_state_status()?,
SessionState::Cooking
))
}
#[inline(always)]
pub fn is_valid(&self) -> bool {
crate::ffi::is_session_valid(self)
}
pub fn get_status_string(
&self,
status: StatusType,
verbosity: StatusVerbosity,
) -> Result<String> {
debug_assert!(self.is_valid());
crate::ffi::get_status_string(self, status, verbosity)
}
pub fn get_cook_result_string(&self, verbosity: StatusVerbosity) -> Result<String> {
debug_assert!(self.is_valid());
self.get_status_string(StatusType::CookResult, verbosity)
}
pub fn cooking_total_count(&self) -> Result<i32> {
debug_assert!(self.is_valid());
crate::ffi::get_cooking_total_count(self)
}
pub fn cooking_current_count(&self) -> Result<i32> {
debug_assert!(self.is_valid());
crate::ffi::get_cooking_current_count(self)
}
pub fn cook(&self) -> Result<CookResult> {
debug_assert!(self.is_valid());
debug!("Cooking session..");
if self.inner.options.threaded {
loop {
match self.get_cook_state_status()? {
SessionState::Ready => break Ok(CookResult::Succeeded),
SessionState::ReadyWithFatalErrors => {
self.interrupt()?;
let err = self.get_cook_result_string(StatusVerbosity::Errors)?;
break Ok(CookResult::FatalErrors(err));
}
SessionState::ReadyWithCookErrors => {
let err = self.get_cook_result_string(StatusVerbosity::Errors)?;
break Ok(CookResult::CookErrors(err));
}
_ => {}
}
}
} else {
Ok(CookResult::Succeeded)
}
}
pub fn get_connection_error(&self, clear: bool) -> Result<String> {
debug_assert!(self.is_valid());
crate::ffi::get_connection_error(clear)
}
pub fn get_time(&self) -> Result<f64> {
debug_assert!(self.is_valid());
crate::ffi::get_time(self)
}
pub fn set_time(&self, time: f64) -> Result<()> {
debug_assert!(self.is_valid());
crate::ffi::set_time(self, time)
}
pub fn lock(&self) -> parking_lot::ReentrantMutexGuard<'_, ()> {
self.inner.lock.lock()
}
pub fn set_timeline_options(&self, options: TimelineOptions) -> Result<()> {
debug_assert!(self.is_valid());
crate::ffi::set_timeline_options(self, &options.0)
}
pub fn get_timeline_options(&self) -> Result<TimelineOptions> {
debug_assert!(self.is_valid());
crate::ffi::get_timeline_options(self).map(TimelineOptions)
}
pub fn set_use_houdini_time(&self, do_use: bool) -> Result<()> {
debug_assert!(self.is_valid());
crate::ffi::set_use_houdini_time(self, do_use)
}
pub fn get_use_houdini_time(&self) -> Result<bool> {
debug_assert!(self.is_valid());
crate::ffi::get_use_houdini_time(self)
}
pub fn get_viewport(&self) -> Result<Viewport> {
debug_assert!(self.is_valid());
crate::ffi::get_viewport(self).map(Viewport)
}
pub fn set_viewport(&self, viewport: &Viewport) -> Result<()> {
debug_assert!(self.is_valid());
crate::ffi::set_viewport(self, viewport)
}
pub fn set_sync(&self, enable: bool) -> Result<()> {
debug_assert!(self.is_valid());
crate::ffi::set_session_sync(self, enable)
}
pub fn get_sync_info(&self) -> Result<SessionSyncInfo> {
debug_assert!(self.is_valid());
crate::ffi::get_session_sync_info(self).map(SessionSyncInfo)
}
pub fn set_sync_info(&self, info: &SessionSyncInfo) -> Result<()> {
debug_assert!(self.is_valid());
crate::ffi::set_session_sync_info(self, &info.0)
}
pub fn get_license_type(&self) -> Result<License> {
debug_assert!(self.is_valid());
crate::ffi::session_get_license_type(self)
}
pub fn render_cop_to_image(
&self,
cop_node: impl Into<NodeHandle>,
image_planes: impl AsRef<str>,
path: impl AsRef<Path>,
) -> Result<String> {
debug!("Start rendering COP to image.");
let cop_node = cop_node.into();
debug_assert!(cop_node.is_valid(self)?);
crate::ffi::render_cop_to_image(self, cop_node)?;
crate::material::extract_image_to_file(self, cop_node, image_planes, path)
}
pub fn render_texture_to_image(
&self,
node: impl Into<NodeHandle>,
parm_name: &str,
) -> Result<()> {
debug_assert!(self.is_valid());
let name = CString::new(parm_name)?;
let node = node.into();
let id = crate::ffi::get_parm_id_from_name(&name, node, self)?;
crate::ffi::render_texture_to_image(self, node, crate::parameter::ParmHandle(id))
}
pub fn extract_image_to_file(
&self,
node: impl Into<NodeHandle>,
image_planes: &str,
path: impl AsRef<Path>,
) -> Result<String> {
crate::material::extract_image_to_file(self, node.into(), image_planes, path)
}
pub fn extract_image_to_memory(
&self,
node: impl Into<NodeHandle>,
buffer: &mut Vec<u8>,
image_planes: impl AsRef<str>,
format: impl AsRef<str>,
) -> Result<()> {
debug_assert!(self.is_valid());
crate::material::extract_image_to_memory(self, node.into(), buffer, image_planes, format)
}
pub fn get_image_info(&self, node: impl Into<NodeHandle>) -> Result<ImageInfo> {
debug_assert!(self.is_valid());
crate::ffi::get_image_info(self, node.into()).map(ImageInfo)
}
pub fn render_cop_to_memory(
&self,
cop_node: impl Into<NodeHandle>,
buffer: &mut Vec<u8>,
image_planes: impl AsRef<str>,
format: impl AsRef<str>,
) -> Result<()> {
debug!("Start rendering COP to memory.");
let cop_node = cop_node.into();
debug_assert!(cop_node.is_valid(self)?);
crate::ffi::render_cop_to_image(self, cop_node)?;
crate::material::extract_image_to_memory(self, cop_node, buffer, image_planes, format)
}
pub fn get_supported_image_formats(&self) -> Result<Vec<ImageFileFormat<'_>>> {
debug_assert!(self.is_valid());
crate::ffi::get_supported_image_file_formats(self).map(|v| {
v.into_iter()
.map(|inner| ImageFileFormat(inner, self.into()))
.collect()
})
}
pub fn get_active_cache_names(&self) -> Result<StringArray> {
debug_assert!(self.is_valid());
crate::ffi::get_active_cache_names(self)
}
pub fn get_cache_property_value(
&self,
cache_name: &str,
property: CacheProperty,
) -> Result<i32> {
let cache_name = CString::new(cache_name)?;
crate::ffi::get_cache_property(self, &cache_name, property)
}
pub fn set_cache_property_value(
&self,
cache_name: &str,
property: CacheProperty,
value: i32,
) -> Result<()> {
let cache_name = CString::new(cache_name)?;
crate::ffi::set_cache_property(self, &cache_name, property, value)
}
pub fn python_thread_interpreter_lock(&self, lock: bool) -> Result<()> {
debug_assert!(self.is_valid());
crate::ffi::python_thread_interpreter_lock(self, lock)
}
pub fn get_compositor_options(&self) -> Result<CompositorOptions> {
crate::ffi::get_compositor_options(self).map(CompositorOptions)
}
pub fn set_compositor_options(&self, options: &CompositorOptions) -> Result<()> {
crate::ffi::set_compositor_options(self, &options.0)
}
pub fn get_preset_names(&self, bytes: &[u8]) -> Result<Vec<String>> {
debug_assert!(self.is_valid());
let mut handles = vec![];
for handle in crate::ffi::get_preset_names(self, bytes)? {
let v = crate::stringhandle::get_string(handle, self)?;
handles.push(v);
}
Ok(handles)
}
pub fn start_performance_monitor_profile(&self, title: &str) -> Result<i32> {
let title = CString::new(title)?;
crate::ffi::start_performance_monitor_profile(self, &title)
}
pub fn stop_performance_monitor_profile(
&self,
profile_id: i32,
output_file: &str,
) -> Result<()> {
let output_file = CString::new(output_file)?;
crate::ffi::stop_performance_monitor_profile(self, profile_id, &output_file)
}
pub fn get_job_status(&self, job_id: i32) -> Result<JobStatus> {
crate::ffi::get_job_status(self, job_id)
}
}
impl Drop for Session {
fn drop(&mut self) {
if Arc::strong_count(&self.inner) == 1 {
debug!("Dropping session pid: {:?}", self.server_pid());
if self.is_valid() {
if self.inner.options.cleanup
&& let Err(e) = self.cleanup()
{
error!("Session cleanup failed in Drop: {}", e);
}
if let Err(e) = crate::ffi::shutdown_session(self) {
error!("Could not shutdown session in Drop: {}", e);
}
if let Err(e) = crate::ffi::close_session(self) {
error!("Closing session failed in Drop: {}", e);
}
} else {
debug!("Session was invalid in Drop!");
if let ConnectionType::ThriftPipe(f) = &self.inner.connection {
let _ = std::fs::remove_file(f);
}
}
}
}
}
pub fn connect_to_pipe(
pipe: impl AsRef<Path>,
options: Option<&SessionOptions>,
timeout: Option<Duration>,
pid: Option<u32>,
) -> Result<Session> {
debug!("Connecting to Thrift session: {:?}", pipe.as_ref());
let c_str = utils::path_to_cstring(&pipe)?;
let pipe = pipe.as_ref().as_os_str().to_os_string();
let timeout = timeout.unwrap_or_default();
let options = options.cloned().unwrap_or_default();
let mut waited = Duration::from_secs(0);
let wait_ms = Duration::from_millis(100);
let handle = loop {
let mut last_error = None;
debug!("Trying to connect to pipe server");
match crate::ffi::new_thrift_piped_session(&c_str, &options.session_info.0) {
Ok(handle) => break handle,
Err(e) => {
last_error.replace(e);
std::thread::sleep(wait_ms);
waited += wait_ms;
}
}
if waited > timeout {
return Err(last_error.unwrap()).context("Connection timeout");
}
};
let connection = ConnectionType::ThriftPipe(pipe);
let session = Session::new(handle, connection, options, pid);
session.initialize()?;
Ok(session)
}
pub fn connect_to_memory_server(
memory_name: &str,
options: Option<&SessionOptions>,
pid: Option<u32>,
) -> Result<Session> {
let mem_name = String::from(memory_name);
let mem_name_cstr = CString::new(memory_name)?;
let options = options.cloned().unwrap_or_default();
let handle =
crate::ffi::new_thrift_shared_memory_session(&mem_name_cstr, &options.session_info.0)?;
let connection = ConnectionType::SharedMemory(mem_name);
let session = Session::new(handle, connection, options, pid);
session.initialize()?;
Ok(session)
}
pub fn connect_to_socket(
addr: std::net::SocketAddrV4,
options: Option<&SessionOptions>,
) -> Result<Session> {
debug!("Connecting to socket server: {:?}", addr);
let host = CString::new(addr.ip().to_string()).expect("SocketAddr->CString");
let options = options.cloned().unwrap_or_default();
let handle =
crate::ffi::new_thrift_socket_session(addr.port() as i32, &host, &options.session_info.0)?;
let connection = ConnectionType::ThriftSocket(addr);
let session = Session::new(handle, connection, options, None);
session.initialize()?;
Ok(session)
}
pub fn new_in_process(options: Option<&SessionOptions>) -> Result<Session> {
debug!("Creating new in-process session");
let options = options.cloned().unwrap_or_default();
let handle = crate::ffi::create_inprocess_session(&options.session_info.0)?;
let connection = ConnectionType::InProcess;
let session = Session::new(handle, connection, options, Some(std::process::id()));
session.initialize()?;
Ok(session)
}
#[derive(Clone, Debug)]
pub struct SessionOptions {
pub cook_opt: CookOptions,
pub session_info: SessionInfo,
pub threaded: bool,
pub cleanup: bool,
pub log_file: Option<CString>,
pub ignore_already_init: bool,
pub env_files: Option<CString>,
pub env_variables: Option<Vec<(String, String)>>,
pub otl_path: Option<CString>,
pub dso_path: Option<CString>,
pub img_dso_path: Option<CString>,
pub aud_dso_path: Option<CString>,
}
impl Default for SessionOptions {
fn default() -> Self {
SessionOptions {
cook_opt: CookOptions::default(),
session_info: SessionInfo::default(),
threaded: false,
cleanup: false,
log_file: None,
ignore_already_init: true,
env_files: None,
env_variables: None,
otl_path: None,
dso_path: None,
img_dso_path: None,
aud_dso_path: None,
}
}
}
#[derive(Default)]
pub struct SessionOptionsBuilder {
cook_opt: CookOptions,
session_info: SessionInfo,
threaded: bool,
cleanup: bool,
log_file: Option<CString>,
ignore_already_init: bool,
env_variables: Option<Vec<(String, String)>>,
env_files: Option<CString>,
otl_path: Option<CString>,
dso_path: Option<CString>,
img_dso_path: Option<CString>,
aud_dso_path: Option<CString>,
}
impl SessionOptionsBuilder {
pub fn houdini_env_files<I>(mut self, files: I) -> Self
where
I: IntoIterator,
I::Item: AsRef<str>,
{
let paths = utils::join_paths(files);
self.env_files
.replace(CString::new(paths).expect("Zero byte"));
self
}
pub fn env_variables<'a, I, K, V>(mut self, variables: I) -> Self
where
I: Iterator<Item = &'a (K, V)>,
K: ToString + 'a,
V: ToString + 'a,
{
self.env_variables.replace(
variables
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
);
self
}
pub fn otl_search_paths<I>(mut self, paths: I) -> Self
where
I: IntoIterator,
I::Item: AsRef<str>,
{
let paths = utils::join_paths(paths);
self.otl_path
.replace(CString::new(paths).expect("Zero byte"));
self
}
pub fn dso_search_paths<P>(mut self, paths: P) -> Self
where
P: IntoIterator,
P::Item: AsRef<str>,
{
let paths = utils::join_paths(paths);
self.dso_path
.replace(CString::new(paths).expect("Zero byte"));
self
}
pub fn image_search_paths<P>(mut self, paths: P) -> Self
where
P: IntoIterator,
P::Item: AsRef<str>,
{
let paths = utils::join_paths(paths);
self.img_dso_path
.replace(CString::new(paths).expect("Zero byte"));
self
}
pub fn audio_search_paths<P>(mut self, paths: P) -> Self
where
P: IntoIterator,
P::Item: AsRef<str>,
{
let paths = utils::join_paths(paths);
self.aud_dso_path
.replace(CString::new(paths).expect("Zero byte"));
self
}
pub fn ignore_already_init(mut self, ignore: bool) -> Self {
self.ignore_already_init = ignore;
self
}
pub fn cook_options(mut self, options: CookOptions) -> Self {
self.cook_opt = options;
self
}
pub fn session_info(mut self, info: SessionInfo) -> Self {
self.session_info = info;
self
}
pub fn threaded(mut self, threaded: bool) -> Self {
self.threaded = threaded;
self
}
pub fn cleanup_on_close(mut self, cleanup: bool) -> Self {
self.cleanup = cleanup;
self
}
pub fn log_file(mut self, file: impl AsRef<Path>) -> Self {
self.log_file = Some(utils::path_to_cstring(file).unwrap());
self
}
pub fn build(mut self) -> SessionOptions {
self.write_temp_env_file();
SessionOptions {
cook_opt: self.cook_opt,
session_info: self.session_info,
threaded: self.threaded,
cleanup: self.cleanup,
log_file: self.log_file,
ignore_already_init: self.cleanup,
env_files: self.env_files,
env_variables: self.env_variables,
otl_path: self.otl_path,
dso_path: self.dso_path,
img_dso_path: self.img_dso_path,
aud_dso_path: self.aud_dso_path,
}
}
fn write_temp_env_file(&mut self) {
use std::io::Write;
if let Some(ref env) = self.env_variables {
let mut file = tempfile::Builder::new()
.suffix("_hars.env")
.tempfile()
.expect("tempfile");
for (k, v) in env.iter() {
writeln!(file, "{}={}", k, v).expect("write to .env file");
}
let (_, tmp_file) = file.keep().expect("persistent tempfile");
debug!(
"Creating temporary environment file: {}",
tmp_file.to_string_lossy()
);
let tmp_file = CString::new(tmp_file.to_string_lossy().to_string()).expect("null byte");
if let Some(old) = &mut self.env_files {
let mut bytes = old.as_bytes_with_nul().to_vec();
bytes.extend(tmp_file.into_bytes_with_nul());
self.env_files
.replace(unsafe { CString::from_vec_with_nul_unchecked(bytes) });
} else {
self.env_files.replace(tmp_file);
}
}
}
}
impl SessionOptions {
pub fn builder() -> SessionOptionsBuilder {
SessionOptionsBuilder::default()
}
}
impl From<i32> for SessionState {
fn from(s: i32) -> Self {
match s {
0 => SessionState::Ready,
1 => SessionState::ReadyWithFatalErrors,
2 => SessionState::ReadyWithCookErrors,
3 => SessionState::StartingCook,
4 => SessionState::Cooking,
5 => SessionState::StartingLoad,
6 => SessionState::Loading,
7 => SessionState::Max,
_ => panic!("Unmatched SessionState - {s}"),
}
}
}
pub fn start_engine_pipe_server(
path: impl AsRef<Path>,
log_file: Option<&str>,
options: &ThriftServerOptions,
) -> Result<u32> {
debug!("Starting named pipe server: {:?}", path.as_ref());
let log_file = log_file.map(CString::new).transpose()?;
let c_str = utils::path_to_cstring(path)?;
crate::ffi::clear_connection_error()?;
crate::ffi::start_thrift_pipe_server(&c_str, &options.0, log_file.as_deref())
}
pub fn start_engine_socket_server(
port: u16,
log_file: Option<&str>,
options: &ThriftServerOptions,
) -> Result<u32> {
debug!("Starting socket server on port: {}", port);
let log_file = log_file.map(CString::new).transpose()?;
crate::ffi::clear_connection_error()?;
crate::ffi::start_thrift_socket_server(port as i32, &options.0, log_file.as_deref())
}
pub fn start_houdini_server(
pipe_name: impl AsRef<str>,
houdini_executable: impl AsRef<Path>,
fx_license: bool,
) -> Result<Child> {
std::process::Command::new(houdini_executable.as_ref())
.arg(format!("-hess=pipe:{}", pipe_name.as_ref()))
.arg(if fx_license {
"-force-fx-license"
} else {
"-core"
})
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.map_err(HapiError::from)
}
pub fn start_shared_memory_server(
memory_name: &str,
options: &ThriftServerOptions,
log_file: Option<&CStr>,
) -> Result<u32> {
debug!("Starting shared memory server name: {memory_name}");
let memory_name = CString::new(memory_name)?;
crate::ffi::clear_connection_error()?;
crate::ffi::start_thrift_shared_memory_server(&memory_name, &options.0, log_file)
}
pub fn quick_session(options: Option<&SessionOptions>) -> Result<Session> {
let server_options = ThriftServerOptions::default()
.with_auto_close(true)
.with_timeout_ms(4000f32)
.with_verbosity(StatusVerbosity::Statusverbosity0);
let rand_memory_name = format!("shared-memory-{}", utils::random_string(16));
let log_file = match &options {
None => None,
Some(opt) => opt.log_file.as_deref(),
};
let pid = start_shared_memory_server(&rand_memory_name, &server_options, log_file)?;
connect_to_memory_server(&rand_memory_name, options, Some(pid))
}