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 [parking_lot::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
38pub type SessionState = State;
39
40use crate::ffi::ImageInfo;
41use crate::stringhandle::StringHandle;
42use crate::{ffi::raw, utils};
43
44/// Builder struct for [`Session::node_builder`] API
45pub struct NodeBuilder<'s> {
46    session: &'s Session,
47    name: String,
48    label: Option<String>,
49    parent: Option<NodeHandle>,
50    cook: bool,
51}
52
53impl<'s> NodeBuilder<'s> {
54    /// Give new node a label
55    pub fn with_label(mut self, label: impl Into<String>) -> Self {
56        self.label = Some(label.into());
57        self
58    }
59
60    /// Create new node as child of a parent node.
61    pub fn with_parent<H: AsRef<NodeHandle>>(mut self, parent: H) -> Self {
62        self.parent.replace(*parent.as_ref());
63        self
64    }
65
66    /// Cook node after creation.
67    pub fn cook(mut self, cook: bool) -> Self {
68        self.cook = cook;
69        self
70    }
71
72    /// Consume the builder and create the node
73    pub fn create(self) -> Result<HoudiniNode> {
74        let NodeBuilder {
75            session,
76            name,
77            label,
78            parent,
79            cook,
80        } = self;
81        session.create_node_with(&name, parent, label.as_deref(), cook)
82    }
83}
84
85impl PartialEq for raw::HAPI_Session {
86    fn eq(&self, other: &Self) -> bool {
87        self.type_ == other.type_ && self.id == other.id
88    }
89}
90
91/// Trait bound for [`Session::get_server_var()`] and [`Session::set_server_var()`]
92pub trait EnvVariable {
93    type Type: ?Sized + ToOwned + Debug;
94    fn get_value(session: &Session, key: impl AsRef<str>)
95        -> Result<<Self::Type as ToOwned>::Owned>;
96    fn set_value(session: &Session, key: impl AsRef<str>, val: &Self::Type) -> Result<()>;
97}
98
99impl EnvVariable for str {
100    type Type = str;
101
102    fn get_value(session: &Session, key: impl AsRef<str>) -> Result<String> {
103        let key = CString::new(key.as_ref())?;
104        let handle = crate::ffi::get_server_env_str(session, &key)?;
105        crate::stringhandle::get_string(handle, session)
106    }
107
108    fn set_value(session: &Session, key: impl AsRef<str>, val: &Self::Type) -> Result<()> {
109        let key = CString::new(key.as_ref())?;
110        let val = CString::new(val)?;
111        crate::ffi::set_server_env_str(session, &key, &val)
112    }
113}
114
115impl EnvVariable for Path {
116    type Type = Self;
117
118    fn get_value(session: &Session, key: impl AsRef<str>) -> Result<PathBuf> {
119        let key = CString::new(key.as_ref())?;
120        crate::stringhandle::get_string(crate::ffi::get_server_env_str(session, &key)?, session)
121            .map(PathBuf::from)
122    }
123
124    fn set_value(session: &Session, key: impl AsRef<str>, val: &Self::Type) -> Result<()> {
125        let key = CString::new(key.as_ref())?;
126        let val = utils::path_to_cstring(val)?;
127        crate::ffi::set_server_env_str(session, &key, &val)
128    }
129}
130
131impl EnvVariable for i32 {
132    type Type = Self;
133
134    fn get_value(session: &Session, key: impl AsRef<str>) -> Result<Self::Type> {
135        let key = CString::new(key.as_ref())?;
136        crate::ffi::get_server_env_int(session, &key)
137    }
138
139    fn set_value(session: &Session, key: impl AsRef<str>, val: &Self::Type) -> Result<()> {
140        let key = CString::new(key.as_ref())?;
141        crate::ffi::set_server_env_int(session, &key, *val)
142    }
143}
144
145/// Result of async cook operation [`Session::cook`]
146#[derive(Debug, Clone, Eq, PartialEq)]
147pub enum CookResult {
148    Succeeded,
149    /// Some nodes cooked with errors
150    CookErrors(String),
151    /// One or more nodes could not cook - should abort cooking
152    FatalErrors(String),
153}
154
155/// By which means the session communicates with the server.
156#[derive(Debug, Clone, Eq, PartialEq)]
157pub enum ConnectionType {
158    ThriftPipe(OsString),
159    ThriftSocket(std::net::SocketAddrV4),
160    SharedMemory(String),
161    InProcess,
162    Custom,
163}
164
165#[derive(Debug)]
166pub(crate) struct SessionInner {
167    pub(crate) handle: raw::HAPI_Session,
168    pub(crate) options: SessionOptions,
169    pub(crate) connection: ConnectionType,
170    pub(crate) pid: Option<u32>,
171    pub(crate) lock: ReentrantMutex<()>,
172}
173
174/// Session represents a unique connection to the Engine instance and all API calls require a valid session.
175/// It implements [`Clone`] and is [`Send`] and [`Sync`]
176#[derive(Debug, Clone)]
177pub struct Session {
178    pub(crate) inner: Arc<SessionInner>,
179}
180
181impl PartialEq for Session {
182    fn eq(&self, other: &Self) -> bool {
183        self.inner.handle.id == other.inner.handle.id
184            && self.inner.handle.type_ == other.inner.handle.type_
185    }
186}
187
188impl Session {
189    fn new(
190        handle: raw::HAPI_Session,
191        connection: ConnectionType,
192        options: SessionOptions,
193        pid: Option<u32>,
194    ) -> Session {
195        Session {
196            inner: Arc::new(SessionInner {
197                handle,
198                options,
199                connection,
200                lock: ReentrantMutex::new(()),
201                pid,
202            }),
203        }
204    }
205
206    /// Return [`SessionType`] current session is initialized with.
207    pub fn session_type(&self) -> SessionType {
208        self.inner.handle.type_
209    }
210
211    /// Return enum with extra connection data such as pipe file or socket.
212    pub fn connection_type(&self) -> &ConnectionType {
213        &self.inner.connection
214    }
215
216    pub fn server_pid(&self) -> Option<u32> {
217        self.inner.pid
218    }
219
220    #[inline(always)]
221    pub(crate) fn ptr(&self) -> *const raw::HAPI_Session {
222        &(self.inner.handle) as *const _
223    }
224
225    /// Set environment variable on the server
226    pub fn set_server_var<T: EnvVariable + ?Sized>(
227        &self,
228        key: &str,
229        value: &T::Type,
230    ) -> Result<()> {
231        debug_assert!(self.is_valid());
232        debug!("Setting server variable {key}={value:?}");
233        T::set_value(self, key, value)
234    }
235
236    /// Get environment variable from the server
237    pub fn get_server_var<T: EnvVariable + ?Sized>(
238        &self,
239        key: &str,
240    ) -> Result<<T::Type as ToOwned>::Owned> {
241        debug_assert!(self.is_valid());
242        T::get_value(self, key)
243    }
244
245    /// Retrieve all server variables
246    pub fn get_server_variables(&self) -> Result<StringArray> {
247        debug_assert!(self.is_valid());
248        let count = crate::ffi::get_server_env_var_count(self)?;
249        let handles = crate::ffi::get_server_env_var_list(self, count)?;
250        crate::stringhandle::get_string_array(&handles, self).context("Calling get_string_array")
251    }
252
253    /// Retrieve string data given a handle.
254    pub fn get_string(&self, handle: StringHandle) -> Result<String> {
255        crate::stringhandle::get_string(handle, self)
256    }
257
258    /// Retrieve multiple strings in batch mode.
259    pub fn get_string_batch(&self, handles: &[StringHandle]) -> Result<StringArray> {
260        crate::stringhandle::get_string_array(handles, self)
261    }
262
263    fn initialize(&self) -> Result<()> {
264        debug!("Initializing session");
265        debug_assert!(self.is_valid());
266        let res = crate::ffi::initialize_session(self, &self.inner.options);
267        match res {
268            Ok(_) => Ok(()),
269            Err(HapiError {
270                kind: Kind::Hapi(HapiResult::AlreadyInitialized),
271                ..
272            }) => {
273                warn!("Session already initialized, skipping");
274                Ok(())
275            }
276            Err(e) => Err(e),
277        }
278    }
279
280    /// Cleanup the session. Session will not be valid after this call
281    /// and needs to be initialized again
282    pub fn cleanup(&self) -> Result<()> {
283        debug!("Cleaning session");
284        debug_assert!(self.is_valid());
285        crate::ffi::cleanup_session(self)
286    }
287
288    /// Check if session is initialized
289    pub fn is_initialized(&self) -> bool {
290        debug_assert!(self.is_valid());
291        crate::ffi::is_session_initialized(self)
292    }
293
294    /// Create an input geometry node which can accept modifications
295    pub fn create_input_node(
296        &self,
297        name: &str,
298        parent: Option<NodeHandle>,
299    ) -> Result<crate::geometry::Geometry> {
300        debug!("Creating input node: {}", name);
301        debug_assert!(self.is_valid());
302        let name = CString::new(name)?;
303        let id = crate::ffi::create_input_node(self, &name, parent)?;
304        let node = HoudiniNode::new(self.clone(), NodeHandle(id), None)?;
305        let info = crate::geometry::GeoInfo::from_node(&node)?;
306        Ok(crate::geometry::Geometry { node, info })
307    }
308
309    /// Create an input geometry node with [`crate::enums::PartType`] set to `Curve`
310    pub fn create_input_curve_node(
311        &self,
312        name: &str,
313        parent: Option<NodeHandle>,
314    ) -> Result<crate::geometry::Geometry> {
315        debug!("Creating input curve node: {}", name);
316        debug_assert!(self.is_valid());
317        let name = CString::new(name)?;
318        let id = crate::ffi::create_input_curve_node(self, &name, parent)?;
319        let node = HoudiniNode::new(self.clone(), NodeHandle(id), None)?;
320        let info = crate::geometry::GeoInfo::from_node(&node)?;
321        Ok(crate::geometry::Geometry { node, info })
322    }
323
324    /// Create a node. `name` must start with a network category, e.g, "Object/geo", "Sop/box",
325    /// in operator namespace was used, the full name may look like this: namespace::Object/mynode
326    /// If you need more creating options, see the [`Session::node_builder`] API.
327    /// New node will *not* be cooked.
328    pub fn create_node(&self, name: impl AsRef<str>) -> Result<HoudiniNode> {
329        self.create_node_with(name.as_ref(), None, None, false)
330    }
331
332    /// A builder pattern for creating a node with more options.
333    pub fn node_builder(&self, node_name: impl Into<String>) -> NodeBuilder {
334        NodeBuilder {
335            session: self,
336            name: node_name.into(),
337            label: None,
338            parent: None,
339            cook: false,
340        }
341    }
342
343    // Internal function for creating nodes
344    pub(crate) fn create_node_with<P>(
345        &self,
346        name: &str,
347        parent: P,
348        label: Option<&str>,
349        cook: bool,
350    ) -> Result<HoudiniNode>
351    where
352        P: Into<Option<NodeHandle>>,
353    {
354        let parent = parent.into();
355        debug!("Creating node instance: {}", name);
356        debug_assert!(self.is_valid());
357        debug_assert!(
358            parent.is_some() || name.contains('/'),
359            "Node name must be fully qualified if parent is not specified"
360        );
361        debug_assert!(
362            !(parent.is_some() && name.contains('/')),
363            "Cannot use fully qualified node name with parent"
364        );
365        let name = CString::new(name)?;
366        let label = label.map(CString::new).transpose()?;
367        let id = crate::ffi::create_node(&name, label.as_deref(), self, parent, cook)?;
368        HoudiniNode::new(self.clone(), NodeHandle(id), None)
369    }
370
371    /// Delete the node from the session. See also [`HoudiniNode::delete`]
372    pub fn delete_node<H: Into<NodeHandle>>(&self, node: H) -> Result<()> {
373        crate::ffi::delete_node(node.into(), self)
374    }
375
376    /// Find a node given an absolute path. To find a child node, pass the `parent` node
377    /// or use [`HoudiniNode::find_child_node`]
378    pub fn get_node_from_path(
379        &self,
380        path: impl AsRef<str>,
381        parent: impl Into<Option<NodeHandle>>,
382    ) -> Result<Option<HoudiniNode>> {
383        debug_assert!(self.is_valid());
384        debug!("Searching node at path: {}", path.as_ref());
385        let path = CString::new(path.as_ref())?;
386        match crate::ffi::get_node_from_path(self, parent.into(), &path) {
387            Ok(handle) => Ok(NodeHandle(handle).to_node(self).ok()),
388            Err(HapiError {
389                kind: Kind::Hapi(HapiResult::InvalidArgument),
390                ..
391            }) => Ok(None),
392            Err(e) => Err(e),
393        }
394    }
395
396    /// Find a parameter by path, absolute or relative to a start node.
397    pub fn find_parameter_from_path(
398        &self,
399        path: impl AsRef<str>,
400        start: impl Into<Option<NodeHandle>>,
401    ) -> Result<Option<Parameter>> {
402        debug_assert!(self.is_valid());
403        debug!("Searching parameter at path: {}", path.as_ref());
404        let Some((path, parm)) = path.as_ref().rsplit_once('/') else {
405            return Ok(None);
406        };
407        let Some(node) = self.get_node_from_path(path, start)? else {
408            debug!("Node {} not found", path);
409            return Ok(None);
410        };
411        Ok(node.parameter(parm).ok())
412    }
413
414    /// Returns a manager (root) node such as OBJ, TOP, CHOP, etc
415    pub fn get_manager_node(&self, manager: ManagerType) -> Result<ManagerNode> {
416        debug_assert!(self.is_valid());
417        debug!("Getting Manager node of type: {:?}", manager);
418        let node_type = NodeType::from(manager);
419        let handle = crate::ffi::get_manager_node(self, node_type)?;
420        Ok(ManagerNode {
421            session: self.clone(),
422            handle: NodeHandle(handle),
423            node_type: manager,
424        })
425    }
426
427    /// Return a list of transforms for all object nodes under a given parent node.
428    pub fn get_composed_object_transform(
429        &self,
430        parent: impl AsRef<NodeHandle>,
431        rst_order: RSTOrder,
432    ) -> Result<Vec<Transform>> {
433        debug_assert!(self.is_valid());
434        crate::ffi::get_composed_object_transforms(self, *parent.as_ref(), rst_order)
435            .map(|transforms| transforms.into_iter().map(Transform).collect())
436    }
437
438    /// Save current session to hip file
439    pub fn save_hip(&self, path: impl AsRef<Path>, lock_nodes: bool) -> Result<()> {
440        debug!("Saving hip file: {:?}", path.as_ref());
441        debug_assert!(self.is_valid());
442        let path = utils::path_to_cstring(path)?;
443        crate::ffi::save_hip(self, &path, lock_nodes)
444    }
445
446    /// Load a hip file into current session
447    pub fn load_hip(&self, path: impl AsRef<Path>, cook: bool) -> Result<()> {
448        debug!("Loading hip file: {:?}", path.as_ref());
449        debug_assert!(self.is_valid());
450        let path = utils::path_to_cstring(path)?;
451        crate::ffi::load_hip(self, &path, cook)
452    }
453
454    /// Merge a hip file into current session
455    pub fn merge_hip(&self, name: &str, cook: bool) -> Result<i32> {
456        debug!("Merging hip file: {}", name);
457        debug_assert!(self.is_valid());
458        let name = CString::new(name)?;
459        crate::ffi::merge_hip(self, &name, cook)
460    }
461
462    /// Get node ids created by merging [`Session::merge_hip`] a hip file.
463    pub fn get_hip_file_nodes(&self, hip_id: i32) -> Result<Vec<NodeHandle>> {
464        crate::ffi::get_hipfile_node_ids(self, hip_id)
465            .map(|handles| handles.into_iter().map(NodeHandle).collect())
466    }
467
468    /// Load an HDA file into current session
469    pub fn load_asset_file(&self, file: impl AsRef<Path>) -> Result<AssetLibrary> {
470        debug_assert!(self.is_valid());
471        AssetLibrary::from_file(self.clone(), file)
472    }
473
474    /// Returns a list of loaded asset libraries including Houdini's default.
475    pub fn get_loaded_asset_libraries(&self) -> Result<Vec<AssetLibrary>> {
476        debug_assert!(self.is_valid());
477
478        crate::ffi::get_asset_library_ids(self)?
479            .into_iter()
480            .map(|library_id| {
481                crate::ffi::get_asset_library_file_path(self, library_id).map(|lib_file| {
482                    AssetLibrary {
483                        lib_id: library_id,
484                        session: self.clone(),
485                        file: Some(PathBuf::from(lib_file)),
486                    }
487                })
488            })
489            .collect::<Result<Vec<_>>>()
490    }
491
492    /// Interrupt session cooking
493    pub fn interrupt(&self) -> Result<()> {
494        debug_assert!(self.is_valid());
495        crate::ffi::interrupt(self)
496    }
497
498    /// Get session state of a requested [`crate::enums::StatusType`]
499    pub fn get_status(&self, flag: StatusType) -> Result<SessionState> {
500        debug_assert!(self.is_valid());
501        crate::ffi::get_status(self, flag)
502    }
503
504    /// Is session currently cooking. In non-threaded mode always returns false
505    pub fn is_cooking(&self) -> Result<bool> {
506        debug_assert!(self.is_valid());
507        Ok(matches!(
508            self.get_status(StatusType::CookState)?,
509            SessionState::Cooking
510        ))
511    }
512
513    /// Explicit check if the session is valid. Many APIs do this check in the debug build.
514    #[inline(always)]
515    pub fn is_valid(&self) -> bool {
516        crate::ffi::is_session_valid(self)
517    }
518
519    /// Get the status message given a type and verbosity
520    pub fn get_status_string(
521        &self,
522        status: StatusType,
523        verbosity: StatusVerbosity,
524    ) -> Result<String> {
525        debug_assert!(self.is_valid());
526        crate::ffi::get_status_string(self, status, verbosity)
527    }
528
529    /// Get session cook result status as string
530    pub fn get_cook_result_string(&self, verbosity: StatusVerbosity) -> Result<String> {
531        debug_assert!(self.is_valid());
532        self.get_status_string(StatusType::CookResult, verbosity)
533    }
534
535    /// How many nodes need to cook
536    pub fn cooking_total_count(&self) -> Result<i32> {
537        debug_assert!(self.is_valid());
538        crate::ffi::get_cooking_total_count(self)
539    }
540
541    /// How many nodes have already cooked
542    pub fn cooking_current_count(&self) -> Result<i32> {
543        debug_assert!(self.is_valid());
544        crate::ffi::get_cooking_current_count(self)
545    }
546
547    /// In threaded mode wait for Session finishes cooking. In single-thread mode, immediately return
548    /// See [Documentation](https://www.sidefx.com/docs/hengine/_h_a_p_i__sessions.html)
549    pub fn cook(&self) -> Result<CookResult> {
550        debug_assert!(self.is_valid());
551        debug!("Cooking session..");
552        if self.inner.options.threaded {
553            loop {
554                match self.get_status(StatusType::CookState)? {
555                    SessionState::Ready => break Ok(CookResult::Succeeded),
556                    SessionState::ReadyWithFatalErrors => {
557                        self.interrupt()?;
558                        let err = self.get_cook_result_string(StatusVerbosity::Errors)?;
559                        break Ok(CookResult::FatalErrors(err));
560                    }
561                    SessionState::ReadyWithCookErrors => {
562                        let err = self.get_cook_result_string(StatusVerbosity::Errors)?;
563                        break Ok(CookResult::CookErrors(err));
564                    }
565                    // Continue polling
566                    _ => {}
567                }
568            }
569        } else {
570            // In single threaded mode, the cook happens inside of HAPI_CookNode(),
571            // and HAPI_GetStatus() will immediately return HAPI_STATE_READY.
572            Ok(CookResult::Succeeded)
573        }
574    }
575
576    /// Retrieve connection error if could not connect to engine instance
577    pub fn get_connection_error(&self, clear: bool) -> Result<String> {
578        debug_assert!(self.is_valid());
579        crate::ffi::get_connection_error(clear)
580    }
581
582    /// Get Houdini time
583    pub fn get_time(&self) -> Result<f32> {
584        debug_assert!(self.is_valid());
585        crate::ffi::get_time(self)
586    }
587
588    /// Set Houdini time
589    pub fn set_time(&self, time: f32) -> Result<()> {
590        debug_assert!(self.is_valid());
591        crate::ffi::set_time(self, time)
592    }
593
594    /// Lock the internal reentrant mutex. Should not be used in general, but may be useful
595    /// in certain situations when a series of API calls must be done in sequence
596    pub fn lock(&self) -> parking_lot::ReentrantMutexGuard<()> {
597        self.inner.lock.lock()
598    }
599
600    /// Set Houdini timeline options
601    pub fn set_timeline_options(&self, options: TimelineOptions) -> Result<()> {
602        debug_assert!(self.is_valid());
603        crate::ffi::set_timeline_options(self, &options.0)
604    }
605
606    /// Get Houdini timeline options
607    pub fn get_timeline_options(&self) -> Result<TimelineOptions> {
608        debug_assert!(self.is_valid());
609        crate::ffi::get_timeline_options(self).map(TimelineOptions)
610    }
611
612    /// Set session to use Houdini time
613    pub fn set_use_houdini_time(&self, do_use: bool) -> Result<()> {
614        debug_assert!(self.is_valid());
615        crate::ffi::set_use_houdini_time(self, do_use)
616    }
617
618    /// Check if session uses Houdini time
619    pub fn get_use_houdini_time(&self) -> Result<bool> {
620        debug_assert!(self.is_valid());
621        crate::ffi::get_use_houdini_time(self)
622    }
623
624    /// Get the viewport(camera) position
625    pub fn get_viewport(&self) -> Result<Viewport> {
626        debug_assert!(self.is_valid());
627        crate::ffi::get_viewport(self).map(Viewport)
628    }
629
630    /// Set the viewport(camera) position
631    pub fn set_viewport(&self, viewport: &Viewport) -> Result<()> {
632        debug_assert!(self.is_valid());
633        crate::ffi::set_viewport(self, viewport)
634    }
635
636    /// Set session sync mode on/off
637    pub fn set_sync(&self, enable: bool) -> Result<()> {
638        debug_assert!(self.is_valid());
639        crate::ffi::set_session_sync(self, enable)
640    }
641    /// Get session sync info
642    pub fn get_sync_info(&self) -> Result<SessionSyncInfo> {
643        debug_assert!(self.is_valid());
644        crate::ffi::get_session_sync_info(self).map(SessionSyncInfo)
645    }
646
647    /// Set session sync info
648    pub fn set_sync_info(&self, info: &SessionSyncInfo) -> Result<()> {
649        debug_assert!(self.is_valid());
650        crate::ffi::set_session_sync_info(self, &info.0)
651    }
652
653    /// Get license type used by this session
654    pub fn get_license_type(&self) -> Result<License> {
655        debug_assert!(self.is_valid());
656        crate::ffi::session_get_license_type(self)
657    }
658
659    /// Render a COP node to an image file
660    pub fn render_cop_to_image(
661        &self,
662        cop_node: impl Into<NodeHandle>,
663        image_planes: impl AsRef<str>,
664        path: impl AsRef<Path>,
665    ) -> Result<String> {
666        debug!("Start rendering COP to image.");
667        let cop_node = cop_node.into();
668        debug_assert!(cop_node.is_valid(self)?);
669        crate::ffi::render_cop_to_image(self, cop_node)?;
670        crate::material::extract_image_to_file(self, cop_node, image_planes, path)
671    }
672
673    pub fn render_texture_to_image(
674        &self,
675        node: impl Into<NodeHandle>,
676        parm_name: &str,
677    ) -> Result<()> {
678        debug_assert!(self.is_valid());
679        let name = CString::new(parm_name)?;
680        let node = node.into();
681        let id = crate::ffi::get_parm_id_from_name(&name, node, self)?;
682        crate::ffi::render_texture_to_image(&self, node, crate::parameter::ParmHandle(id))
683    }
684
685    pub fn extract_image_to_file(
686        &self,
687        node: impl Into<NodeHandle>,
688        image_planes: &str,
689        path: impl AsRef<Path>,
690    ) -> Result<String> {
691        crate::material::extract_image_to_file(self, node.into(), image_planes, path)
692    }
693
694    pub fn extract_image_to_memory(
695        &self,
696        node: impl Into<NodeHandle>,
697        buffer: &mut Vec<u8>,
698        image_planes: impl AsRef<str>,
699        format: impl AsRef<str>,
700    ) -> Result<()> {
701        debug_assert!(self.is_valid());
702        crate::material::extract_image_to_memory(self, node.into(), buffer, image_planes, format)
703    }
704
705    pub fn get_image_info(&self, node: impl Into<NodeHandle>) -> Result<ImageInfo> {
706        debug_assert!(self.is_valid());
707        crate::ffi::get_image_info(self, node.into()).map(ImageInfo)
708    }
709
710    /// Render a COP node to a memory buffer
711    pub fn render_cop_to_memory(
712        &self,
713        cop_node: impl Into<NodeHandle>,
714        buffer: &mut Vec<u8>,
715        image_planes: impl AsRef<str>,
716        format: impl AsRef<str>,
717    ) -> Result<()> {
718        debug!("Start rendering COP to memory.");
719        let cop_node = cop_node.into();
720        debug_assert!(cop_node.is_valid(self)?);
721        crate::ffi::render_cop_to_image(self, cop_node)?;
722        crate::material::extract_image_to_memory(self, cop_node, buffer, image_planes, format)
723    }
724
725    pub fn get_supported_image_formats(&self) -> Result<Vec<ImageFileFormat<'_>>> {
726        debug_assert!(self.is_valid());
727        crate::ffi::get_supported_image_file_formats(self).map(|v| {
728            v.into_iter()
729                .map(|inner| ImageFileFormat(inner, self.into()))
730                .collect()
731        })
732    }
733
734    pub fn get_active_cache_names(&self) -> Result<StringArray> {
735        debug_assert!(self.is_valid());
736        crate::ffi::get_active_cache_names(self)
737    }
738
739    pub fn get_cache_property_value(
740        &self,
741        cache_name: &str,
742        property: CacheProperty,
743    ) -> Result<i32> {
744        let cache_name = CString::new(cache_name)?;
745        crate::ffi::get_cache_property(self, &cache_name, property)
746    }
747
748    pub fn set_cache_property_value(
749        &self,
750        cache_name: &str,
751        property: CacheProperty,
752        value: i32,
753    ) -> Result<()> {
754        let cache_name = CString::new(cache_name)?;
755        crate::ffi::set_cache_property(self, &cache_name, property, value)
756    }
757
758    pub fn python_thread_interpreter_lock(&self, lock: bool) -> Result<()> {
759        debug_assert!(self.is_valid());
760        crate::ffi::python_thread_interpreter_lock(self, lock)
761    }
762    pub fn get_compositor_options(&self) -> Result<CompositorOptions> {
763        crate::ffi::get_compositor_options(self).map(CompositorOptions)
764    }
765
766    pub fn set_compositor_options(&self, options: &CompositorOptions) -> Result<()> {
767        crate::ffi::set_compositor_options(self, &options.0)
768    }
769
770    pub fn get_preset_names(&self, bytes: &[u8]) -> Result<Vec<String>> {
771        debug_assert!(self.is_valid());
772        let mut handles = vec![];
773        for handle in crate::ffi::get_preset_names(self, bytes)? {
774            let v = crate::stringhandle::get_string(handle, self)?;
775            handles.push(v);
776        }
777        Ok(handles)
778    }
779
780    pub fn start_performance_monitor_profile(&self, title: &str) -> Result<i32> {
781        let title = CString::new(title)?;
782        crate::ffi::start_performance_monitor_profile(self, &title)
783    }
784
785    pub fn stop_performance_monitor_profile(
786        &self,
787        profile_id: i32,
788        output_file: &str,
789    ) -> Result<()> {
790        let output_file = CString::new(output_file)?;
791        crate::ffi::stop_performance_monitor_profile(self, profile_id, &output_file)
792    }
793
794    pub fn get_job_status(&self, job_id: i32) -> Result<JobStatus> {
795        crate::ffi::get_job_status(self, job_id)
796    }
797}
798
799impl Drop for Session {
800    fn drop(&mut self) {
801        if Arc::strong_count(&self.inner) == 1 {
802            debug!("Dropping session pid: {:?}", self.server_pid());
803            if self.is_valid() {
804                if self.inner.options.cleanup {
805                    if let Err(e) = self.cleanup() {
806                        error!("Session cleanup failed in Drop: {}", e);
807                    }
808                }
809                if let Err(e) = crate::ffi::shutdown_session(self) {
810                    error!("Could not shutdown session in Drop: {}", e);
811                }
812                if let Err(e) = crate::ffi::close_session(self) {
813                    error!("Closing session failed in Drop: {}", e);
814                }
815            } else {
816                // The server should automatically delete the pipe file when closed successfully,
817                // but we could try a cleanup just in case.
818                debug!("Session was invalid in Drop!");
819                if let ConnectionType::ThriftPipe(f) = &self.inner.connection {
820                    let _ = std::fs::remove_file(f);
821                }
822            }
823        }
824    }
825}
826
827/// Connect to the engine process via a pipe file.
828/// If `timeout` is Some, function will try to connect to
829/// the server multiple times every 100ms until `timeout` is reached.
830/// Note: Default SessionOptions create a blocking session, non-threaded session,
831/// use [`SessionOptionsBuilder`] to configure this.
832pub fn connect_to_pipe(
833    pipe: impl AsRef<Path>,
834    options: Option<&SessionOptions>,
835    timeout: Option<Duration>,
836    pid: Option<u32>,
837) -> Result<Session> {
838    debug!("Connecting to Thrift session: {:?}", pipe.as_ref());
839    let c_str = utils::path_to_cstring(&pipe)?;
840    let pipe = pipe.as_ref().as_os_str().to_os_string();
841    let timeout = timeout.unwrap_or_default();
842    let options = options.cloned().unwrap_or_default();
843    let mut waited = Duration::from_secs(0);
844    let wait_ms = Duration::from_millis(100);
845    let handle = loop {
846        let mut last_error = None;
847        debug!("Trying to connect to pipe server");
848        match crate::ffi::new_thrift_piped_session(&c_str, &options.session_info.0) {
849            Ok(handle) => break handle,
850            Err(e) => {
851                last_error.replace(e);
852                std::thread::sleep(wait_ms);
853                waited += wait_ms;
854            }
855        }
856        if waited > timeout {
857            // last_error is guarantied to be Some().
858            return Err(last_error.unwrap()).context("Connection timeout");
859        }
860    };
861    let connection = ConnectionType::ThriftPipe(pipe);
862    let session = Session::new(handle, connection, options, pid);
863    session.initialize()?;
864    Ok(session)
865}
866
867pub fn connect_to_memory_server(
868    memory_name: &str,
869    options: Option<&SessionOptions>,
870    pid: Option<u32>,
871) -> Result<Session> {
872    let mem_name = String::from(memory_name);
873    let mem_name_cstr = CString::new(memory_name)?;
874
875    let options = options.cloned().unwrap_or_default();
876    let handle =
877        crate::ffi::new_thrift_shared_memory_session(&mem_name_cstr, &options.session_info.0)?;
878
879    let connection = ConnectionType::SharedMemory(mem_name);
880    let session = Session::new(handle, connection, options, pid);
881    session.initialize()?;
882    Ok(session)
883}
884
885/// Connect to the engine process via a Unix socket
886pub fn connect_to_socket(
887    addr: std::net::SocketAddrV4,
888    options: Option<&SessionOptions>,
889) -> Result<Session> {
890    debug!("Connecting to socket server: {:?}", addr);
891    let host = CString::new(addr.ip().to_string()).expect("SocketAddr->CString");
892    let options = options.cloned().unwrap_or_default();
893    let handle =
894        crate::ffi::new_thrift_socket_session(addr.port() as i32, &host, &options.session_info.0)?;
895    let connection = ConnectionType::ThriftSocket(addr);
896    let session = Session::new(handle, connection, options, None);
897    session.initialize()?;
898    Ok(session)
899}
900
901/// Create in-process session
902pub fn new_in_process(options: Option<&SessionOptions>) -> Result<Session> {
903    debug!("Creating new in-process session");
904    let options = options.cloned().unwrap_or_default();
905    let handle = crate::ffi::create_inprocess_session(&options.session_info.0)?;
906    let connection = ConnectionType::InProcess;
907    let session = Session::new(handle, connection, options, Some(std::process::id()));
908    session.initialize()?;
909    Ok(session)
910}
911
912/// Session options passed to session create functions like [`connect_to_pipe`]
913#[derive(Clone, Debug)]
914pub struct SessionOptions {
915    /// Session cook options
916    pub cook_opt: CookOptions,
917    /// Session connection options
918    pub session_info: SessionInfo,
919    /// Create a Threaded server connection
920    pub threaded: bool,
921    /// Cleanup session upon close
922    pub cleanup: bool,
923    pub log_file: Option<CString>,
924    /// Do not error out if session is already initialized
925    pub ignore_already_init: bool,
926    pub env_files: Option<CString>,
927    pub env_variables: Option<Vec<(String, String)>>,
928    pub otl_path: Option<CString>,
929    pub dso_path: Option<CString>,
930    pub img_dso_path: Option<CString>,
931    pub aud_dso_path: Option<CString>,
932}
933
934impl Default for SessionOptions {
935    fn default() -> Self {
936        SessionOptions {
937            cook_opt: CookOptions::default(),
938            session_info: SessionInfo::default(),
939            threaded: false,
940            cleanup: false,
941            log_file: None,
942            ignore_already_init: true,
943            env_files: None,
944            env_variables: None,
945            otl_path: None,
946            dso_path: None,
947            img_dso_path: None,
948            aud_dso_path: None,
949        }
950    }
951}
952
953#[derive(Default)]
954/// A build for SessionOptions.
955pub struct SessionOptionsBuilder {
956    cook_opt: CookOptions,
957    session_info: SessionInfo,
958    threaded: bool,
959    cleanup: bool,
960    log_file: Option<CString>,
961    ignore_already_init: bool,
962    env_variables: Option<Vec<(String, String)>>,
963    env_files: Option<CString>,
964    otl_path: Option<CString>,
965    dso_path: Option<CString>,
966    img_dso_path: Option<CString>,
967    aud_dso_path: Option<CString>,
968}
969
970impl SessionOptionsBuilder {
971    /// A list of Houdini environment file the Engine will load environment from.
972    pub fn houdini_env_files<I>(mut self, files: I) -> Self
973    where
974        I: IntoIterator,
975        I::Item: AsRef<str>,
976    {
977        let paths = utils::join_paths(files);
978        self.env_files
979            .replace(CString::new(paths).expect("Zero byte"));
980        self
981    }
982
983    /// Set the server environment variables. See also [`Session::set_server_var`].
984    /// The difference is this method writes out a temp file with the variables and
985    /// implicitly pass it to the engine (as if [`Self::houdini_env_files`] was used.
986    pub fn env_variables<'a, I, K, V>(mut self, variables: I) -> Self
987    where
988        I: Iterator<Item = &'a (K, V)>,
989        K: ToString + 'a,
990        V: ToString + 'a,
991    {
992        self.env_variables.replace(
993            variables
994                .map(|(k, v)| (k.to_string(), v.to_string()))
995                .collect(),
996        );
997        self
998    }
999
1000    /// Add search paths for the Engine to find HDAs.
1001    pub fn otl_search_paths<I>(mut self, paths: I) -> Self
1002    where
1003        I: IntoIterator,
1004        I::Item: AsRef<str>,
1005    {
1006        let paths = utils::join_paths(paths);
1007        self.otl_path
1008            .replace(CString::new(paths).expect("Zero byte"));
1009        self
1010    }
1011
1012    /// Add search paths for the Engine to find DSO plugins.
1013    pub fn dso_search_paths<P>(mut self, paths: P) -> Self
1014    where
1015        P: IntoIterator,
1016        P::Item: AsRef<str>,
1017    {
1018        let paths = utils::join_paths(paths);
1019        self.dso_path
1020            .replace(CString::new(paths).expect("Zero byte"));
1021        self
1022    }
1023
1024    /// Add search paths for the Engine to find image plugins.
1025    pub fn image_search_paths<P>(mut self, paths: P) -> Self
1026    where
1027        P: IntoIterator,
1028        P::Item: AsRef<str>,
1029    {
1030        let paths = utils::join_paths(paths);
1031        self.img_dso_path
1032            .replace(CString::new(paths).expect("Zero byte"));
1033        self
1034    }
1035
1036    /// Add search paths for the Engine to find audio files.
1037    pub fn audio_search_paths<P>(mut self, paths: P) -> Self
1038    where
1039        P: IntoIterator,
1040        P::Item: AsRef<str>,
1041    {
1042        let paths = utils::join_paths(paths);
1043        self.aud_dso_path
1044            .replace(CString::new(paths).expect("Zero byte"));
1045        self
1046    }
1047
1048    /// Do not error when connecting to a server process which has a session already initialized.
1049    pub fn ignore_already_init(mut self, ignore: bool) -> Self {
1050        self.ignore_already_init = ignore;
1051        self
1052    }
1053
1054    /// Pass session [`CookOptions`]
1055    pub fn cook_options(mut self, options: CookOptions) -> Self {
1056        self.cook_opt = options;
1057        self
1058    }
1059
1060    /// Session init options [`SessionInfo`]
1061    pub fn session_info(mut self, info: SessionInfo) -> Self {
1062        self.session_info = info;
1063        self
1064    }
1065
1066    /// Makes the server operate in threaded mode. See the official docs for more info.
1067    pub fn threaded(mut self, threaded: bool) -> Self {
1068        self.threaded = threaded;
1069        self
1070    }
1071
1072    /// Cleanup the server session when the last connection drops.
1073    pub fn cleanup_on_close(mut self, cleanup: bool) -> Self {
1074        self.cleanup = cleanup;
1075        self
1076    }
1077
1078    pub fn log_file(mut self, file: impl AsRef<Path>) -> Self {
1079        self.log_file = Some(utils::path_to_cstring(file).unwrap());
1080        self
1081    }
1082
1083    /// Consume the builder and return the result.
1084    pub fn build(mut self) -> SessionOptions {
1085        self.write_temp_env_file();
1086        SessionOptions {
1087            cook_opt: self.cook_opt,
1088            session_info: self.session_info,
1089            threaded: self.threaded,
1090            cleanup: self.cleanup,
1091            log_file: self.log_file,
1092            ignore_already_init: self.cleanup,
1093            env_files: self.env_files,
1094            env_variables: self.env_variables,
1095            otl_path: self.otl_path,
1096            dso_path: self.dso_path,
1097            img_dso_path: self.img_dso_path,
1098            aud_dso_path: self.aud_dso_path,
1099        }
1100    }
1101    // Helper function for Self::env_variables
1102    fn write_temp_env_file(&mut self) {
1103        use std::io::Write;
1104
1105        if let Some(ref env) = self.env_variables {
1106            let mut file = tempfile::Builder::new()
1107                .suffix("_hars.env")
1108                .tempfile()
1109                .expect("tempfile");
1110            for (k, v) in env.iter() {
1111                writeln!(file, "{}={}", k, v).expect("write to .env file");
1112            }
1113            let (_, tmp_file) = file.keep().expect("persistent tempfile");
1114            debug!(
1115                "Creating temporary environment file: {}",
1116                tmp_file.to_string_lossy()
1117            );
1118            let tmp_file = CString::new(tmp_file.to_string_lossy().to_string()).expect("null byte");
1119
1120            if let Some(old) = &mut self.env_files {
1121                let mut bytes = old.as_bytes_with_nul().to_vec();
1122                bytes.extend(tmp_file.into_bytes_with_nul());
1123                self.env_files
1124                    // SAFETY: the bytes vec was obtained from the two CString's above.
1125                    .replace(unsafe { CString::from_vec_with_nul_unchecked(bytes) });
1126            } else {
1127                self.env_files.replace(tmp_file);
1128            }
1129        }
1130    }
1131}
1132
1133impl SessionOptions {
1134    /// Create a [`SessionOptionsBuilder`]. Same as [`SessionOptionsBuilder::default()`].
1135    pub fn builder() -> SessionOptionsBuilder {
1136        SessionOptionsBuilder::default()
1137    }
1138}
1139
1140impl From<i32> for SessionState {
1141    fn from(s: i32) -> Self {
1142        match s {
1143            0 => SessionState::Ready,
1144            1 => SessionState::ReadyWithFatalErrors,
1145            2 => SessionState::ReadyWithCookErrors,
1146            3 => SessionState::StartingCook,
1147            4 => SessionState::Cooking,
1148            5 => SessionState::StartingLoad,
1149            6 => SessionState::Loading,
1150            7 => SessionState::Max,
1151            _ => panic!("Unmatched SessionState - {s}"),
1152        }
1153    }
1154}
1155
1156/// Spawn a new pipe Engine process and return its PID
1157pub fn start_engine_pipe_server(
1158    path: impl AsRef<Path>,
1159    log_file: Option<&str>,
1160    options: &ThriftServerOptions,
1161) -> Result<u32> {
1162    debug!("Starting named pipe server: {:?}", path.as_ref());
1163    let log_file = log_file.map(CString::new).transpose()?;
1164    let c_str = utils::path_to_cstring(path)?;
1165    crate::ffi::clear_connection_error()?;
1166    crate::ffi::start_thrift_pipe_server(&c_str, &options.0, log_file.as_deref())
1167}
1168
1169/// Spawn a new socket Engine server and return its PID
1170pub fn start_engine_socket_server(
1171    port: u16,
1172    log_file: Option<&str>,
1173    options: &ThriftServerOptions,
1174) -> Result<u32> {
1175    debug!("Starting socket server on port: {}", port);
1176    let log_file = log_file.map(CString::new).transpose()?;
1177    crate::ffi::clear_connection_error()?;
1178    crate::ffi::start_thrift_socket_server(port as i32, &options.0, log_file.as_deref())
1179}
1180
1181/// Start an interactive Houdini session with engine server embedded.
1182pub fn start_houdini_server(
1183    pipe_name: impl AsRef<str>,
1184    houdini_executable: impl AsRef<Path>,
1185    fx_license: bool,
1186) -> Result<Child> {
1187    std::process::Command::new(houdini_executable.as_ref())
1188        .arg(format!("-hess=pipe:{}", pipe_name.as_ref()))
1189        .arg(if fx_license {
1190            "-force-fx-license"
1191        } else {
1192            "-core"
1193        })
1194        .stdin(std::process::Stdio::null())
1195        .stdout(std::process::Stdio::null())
1196        .stderr(std::process::Stdio::null())
1197        .spawn()
1198        .map_err(HapiError::from)
1199}
1200
1201/// Spawn a new Engine server utilizing shared memory to transfer data.
1202pub fn start_shared_memory_server(
1203    memory_name: &str,
1204    options: &ThriftServerOptions,
1205    log_file: Option<&CStr>,
1206) -> Result<u32> {
1207    debug!("Starting shared memory server name: {memory_name}");
1208    let memory_name = CString::new(memory_name)?;
1209    crate::ffi::clear_connection_error()?;
1210    crate::ffi::start_thrift_shared_memory_server(&memory_name, &options.0, log_file.as_deref())
1211}
1212
1213/// A quick drop-in session, useful for on-off jobs
1214/// It starts a **single-threaded** shared memory server and initialize a session with default options
1215pub fn quick_session(options: Option<&SessionOptions>) -> Result<Session> {
1216    let server_options = ThriftServerOptions::default()
1217        .with_auto_close(true)
1218        .with_timeout_ms(4000f32)
1219        .with_verbosity(StatusVerbosity::Statusverbosity0);
1220    let rand_memory_name = format!("shared-memory-{}", utils::random_string(16));
1221    let log_file = match &options {
1222        None => None,
1223        Some(opt) => opt.log_file.as_deref(),
1224    };
1225    let pid = start_shared_memory_server(&rand_memory_name, &server_options, log_file)?;
1226    connect_to_memory_server(&rand_memory_name, options, Some(pid))
1227}