1use log::{debug, error, warn};
18use parking_lot::ReentrantMutex;
19use std::ffi::{CStr, OsString};
20use std::fmt::Debug;
21use std::path::PathBuf;
22use std::process::Child;
23use std::time::Duration;
24use std::{ffi::CString, path::Path, sync::Arc};
25
26pub use crate::{
27 asset::AssetLibrary,
28 errors::*,
29 ffi::{
30 enums::*, CompositorOptions, CookOptions, ImageFileFormat, SessionInfo, SessionSyncInfo,
31 ThriftServerOptions, TimelineOptions, Viewport,
32 },
33 node::{HoudiniNode, ManagerNode, ManagerType, NodeHandle, NodeType, Transform},
34 parameter::Parameter,
35 stringhandle::StringArray,
36};
37
38pub type SessionState = State;
40
41use crate::ffi::ImageInfo;
42use crate::stringhandle::StringHandle;
43use crate::{ffi::raw, utils};
44
45pub struct NodeBuilder<'s> {
47 session: &'s Session,
48 name: String,
49 label: Option<String>,
50 parent: Option<NodeHandle>,
51 cook: bool,
52}
53
54impl NodeBuilder<'_> {
55 pub fn with_label(mut self, label: impl Into<String>) -> Self {
57 self.label = Some(label.into());
58 self
59 }
60
61 pub fn with_parent<H: AsRef<NodeHandle>>(mut self, parent: H) -> Self {
63 self.parent.replace(*parent.as_ref());
64 self
65 }
66
67 pub fn cook(mut self, cook: bool) -> Self {
69 self.cook = cook;
70 self
71 }
72
73 pub fn create(self) -> Result<HoudiniNode> {
75 let NodeBuilder {
76 session,
77 name,
78 label,
79 parent,
80 cook,
81 } = self;
82 session.create_node_with(&name, parent, label.as_deref(), cook)
83 }
84}
85
86impl PartialEq for raw::HAPI_Session {
87 fn eq(&self, other: &Self) -> bool {
88 self.type_ == other.type_ && self.id == other.id
89 }
90}
91
92pub trait EnvVariable {
94 type Type: ?Sized + ToOwned + Debug;
95 fn get_value(session: &Session, key: impl AsRef<str>)
96 -> Result<<Self::Type as ToOwned>::Owned>;
97 fn set_value(session: &Session, key: impl AsRef<str>, val: &Self::Type) -> Result<()>;
98}
99
100impl EnvVariable for str {
101 type Type = str;
102
103 fn get_value(session: &Session, key: impl AsRef<str>) -> Result<String> {
104 let key = CString::new(key.as_ref())?;
105 let handle = crate::ffi::get_server_env_str(session, &key)?;
106 crate::stringhandle::get_string(handle, session)
107 }
108
109 fn set_value(session: &Session, key: impl AsRef<str>, val: &Self::Type) -> Result<()> {
110 let key = CString::new(key.as_ref())?;
111 let val = CString::new(val)?;
112 crate::ffi::set_server_env_str(session, &key, &val)
113 }
114}
115
116impl EnvVariable for Path {
117 type Type = Self;
118
119 fn get_value(session: &Session, key: impl AsRef<str>) -> Result<PathBuf> {
120 let key = CString::new(key.as_ref())?;
121 crate::stringhandle::get_string(crate::ffi::get_server_env_str(session, &key)?, session)
122 .map(PathBuf::from)
123 }
124
125 fn set_value(session: &Session, key: impl AsRef<str>, val: &Self::Type) -> Result<()> {
126 let key = CString::new(key.as_ref())?;
127 let val = utils::path_to_cstring(val)?;
128 crate::ffi::set_server_env_str(session, &key, &val)
129 }
130}
131
132impl EnvVariable for i32 {
133 type Type = Self;
134
135 fn get_value(session: &Session, key: impl AsRef<str>) -> Result<Self::Type> {
136 let key = CString::new(key.as_ref())?;
137 crate::ffi::get_server_env_int(session, &key)
138 }
139
140 fn set_value(session: &Session, key: impl AsRef<str>, val: &Self::Type) -> Result<()> {
141 let key = CString::new(key.as_ref())?;
142 crate::ffi::set_server_env_int(session, &key, *val)
143 }
144}
145
146#[derive(Debug, Clone, Eq, PartialEq)]
148pub enum CookResult {
149 Succeeded,
150 CookErrors(String),
152 FatalErrors(String),
154}
155
156impl CookResult {
157 pub fn message(&self) -> Option<&str> {
159 match self {
160 Self::Succeeded => None,
161 Self::CookErrors(msg) => Some(msg.as_str()),
162 Self::FatalErrors(msg) => Some(msg.as_str()),
163 }
164 }
165}
166
167#[derive(Debug, Clone, Eq, PartialEq)]
169pub enum ConnectionType {
170 ThriftPipe(OsString),
171 ThriftSocket(std::net::SocketAddrV4),
172 SharedMemory(String),
173 InProcess,
174 Custom,
175}
176
177#[derive(Debug)]
178pub(crate) struct SessionInner {
179 pub(crate) handle: raw::HAPI_Session,
180 pub(crate) options: SessionOptions,
181 pub(crate) connection: ConnectionType,
182 pub(crate) pid: Option<u32>,
183 pub(crate) lock: ReentrantMutex<()>,
184}
185
186#[derive(Debug, Clone)]
189pub struct Session {
190 pub(crate) inner: Arc<SessionInner>,
191}
192
193impl PartialEq for Session {
194 fn eq(&self, other: &Self) -> bool {
195 self.inner.handle.id == other.inner.handle.id
196 && self.inner.handle.type_ == other.inner.handle.type_
197 }
198}
199
200impl Session {
201 fn new(
202 handle: raw::HAPI_Session,
203 connection: ConnectionType,
204 options: SessionOptions,
205 pid: Option<u32>,
206 ) -> Session {
207 Session {
208 inner: Arc::new(SessionInner {
209 handle,
210 options,
211 connection,
212 lock: ReentrantMutex::new(()),
213 pid,
214 }),
215 }
216 }
217
218 pub fn session_type(&self) -> SessionType {
220 self.inner.handle.type_
221 }
222
223 pub fn connection_type(&self) -> &ConnectionType {
225 &self.inner.connection
226 }
227
228 pub fn server_pid(&self) -> Option<u32> {
229 self.inner.pid
230 }
231
232 #[inline(always)]
233 pub(crate) fn ptr(&self) -> *const raw::HAPI_Session {
234 &(self.inner.handle) as *const _
235 }
236
237 pub fn set_server_var<T: EnvVariable + ?Sized>(
239 &self,
240 key: &str,
241 value: &T::Type,
242 ) -> Result<()> {
243 debug_assert!(self.is_valid());
244 debug!("Setting server variable {key}={value:?}");
245 T::set_value(self, key, value)
246 }
247
248 pub fn get_server_var<T: EnvVariable + ?Sized>(
250 &self,
251 key: &str,
252 ) -> Result<<T::Type as ToOwned>::Owned> {
253 debug_assert!(self.is_valid());
254 T::get_value(self, key)
255 }
256
257 pub fn get_server_variables(&self) -> Result<StringArray> {
259 debug_assert!(self.is_valid());
260 let count = crate::ffi::get_server_env_var_count(self)?;
261 let handles = crate::ffi::get_server_env_var_list(self, count)?;
262 crate::stringhandle::get_string_array(&handles, self).context("Calling get_string_array")
263 }
264
265 pub fn get_string(&self, handle: StringHandle) -> Result<String> {
267 crate::stringhandle::get_string(handle, self)
268 }
269
270 pub fn get_string_batch(&self, handles: &[StringHandle]) -> Result<StringArray> {
272 crate::stringhandle::get_string_array(handles, self)
273 }
274
275 fn initialize(&self) -> Result<()> {
276 debug!("Initializing session");
277 debug_assert!(self.is_valid());
278 let res = crate::ffi::initialize_session(self, &self.inner.options);
279 match res {
280 Ok(_) => Ok(()),
281 Err(HapiError {
282 kind: Kind::Hapi(HapiResult::AlreadyInitialized),
283 ..
284 }) => {
285 warn!("Session already initialized, skipping");
286 Ok(())
287 }
288 Err(e) => Err(e),
289 }
290 }
291
292 pub fn cleanup(&self) -> Result<()> {
295 debug!("Cleaning session");
296 debug_assert!(self.is_valid());
297 crate::ffi::cleanup_session(self)
298 }
299
300 pub fn is_initialized(&self) -> bool {
302 debug_assert!(self.is_valid());
303 crate::ffi::is_session_initialized(self)
304 }
305
306 pub fn create_input_node(
308 &self,
309 name: &str,
310 parent: Option<NodeHandle>,
311 ) -> Result<crate::geometry::Geometry> {
312 debug!("Creating input node: {}", name);
313 debug_assert!(self.is_valid());
314 let name = CString::new(name)?;
315 let id = crate::ffi::create_input_node(self, &name, parent)?;
316 let node = HoudiniNode::new(self.clone(), NodeHandle(id), None)?;
317 let info = crate::geometry::GeoInfo::from_node(&node)?;
318 Ok(crate::geometry::Geometry { node, info })
319 }
320
321 pub fn create_input_curve_node(
323 &self,
324 name: &str,
325 parent: Option<NodeHandle>,
326 ) -> Result<crate::geometry::Geometry> {
327 debug!("Creating input curve node: {}", name);
328 debug_assert!(self.is_valid());
329 let name = CString::new(name)?;
330 let id = crate::ffi::create_input_curve_node(self, &name, parent)?;
331 let node = HoudiniNode::new(self.clone(), NodeHandle(id), None)?;
332 let info = crate::geometry::GeoInfo::from_node(&node)?;
333 Ok(crate::geometry::Geometry { node, info })
334 }
335
336 pub fn create_node(&self, name: impl AsRef<str>) -> Result<HoudiniNode> {
341 self.create_node_with(name.as_ref(), None, None, false)
342 }
343
344 pub fn node_builder(&self, node_name: impl Into<String>) -> NodeBuilder {
346 NodeBuilder {
347 session: self,
348 name: node_name.into(),
349 label: None,
350 parent: None,
351 cook: false,
352 }
353 }
354
355 pub(crate) fn create_node_with<P>(
357 &self,
358 name: &str,
359 parent: P,
360 label: Option<&str>,
361 cook: bool,
362 ) -> Result<HoudiniNode>
363 where
364 P: Into<Option<NodeHandle>>,
365 {
366 let parent = parent.into();
367 debug!("Creating node instance: {}", name);
368 debug_assert!(self.is_valid());
369 debug_assert!(
370 parent.is_some() || name.contains('/'),
371 "Node name must be fully qualified if parent is not specified"
372 );
373 debug_assert!(
374 !(parent.is_some() && name.contains('/')),
375 "Cannot use fully qualified node name with parent"
376 );
377 let name = CString::new(name)?;
378 let label = label.map(CString::new).transpose()?;
379 let node_id = crate::ffi::create_node(&name, label.as_deref(), self, parent, cook)?;
380 if self.inner.options.threaded {
381 use std::borrow::Cow;
382 if let CookResult::FatalErrors(message) = self.cook()? {
383 return Err(HapiError::new(
384 Kind::Hapi(HapiResult::Failure),
385 Some(Cow::Owned(format!(
386 "Could not create node {:?}",
387 name.to_string_lossy()
388 ))),
389 Some(Cow::Owned(message)),
390 ));
391 }
392 }
393 HoudiniNode::new(self.clone(), NodeHandle(node_id), None)
394 }
395
396 pub fn delete_node<H: Into<NodeHandle>>(&self, node: H) -> Result<()> {
398 crate::ffi::delete_node(node.into(), self)
399 }
400
401 pub fn get_node_from_path(
404 &self,
405 path: impl AsRef<str>,
406 parent: impl Into<Option<NodeHandle>>,
407 ) -> Result<Option<HoudiniNode>> {
408 debug_assert!(self.is_valid());
409 debug!("Searching node at path: {}", path.as_ref());
410 let path = CString::new(path.as_ref())?;
411 match crate::ffi::get_node_from_path(self, parent.into(), &path) {
412 Ok(handle) => Ok(NodeHandle(handle).to_node(self).ok()),
413 Err(HapiError {
414 kind: Kind::Hapi(HapiResult::InvalidArgument),
415 ..
416 }) => Ok(None),
417 Err(e) => Err(e),
418 }
419 }
420
421 pub fn find_parameter_from_path(
423 &self,
424 path: impl AsRef<str>,
425 start: impl Into<Option<NodeHandle>>,
426 ) -> Result<Option<Parameter>> {
427 debug_assert!(self.is_valid());
428 debug!("Searching parameter at path: {}", path.as_ref());
429 let Some((path, parm)) = path.as_ref().rsplit_once('/') else {
430 return Ok(None);
431 };
432 let Some(node) = self.get_node_from_path(path, start)? else {
433 debug!("Node {} not found", path);
434 return Ok(None);
435 };
436 Ok(node.parameter(parm).ok())
437 }
438
439 pub fn get_manager_node(&self, manager: ManagerType) -> Result<ManagerNode> {
441 debug_assert!(self.is_valid());
442 debug!("Getting Manager node of type: {:?}", manager);
443 let node_type = NodeType::from(manager);
444 let handle = crate::ffi::get_manager_node(self, node_type)?;
445 Ok(ManagerNode {
446 session: self.clone(),
447 handle: NodeHandle(handle),
448 node_type: manager,
449 })
450 }
451
452 pub fn get_composed_object_transform(
454 &self,
455 parent: impl AsRef<NodeHandle>,
456 rst_order: RSTOrder,
457 ) -> Result<Vec<Transform>> {
458 debug_assert!(self.is_valid());
459 crate::ffi::get_composed_object_transforms(self, *parent.as_ref(), rst_order)
460 .map(|transforms| transforms.into_iter().map(Transform).collect())
461 }
462
463 pub fn save_hip(&self, path: impl AsRef<Path>, lock_nodes: bool) -> Result<()> {
465 debug!("Saving hip file: {:?}", path.as_ref());
466 debug_assert!(self.is_valid());
467 let path = utils::path_to_cstring(path)?;
468 crate::ffi::save_hip(self, &path, lock_nodes)
469 }
470
471 pub fn load_hip(&self, path: impl AsRef<Path>, cook: bool) -> Result<()> {
473 debug!("Loading hip file: {:?}", path.as_ref());
474 debug_assert!(self.is_valid());
475 let path = utils::path_to_cstring(path)?;
476 crate::ffi::load_hip(self, &path, cook)
477 }
478
479 pub fn merge_hip(&self, name: &str, cook: bool) -> Result<i32> {
481 debug!("Merging hip file: {}", name);
482 debug_assert!(self.is_valid());
483 let name = CString::new(name)?;
484 crate::ffi::merge_hip(self, &name, cook)
485 }
486
487 pub fn get_hip_file_nodes(&self, hip_id: i32) -> Result<Vec<NodeHandle>> {
489 crate::ffi::get_hipfile_node_ids(self, hip_id)
490 .map(|handles| handles.into_iter().map(NodeHandle).collect())
491 }
492
493 pub fn load_asset_file(&self, file: impl AsRef<Path>) -> Result<AssetLibrary> {
495 debug_assert!(self.is_valid());
496 AssetLibrary::from_file(self.clone(), file)
497 }
498
499 pub fn get_loaded_asset_libraries(&self) -> Result<Vec<AssetLibrary>> {
501 debug_assert!(self.is_valid());
502
503 crate::ffi::get_asset_library_ids(self)?
504 .into_iter()
505 .map(|library_id| {
506 crate::ffi::get_asset_library_file_path(self, library_id).map(|lib_file| {
507 AssetLibrary {
508 lib_id: library_id,
509 session: self.clone(),
510 file: Some(PathBuf::from(lib_file)),
511 }
512 })
513 })
514 .collect::<Result<Vec<_>>>()
515 }
516
517 pub fn interrupt(&self) -> Result<()> {
519 debug_assert!(self.is_valid());
520 crate::ffi::interrupt(self)
521 }
522
523 #[doc(hidden)]
525 #[allow(unused)]
526 pub(crate) fn get_call_result_status(&self) -> Result<HapiResult> {
527 debug_assert!(self.is_valid());
528 let status = crate::ffi::get_status_code(self, StatusType::CallResult)?;
529 Ok(unsafe { std::mem::transmute::<i32, HapiResult>(status) })
530 }
531
532 pub fn get_cook_state_status(&self) -> Result<SessionState> {
534 debug_assert!(self.is_valid());
535 crate::ffi::get_cook_state_status(self)
536 }
537
538 pub fn is_cooking(&self) -> Result<bool> {
540 debug_assert!(self.is_valid());
541 Ok(matches!(
542 self.get_cook_state_status()?,
543 SessionState::Cooking
544 ))
545 }
546
547 #[inline(always)]
549 pub fn is_valid(&self) -> bool {
550 crate::ffi::is_session_valid(self)
551 }
552
553 pub fn get_status_string(
555 &self,
556 status: StatusType,
557 verbosity: StatusVerbosity,
558 ) -> Result<String> {
559 debug_assert!(self.is_valid());
560 crate::ffi::get_status_string(self, status, verbosity)
561 }
562
563 pub fn get_cook_result_string(&self, verbosity: StatusVerbosity) -> Result<String> {
565 debug_assert!(self.is_valid());
566 self.get_status_string(StatusType::CookResult, verbosity)
567 }
568
569 pub fn cooking_total_count(&self) -> Result<i32> {
571 debug_assert!(self.is_valid());
572 crate::ffi::get_cooking_total_count(self)
573 }
574
575 pub fn cooking_current_count(&self) -> Result<i32> {
577 debug_assert!(self.is_valid());
578 crate::ffi::get_cooking_current_count(self)
579 }
580
581 pub fn cook(&self) -> Result<CookResult> {
584 debug_assert!(self.is_valid());
585 debug!("Cooking session..");
586 if self.inner.options.threaded {
587 loop {
588 match self.get_cook_state_status()? {
589 SessionState::Ready => break Ok(CookResult::Succeeded),
590 SessionState::ReadyWithFatalErrors => {
591 self.interrupt()?;
592 let err = self.get_cook_result_string(StatusVerbosity::Errors)?;
593 break Ok(CookResult::FatalErrors(err));
594 }
595 SessionState::ReadyWithCookErrors => {
596 let err = self.get_cook_result_string(StatusVerbosity::Errors)?;
597 break Ok(CookResult::CookErrors(err));
598 }
599 _ => {}
601 }
602 }
603 } else {
604 Ok(CookResult::Succeeded)
607 }
608 }
609
610 pub fn get_connection_error(&self, clear: bool) -> Result<String> {
612 debug_assert!(self.is_valid());
613 crate::ffi::get_connection_error(clear)
614 }
615
616 pub fn get_time(&self) -> Result<f32> {
618 debug_assert!(self.is_valid());
619 crate::ffi::get_time(self)
620 }
621
622 pub fn set_time(&self, time: f32) -> Result<()> {
624 debug_assert!(self.is_valid());
625 crate::ffi::set_time(self, time)
626 }
627
628 pub fn lock(&self) -> parking_lot::ReentrantMutexGuard<()> {
631 self.inner.lock.lock()
632 }
633
634 pub fn set_timeline_options(&self, options: TimelineOptions) -> Result<()> {
636 debug_assert!(self.is_valid());
637 crate::ffi::set_timeline_options(self, &options.0)
638 }
639
640 pub fn get_timeline_options(&self) -> Result<TimelineOptions> {
642 debug_assert!(self.is_valid());
643 crate::ffi::get_timeline_options(self).map(TimelineOptions)
644 }
645
646 pub fn set_use_houdini_time(&self, do_use: bool) -> Result<()> {
648 debug_assert!(self.is_valid());
649 crate::ffi::set_use_houdini_time(self, do_use)
650 }
651
652 pub fn get_use_houdini_time(&self) -> Result<bool> {
654 debug_assert!(self.is_valid());
655 crate::ffi::get_use_houdini_time(self)
656 }
657
658 pub fn get_viewport(&self) -> Result<Viewport> {
660 debug_assert!(self.is_valid());
661 crate::ffi::get_viewport(self).map(Viewport)
662 }
663
664 pub fn set_viewport(&self, viewport: &Viewport) -> Result<()> {
666 debug_assert!(self.is_valid());
667 crate::ffi::set_viewport(self, viewport)
668 }
669
670 pub fn set_sync(&self, enable: bool) -> Result<()> {
672 debug_assert!(self.is_valid());
673 crate::ffi::set_session_sync(self, enable)
674 }
675 pub fn get_sync_info(&self) -> Result<SessionSyncInfo> {
677 debug_assert!(self.is_valid());
678 crate::ffi::get_session_sync_info(self).map(SessionSyncInfo)
679 }
680
681 pub fn set_sync_info(&self, info: &SessionSyncInfo) -> Result<()> {
683 debug_assert!(self.is_valid());
684 crate::ffi::set_session_sync_info(self, &info.0)
685 }
686
687 pub fn get_license_type(&self) -> Result<License> {
689 debug_assert!(self.is_valid());
690 crate::ffi::session_get_license_type(self)
691 }
692
693 pub fn render_cop_to_image(
695 &self,
696 cop_node: impl Into<NodeHandle>,
697 image_planes: impl AsRef<str>,
698 path: impl AsRef<Path>,
699 ) -> Result<String> {
700 debug!("Start rendering COP to image.");
701 let cop_node = cop_node.into();
702 debug_assert!(cop_node.is_valid(self)?);
703 crate::ffi::render_cop_to_image(self, cop_node)?;
704 crate::material::extract_image_to_file(self, cop_node, image_planes, path)
705 }
706
707 pub fn render_texture_to_image(
708 &self,
709 node: impl Into<NodeHandle>,
710 parm_name: &str,
711 ) -> Result<()> {
712 debug_assert!(self.is_valid());
713 let name = CString::new(parm_name)?;
714 let node = node.into();
715 let id = crate::ffi::get_parm_id_from_name(&name, node, self)?;
716 crate::ffi::render_texture_to_image(self, node, crate::parameter::ParmHandle(id))
717 }
718
719 pub fn extract_image_to_file(
720 &self,
721 node: impl Into<NodeHandle>,
722 image_planes: &str,
723 path: impl AsRef<Path>,
724 ) -> Result<String> {
725 crate::material::extract_image_to_file(self, node.into(), image_planes, path)
726 }
727
728 pub fn extract_image_to_memory(
729 &self,
730 node: impl Into<NodeHandle>,
731 buffer: &mut Vec<u8>,
732 image_planes: impl AsRef<str>,
733 format: impl AsRef<str>,
734 ) -> Result<()> {
735 debug_assert!(self.is_valid());
736 crate::material::extract_image_to_memory(self, node.into(), buffer, image_planes, format)
737 }
738
739 pub fn get_image_info(&self, node: impl Into<NodeHandle>) -> Result<ImageInfo> {
740 debug_assert!(self.is_valid());
741 crate::ffi::get_image_info(self, node.into()).map(ImageInfo)
742 }
743
744 pub fn render_cop_to_memory(
746 &self,
747 cop_node: impl Into<NodeHandle>,
748 buffer: &mut Vec<u8>,
749 image_planes: impl AsRef<str>,
750 format: impl AsRef<str>,
751 ) -> Result<()> {
752 debug!("Start rendering COP to memory.");
753 let cop_node = cop_node.into();
754 debug_assert!(cop_node.is_valid(self)?);
755 crate::ffi::render_cop_to_image(self, cop_node)?;
756 crate::material::extract_image_to_memory(self, cop_node, buffer, image_planes, format)
757 }
758
759 pub fn get_supported_image_formats(&self) -> Result<Vec<ImageFileFormat<'_>>> {
760 debug_assert!(self.is_valid());
761 crate::ffi::get_supported_image_file_formats(self).map(|v| {
762 v.into_iter()
763 .map(|inner| ImageFileFormat(inner, self.into()))
764 .collect()
765 })
766 }
767
768 pub fn get_active_cache_names(&self) -> Result<StringArray> {
769 debug_assert!(self.is_valid());
770 crate::ffi::get_active_cache_names(self)
771 }
772
773 pub fn get_cache_property_value(
774 &self,
775 cache_name: &str,
776 property: CacheProperty,
777 ) -> Result<i32> {
778 let cache_name = CString::new(cache_name)?;
779 crate::ffi::get_cache_property(self, &cache_name, property)
780 }
781
782 pub fn set_cache_property_value(
783 &self,
784 cache_name: &str,
785 property: CacheProperty,
786 value: i32,
787 ) -> Result<()> {
788 let cache_name = CString::new(cache_name)?;
789 crate::ffi::set_cache_property(self, &cache_name, property, value)
790 }
791
792 pub fn python_thread_interpreter_lock(&self, lock: bool) -> Result<()> {
793 debug_assert!(self.is_valid());
794 crate::ffi::python_thread_interpreter_lock(self, lock)
795 }
796 pub fn get_compositor_options(&self) -> Result<CompositorOptions> {
797 crate::ffi::get_compositor_options(self).map(CompositorOptions)
798 }
799
800 pub fn set_compositor_options(&self, options: &CompositorOptions) -> Result<()> {
801 crate::ffi::set_compositor_options(self, &options.0)
802 }
803
804 pub fn get_preset_names(&self, bytes: &[u8]) -> Result<Vec<String>> {
805 debug_assert!(self.is_valid());
806 let mut handles = vec![];
807 for handle in crate::ffi::get_preset_names(self, bytes)? {
808 let v = crate::stringhandle::get_string(handle, self)?;
809 handles.push(v);
810 }
811 Ok(handles)
812 }
813
814 pub fn start_performance_monitor_profile(&self, title: &str) -> Result<i32> {
815 let title = CString::new(title)?;
816 crate::ffi::start_performance_monitor_profile(self, &title)
817 }
818
819 pub fn stop_performance_monitor_profile(
820 &self,
821 profile_id: i32,
822 output_file: &str,
823 ) -> Result<()> {
824 let output_file = CString::new(output_file)?;
825 crate::ffi::stop_performance_monitor_profile(self, profile_id, &output_file)
826 }
827
828 pub fn get_job_status(&self, job_id: i32) -> Result<JobStatus> {
829 crate::ffi::get_job_status(self, job_id)
830 }
831}
832
833impl Drop for Session {
834 fn drop(&mut self) {
835 if Arc::strong_count(&self.inner) == 1 {
836 debug!("Dropping session pid: {:?}", self.server_pid());
837 if self.is_valid() {
838 if self.inner.options.cleanup {
839 if let Err(e) = self.cleanup() {
840 error!("Session cleanup failed in Drop: {}", e);
841 }
842 }
843 if let Err(e) = crate::ffi::shutdown_session(self) {
844 error!("Could not shutdown session in Drop: {}", e);
845 }
846 if let Err(e) = crate::ffi::close_session(self) {
847 error!("Closing session failed in Drop: {}", e);
848 }
849 } else {
850 debug!("Session was invalid in Drop!");
853 if let ConnectionType::ThriftPipe(f) = &self.inner.connection {
854 let _ = std::fs::remove_file(f);
855 }
856 }
857 }
858 }
859}
860
861pub fn connect_to_pipe(
867 pipe: impl AsRef<Path>,
868 options: Option<&SessionOptions>,
869 timeout: Option<Duration>,
870 pid: Option<u32>,
871) -> Result<Session> {
872 debug!("Connecting to Thrift session: {:?}", pipe.as_ref());
873 let c_str = utils::path_to_cstring(&pipe)?;
874 let pipe = pipe.as_ref().as_os_str().to_os_string();
875 let timeout = timeout.unwrap_or_default();
876 let options = options.cloned().unwrap_or_default();
877 let mut waited = Duration::from_secs(0);
878 let wait_ms = Duration::from_millis(100);
879 let handle = loop {
880 let mut last_error = None;
881 debug!("Trying to connect to pipe server");
882 match crate::ffi::new_thrift_piped_session(&c_str, &options.session_info.0) {
883 Ok(handle) => break handle,
884 Err(e) => {
885 last_error.replace(e);
886 std::thread::sleep(wait_ms);
887 waited += wait_ms;
888 }
889 }
890 if waited > timeout {
891 return Err(last_error.unwrap()).context("Connection timeout");
893 }
894 };
895 let connection = ConnectionType::ThriftPipe(pipe);
896 let session = Session::new(handle, connection, options, pid);
897 session.initialize()?;
898 Ok(session)
899}
900
901pub fn connect_to_memory_server(
902 memory_name: &str,
903 options: Option<&SessionOptions>,
904 pid: Option<u32>,
905) -> Result<Session> {
906 let mem_name = String::from(memory_name);
907 let mem_name_cstr = CString::new(memory_name)?;
908
909 let options = options.cloned().unwrap_or_default();
910 let handle =
911 crate::ffi::new_thrift_shared_memory_session(&mem_name_cstr, &options.session_info.0)?;
912
913 let connection = ConnectionType::SharedMemory(mem_name);
914 let session = Session::new(handle, connection, options, pid);
915 session.initialize()?;
916 Ok(session)
917}
918
919pub fn connect_to_socket(
921 addr: std::net::SocketAddrV4,
922 options: Option<&SessionOptions>,
923) -> Result<Session> {
924 debug!("Connecting to socket server: {:?}", addr);
925 let host = CString::new(addr.ip().to_string()).expect("SocketAddr->CString");
926 let options = options.cloned().unwrap_or_default();
927 let handle =
928 crate::ffi::new_thrift_socket_session(addr.port() as i32, &host, &options.session_info.0)?;
929 let connection = ConnectionType::ThriftSocket(addr);
930 let session = Session::new(handle, connection, options, None);
931 session.initialize()?;
932 Ok(session)
933}
934
935pub fn new_in_process(options: Option<&SessionOptions>) -> Result<Session> {
937 debug!("Creating new in-process session");
938 let options = options.cloned().unwrap_or_default();
939 let handle = crate::ffi::create_inprocess_session(&options.session_info.0)?;
940 let connection = ConnectionType::InProcess;
941 let session = Session::new(handle, connection, options, Some(std::process::id()));
942 session.initialize()?;
943 Ok(session)
944}
945
946#[derive(Clone, Debug)]
948pub struct SessionOptions {
949 pub cook_opt: CookOptions,
951 pub session_info: SessionInfo,
953 pub threaded: bool,
955 pub cleanup: bool,
957 pub log_file: Option<CString>,
958 pub ignore_already_init: bool,
960 pub env_files: Option<CString>,
961 pub env_variables: Option<Vec<(String, String)>>,
962 pub otl_path: Option<CString>,
963 pub dso_path: Option<CString>,
964 pub img_dso_path: Option<CString>,
965 pub aud_dso_path: Option<CString>,
966}
967
968impl Default for SessionOptions {
969 fn default() -> Self {
970 SessionOptions {
971 cook_opt: CookOptions::default(),
972 session_info: SessionInfo::default(),
973 threaded: false,
974 cleanup: false,
975 log_file: None,
976 ignore_already_init: true,
977 env_files: None,
978 env_variables: None,
979 otl_path: None,
980 dso_path: None,
981 img_dso_path: None,
982 aud_dso_path: None,
983 }
984 }
985}
986
987#[derive(Default)]
988pub struct SessionOptionsBuilder {
990 cook_opt: CookOptions,
991 session_info: SessionInfo,
992 threaded: bool,
993 cleanup: bool,
994 log_file: Option<CString>,
995 ignore_already_init: bool,
996 env_variables: Option<Vec<(String, String)>>,
997 env_files: Option<CString>,
998 otl_path: Option<CString>,
999 dso_path: Option<CString>,
1000 img_dso_path: Option<CString>,
1001 aud_dso_path: Option<CString>,
1002}
1003
1004impl SessionOptionsBuilder {
1005 pub fn houdini_env_files<I>(mut self, files: I) -> Self
1007 where
1008 I: IntoIterator,
1009 I::Item: AsRef<str>,
1010 {
1011 let paths = utils::join_paths(files);
1012 self.env_files
1013 .replace(CString::new(paths).expect("Zero byte"));
1014 self
1015 }
1016
1017 pub fn env_variables<'a, I, K, V>(mut self, variables: I) -> Self
1021 where
1022 I: Iterator<Item = &'a (K, V)>,
1023 K: ToString + 'a,
1024 V: ToString + 'a,
1025 {
1026 self.env_variables.replace(
1027 variables
1028 .map(|(k, v)| (k.to_string(), v.to_string()))
1029 .collect(),
1030 );
1031 self
1032 }
1033
1034 pub fn otl_search_paths<I>(mut self, paths: I) -> Self
1036 where
1037 I: IntoIterator,
1038 I::Item: AsRef<str>,
1039 {
1040 let paths = utils::join_paths(paths);
1041 self.otl_path
1042 .replace(CString::new(paths).expect("Zero byte"));
1043 self
1044 }
1045
1046 pub fn dso_search_paths<P>(mut self, paths: P) -> Self
1048 where
1049 P: IntoIterator,
1050 P::Item: AsRef<str>,
1051 {
1052 let paths = utils::join_paths(paths);
1053 self.dso_path
1054 .replace(CString::new(paths).expect("Zero byte"));
1055 self
1056 }
1057
1058 pub fn image_search_paths<P>(mut self, paths: P) -> Self
1060 where
1061 P: IntoIterator,
1062 P::Item: AsRef<str>,
1063 {
1064 let paths = utils::join_paths(paths);
1065 self.img_dso_path
1066 .replace(CString::new(paths).expect("Zero byte"));
1067 self
1068 }
1069
1070 pub fn audio_search_paths<P>(mut self, paths: P) -> Self
1072 where
1073 P: IntoIterator,
1074 P::Item: AsRef<str>,
1075 {
1076 let paths = utils::join_paths(paths);
1077 self.aud_dso_path
1078 .replace(CString::new(paths).expect("Zero byte"));
1079 self
1080 }
1081
1082 pub fn ignore_already_init(mut self, ignore: bool) -> Self {
1084 self.ignore_already_init = ignore;
1085 self
1086 }
1087
1088 pub fn cook_options(mut self, options: CookOptions) -> Self {
1090 self.cook_opt = options;
1091 self
1092 }
1093
1094 pub fn session_info(mut self, info: SessionInfo) -> Self {
1096 self.session_info = info;
1097 self
1098 }
1099
1100 pub fn threaded(mut self, threaded: bool) -> Self {
1102 self.threaded = threaded;
1103 self
1104 }
1105
1106 pub fn cleanup_on_close(mut self, cleanup: bool) -> Self {
1108 self.cleanup = cleanup;
1109 self
1110 }
1111
1112 pub fn log_file(mut self, file: impl AsRef<Path>) -> Self {
1113 self.log_file = Some(utils::path_to_cstring(file).unwrap());
1114 self
1115 }
1116
1117 pub fn build(mut self) -> SessionOptions {
1119 self.write_temp_env_file();
1120 SessionOptions {
1121 cook_opt: self.cook_opt,
1122 session_info: self.session_info,
1123 threaded: self.threaded,
1124 cleanup: self.cleanup,
1125 log_file: self.log_file,
1126 ignore_already_init: self.cleanup,
1127 env_files: self.env_files,
1128 env_variables: self.env_variables,
1129 otl_path: self.otl_path,
1130 dso_path: self.dso_path,
1131 img_dso_path: self.img_dso_path,
1132 aud_dso_path: self.aud_dso_path,
1133 }
1134 }
1135 fn write_temp_env_file(&mut self) {
1137 use std::io::Write;
1138
1139 if let Some(ref env) = self.env_variables {
1140 let mut file = tempfile::Builder::new()
1141 .suffix("_hars.env")
1142 .tempfile()
1143 .expect("tempfile");
1144 for (k, v) in env.iter() {
1145 writeln!(file, "{}={}", k, v).expect("write to .env file");
1146 }
1147 let (_, tmp_file) = file.keep().expect("persistent tempfile");
1148 debug!(
1149 "Creating temporary environment file: {}",
1150 tmp_file.to_string_lossy()
1151 );
1152 let tmp_file = CString::new(tmp_file.to_string_lossy().to_string()).expect("null byte");
1153
1154 if let Some(old) = &mut self.env_files {
1155 let mut bytes = old.as_bytes_with_nul().to_vec();
1156 bytes.extend(tmp_file.into_bytes_with_nul());
1157 self.env_files
1158 .replace(unsafe { CString::from_vec_with_nul_unchecked(bytes) });
1160 } else {
1161 self.env_files.replace(tmp_file);
1162 }
1163 }
1164 }
1165}
1166
1167impl SessionOptions {
1168 pub fn builder() -> SessionOptionsBuilder {
1170 SessionOptionsBuilder::default()
1171 }
1172}
1173
1174impl From<i32> for SessionState {
1175 fn from(s: i32) -> Self {
1176 match s {
1177 0 => SessionState::Ready,
1178 1 => SessionState::ReadyWithFatalErrors,
1179 2 => SessionState::ReadyWithCookErrors,
1180 3 => SessionState::StartingCook,
1181 4 => SessionState::Cooking,
1182 5 => SessionState::StartingLoad,
1183 6 => SessionState::Loading,
1184 7 => SessionState::Max,
1185 _ => panic!("Unmatched SessionState - {s}"),
1186 }
1187 }
1188}
1189
1190pub fn start_engine_pipe_server(
1192 path: impl AsRef<Path>,
1193 log_file: Option<&str>,
1194 options: &ThriftServerOptions,
1195) -> Result<u32> {
1196 debug!("Starting named pipe server: {:?}", path.as_ref());
1197 let log_file = log_file.map(CString::new).transpose()?;
1198 let c_str = utils::path_to_cstring(path)?;
1199 crate::ffi::clear_connection_error()?;
1200 crate::ffi::start_thrift_pipe_server(&c_str, &options.0, log_file.as_deref())
1201}
1202
1203pub fn start_engine_socket_server(
1205 port: u16,
1206 log_file: Option<&str>,
1207 options: &ThriftServerOptions,
1208) -> Result<u32> {
1209 debug!("Starting socket server on port: {}", port);
1210 let log_file = log_file.map(CString::new).transpose()?;
1211 crate::ffi::clear_connection_error()?;
1212 crate::ffi::start_thrift_socket_server(port as i32, &options.0, log_file.as_deref())
1213}
1214
1215pub fn start_houdini_server(
1217 pipe_name: impl AsRef<str>,
1218 houdini_executable: impl AsRef<Path>,
1219 fx_license: bool,
1220) -> Result<Child> {
1221 std::process::Command::new(houdini_executable.as_ref())
1222 .arg(format!("-hess=pipe:{}", pipe_name.as_ref()))
1223 .arg(if fx_license {
1224 "-force-fx-license"
1225 } else {
1226 "-core"
1227 })
1228 .stdin(std::process::Stdio::null())
1229 .stdout(std::process::Stdio::null())
1230 .stderr(std::process::Stdio::null())
1231 .spawn()
1232 .map_err(HapiError::from)
1233}
1234
1235pub fn start_shared_memory_server(
1237 memory_name: &str,
1238 options: &ThriftServerOptions,
1239 log_file: Option<&CStr>,
1240) -> Result<u32> {
1241 debug!("Starting shared memory server name: {memory_name}");
1242 let memory_name = CString::new(memory_name)?;
1243 crate::ffi::clear_connection_error()?;
1244 crate::ffi::start_thrift_shared_memory_server(&memory_name, &options.0, log_file)
1245}
1246
1247pub fn quick_session(options: Option<&SessionOptions>) -> Result<Session> {
1250 let server_options = ThriftServerOptions::default()
1251 .with_auto_close(true)
1252 .with_timeout_ms(4000f32)
1253 .with_verbosity(StatusVerbosity::Statusverbosity0);
1254 let rand_memory_name = format!("shared-memory-{}", utils::random_string(16));
1255 let log_file = match &options {
1256 None => None,
1257 Some(opt) => opt.log_file.as_deref(),
1258 };
1259 let pid = start_shared_memory_server(&rand_memory_name, &server_options, log_file)?;
1260 connect_to_memory_server(&rand_memory_name, options, Some(pid))
1261}