hapi_rs/
session.rs

1//! Session is responsible for communicating with HAPI
2//!
3//! The Engine [promises](https://www.sidefx.com/docs/hengine/_h_a_p_i__sessions.html#HAPI_Sessions_Multithreading)
4//! to be thread-safe when accessing a single `Session` from multiple threads.
5//! `hapi-rs` relies on this promise and the [Session] struct holds only an `Arc` pointer to the session,
6//! and *does not* protect the session with Mutex, although there is a [ReentrantMutex]
7//! private member which is used internally in a few cases where API calls must be sequential.
8//!
9//! When the last instance of the `Session` is about to get dropped, it'll be cleaned up
10//! (if [SessionOptions::cleanup] was set) and automatically closed.
11//!
12//! The Engine process (pipe or socket) can be auto-terminated as well if told so when starting the server:
13//! See [start_engine_pipe_server] and [start_engine_socket_server]
14//!
15//! [quick_session] terminates the server by default. This is useful for quick one-off jobs.
16//!
17use 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
38// A result of HAPI_GetStatus with HAPI_STATUS_COOK_STATE
39pub type SessionState = State;
40
41use crate::ffi::ImageInfo;
42use crate::stringhandle::StringHandle;
43use crate::{ffi::raw, utils};
44
45/// Builder struct for [`Session::node_builder`] API
46pub 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    /// Give new node a label
56    pub fn with_label(mut self, label: impl Into<String>) -> Self {
57        self.label = Some(label.into());
58        self
59    }
60
61    /// Create new node as child of a parent node.
62    pub fn with_parent<H: AsRef<NodeHandle>>(mut self, parent: H) -> Self {
63        self.parent.replace(*parent.as_ref());
64        self
65    }
66
67    /// Cook node after creation.
68    pub fn cook(mut self, cook: bool) -> Self {
69        self.cook = cook;
70        self
71    }
72
73    /// Consume the builder and create the node
74    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
92/// Trait bound for [`Session::get_server_var()`] and [`Session::set_server_var()`]
93pub 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/// Result of async cook operation [`Session::cook`]
147#[derive(Debug, Clone, Eq, PartialEq)]
148pub enum CookResult {
149    Succeeded,
150    /// Some nodes cooked with errors
151    CookErrors(String),
152    /// One or more nodes could not cook - should abort cooking
153    FatalErrors(String),
154}
155
156impl CookResult {
157    /// Convenient method for cook result message if any
158    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/// By which means the session communicates with the server.
168#[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/// Session represents a unique connection to the Engine instance and all API calls require a valid session.
187/// It implements [`Clone`] and is [`Send`] and [`Sync`]
188#[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    /// Return [`SessionType`] current session is initialized with.
219    pub fn session_type(&self) -> SessionType {
220        self.inner.handle.type_
221    }
222
223    /// Return enum with extra connection data such as pipe file or socket.
224    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    /// Set environment variable on the server
238    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    /// Get environment variable from the server
249    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    /// Retrieve all server variables
258    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    /// Retrieve string data given a handle.
266    pub fn get_string(&self, handle: StringHandle) -> Result<String> {
267        crate::stringhandle::get_string(handle, self)
268    }
269
270    /// Retrieve multiple strings in batch mode.
271    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    /// Cleanup the session. Session will not be valid after this call
293    /// and needs to be initialized again
294    pub fn cleanup(&self) -> Result<()> {
295        debug!("Cleaning session");
296        debug_assert!(self.is_valid());
297        crate::ffi::cleanup_session(self)
298    }
299
300    /// Check if session is initialized
301    pub fn is_initialized(&self) -> bool {
302        debug_assert!(self.is_valid());
303        crate::ffi::is_session_initialized(self)
304    }
305
306    /// Create an input geometry node which can accept modifications
307    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    /// Create an input geometry node with [`PartType`] set to `Curve`
322    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    /// Create a node. `name` must start with a network category, e.g, "Object/geo", "Sop/box",
337    /// in operator namespace was used, the full name may look like this: namespace::Object/mynode
338    /// If you need more creating options, see the [`Session::node_builder`] API.
339    /// New node will *not* be cooked.
340    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    /// A builder pattern for creating a node with more options.
345    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    // Internal function for creating nodes
356    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    /// Delete the node from the session. See also [`HoudiniNode::delete`]
397    pub fn delete_node<H: Into<NodeHandle>>(&self, node: H) -> Result<()> {
398        crate::ffi::delete_node(node.into(), self)
399    }
400
401    /// Find a node given an absolute path. To find a child node, pass the `parent` node
402    /// or use [`HoudiniNode::find_child_node`]
403    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    /// Find a parameter by path, absolute or relative to a start node.
422    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    /// Returns a manager (root) node such as OBJ, TOP, CHOP, etc
440    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    /// Return a list of transforms for all object nodes under a given parent node.
453    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    /// Save current session to hip file
464    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    /// Load a hip file into current session
472    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    /// Merge a hip file into current session
480    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    /// Get node ids created by merging [`Session::merge_hip`] a hip file.
488    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    /// Load an HDA file into current session
494    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    /// Returns a list of loaded asset libraries including Houdini's default.
500    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    /// Interrupt session cooking
518    pub fn interrupt(&self) -> Result<()> {
519        debug_assert!(self.is_valid());
520        crate::ffi::interrupt(self)
521    }
522
523    // Uncertain if this API makes sense.
524    #[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    /// Get session state when the server is in threaded mode.
533    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    /// Is session currently cooking. In non-threaded mode always returns false
539    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    /// Explicit check if the session is valid. Many APIs do this check in the debug build.
548    #[inline(always)]
549    pub fn is_valid(&self) -> bool {
550        crate::ffi::is_session_valid(self)
551    }
552
553    /// Get the status message given a type and verbosity
554    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    /// Get session cook result status as string
564    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    /// How many nodes need to cook
570    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    /// How many nodes have already cooked
576    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    /// In threaded mode wait for Session finishes cooking. In single-thread mode, immediately return
582    /// See [Documentation](https://www.sidefx.com/docs/hengine/_h_a_p_i__sessions.html)
583    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                    // Continue polling
600                    _ => {}
601                }
602            }
603        } else {
604            // In single threaded mode, the cook happens inside of HAPI_CookNode(),
605            // and HAPI_GetStatus() will immediately return HAPI_STATE_READY.
606            Ok(CookResult::Succeeded)
607        }
608    }
609
610    /// Retrieve connection error if could not connect to engine instance
611    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    /// Get Houdini time
617    pub fn get_time(&self) -> Result<f32> {
618        debug_assert!(self.is_valid());
619        crate::ffi::get_time(self)
620    }
621
622    /// Set Houdini time
623    pub fn set_time(&self, time: f32) -> Result<()> {
624        debug_assert!(self.is_valid());
625        crate::ffi::set_time(self, time)
626    }
627
628    /// Lock the internal reentrant mutex. Should not be used in general, but may be useful
629    /// in certain situations when a series of API calls must be done in sequence
630    pub fn lock(&self) -> parking_lot::ReentrantMutexGuard<()> {
631        self.inner.lock.lock()
632    }
633
634    /// Set Houdini timeline options
635    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    /// Get Houdini timeline options
641    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    /// Set session to use Houdini time
647    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    /// Check if session uses Houdini time
653    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    /// Get the viewport(camera) position
659    pub fn get_viewport(&self) -> Result<Viewport> {
660        debug_assert!(self.is_valid());
661        crate::ffi::get_viewport(self).map(Viewport)
662    }
663
664    /// Set the viewport(camera) position
665    pub fn set_viewport(&self, viewport: &Viewport) -> Result<()> {
666        debug_assert!(self.is_valid());
667        crate::ffi::set_viewport(self, viewport)
668    }
669
670    /// Set session sync mode on/off
671    pub fn set_sync(&self, enable: bool) -> Result<()> {
672        debug_assert!(self.is_valid());
673        crate::ffi::set_session_sync(self, enable)
674    }
675    /// Get session sync info
676    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    /// Set session sync info
682    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    /// Get license type used by this session
688    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    /// Render a COP node to an image file
694    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    /// Render a COP node to a memory buffer
745    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                // The server should automatically delete the pipe file when closed successfully,
851                // but we could try a cleanup just in case.
852                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
861/// Connect to the engine process via a pipe file.
862/// If `timeout` is Some, function will try to connect to
863/// the server multiple times every 100ms until `timeout` is reached.
864/// Note: Default SessionOptions create a blocking session, non-threaded session,
865/// use [`SessionOptionsBuilder`] to configure this.
866pub 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            // last_error is guarantied to be Some().
892            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
919/// Connect to the engine process via a Unix socket
920pub 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
935/// Create in-process session
936pub 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/// Session options passed to session create functions like [`connect_to_pipe`]
947#[derive(Clone, Debug)]
948pub struct SessionOptions {
949    /// Session cook options
950    pub cook_opt: CookOptions,
951    /// Session connection options
952    pub session_info: SessionInfo,
953    /// Create a Threaded server connection
954    pub threaded: bool,
955    /// Cleanup session upon close
956    pub cleanup: bool,
957    pub log_file: Option<CString>,
958    /// Do not error out if session is already initialized
959    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)]
988/// A build for SessionOptions.
989pub 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    /// A list of Houdini environment file the Engine will load environment from.
1006    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    /// Set the server environment variables. See also [`Session::set_server_var`].
1018    /// The difference is this method writes out a temp file with the variables and
1019    /// implicitly pass it to the engine (as if [`Self::houdini_env_files`] was used.
1020    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    /// Add search paths for the Engine to find HDAs.
1035    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    /// Add search paths for the Engine to find DSO plugins.
1047    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    /// Add search paths for the Engine to find image plugins.
1059    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    /// Add search paths for the Engine to find audio files.
1071    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    /// Do not error when connecting to a server process which has a session already initialized.
1083    pub fn ignore_already_init(mut self, ignore: bool) -> Self {
1084        self.ignore_already_init = ignore;
1085        self
1086    }
1087
1088    /// Pass session [`CookOptions`]
1089    pub fn cook_options(mut self, options: CookOptions) -> Self {
1090        self.cook_opt = options;
1091        self
1092    }
1093
1094    /// Session init options [`SessionInfo`]
1095    pub fn session_info(mut self, info: SessionInfo) -> Self {
1096        self.session_info = info;
1097        self
1098    }
1099
1100    /// Makes the server operate in threaded mode. See the official docs for more info.
1101    pub fn threaded(mut self, threaded: bool) -> Self {
1102        self.threaded = threaded;
1103        self
1104    }
1105
1106    /// Cleanup the server session when the last connection drops.
1107    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    /// Consume the builder and return the result.
1118    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    // Helper function for Self::env_variables
1136    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                    // SAFETY: the bytes vec was obtained from the two CString's above.
1159                    .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    /// Create a [`SessionOptionsBuilder`]. Same as [`SessionOptionsBuilder::default()`].
1169    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
1190/// Spawn a new pipe Engine process and return its PID
1191pub 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
1203/// Spawn a new socket Engine server and return its PID
1204pub 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
1215/// Start an interactive Houdini session with engine server embedded.
1216pub 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
1235/// Spawn a new Engine server utilizing shared memory to transfer data.
1236pub 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
1247/// A quick drop-in session, useful for on-off jobs
1248/// It starts a **single-threaded** shared memory server and initialize a session with default options
1249pub 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}