Skip to main content

rs_teststand/execution/
thread.rs

1//! One thread of a running execution.
2
3use rs_teststand_sys::{Dispatch, Value};
4
5use crate::Error;
6use crate::dispids::thread;
7
8/// A thread within an [`Execution`](crate::Execution).
9///
10/// Every execution has at least one. A sequence that starts a parallel or
11/// asynchronous step gains more, which is why a front end tracks progress per
12/// thread rather than per execution.
13#[derive(Debug)]
14pub struct Thread {
15    dispatch: Box<dyn Dispatch>,
16}
17
18impl Thread {
19    /// Wraps a dispatch handle returned by the engine.
20    pub(crate) fn new(dispatch: Box<dyn Dispatch>) -> Self {
21        Self { dispatch }
22    }
23
24    /// The thread as a property tree (`Thread.AsPropertyObject`).
25    ///
26    /// # Errors
27    /// [`Error`] if the COM call fails or returns an unexpected type.
28    pub fn as_property_object(&self) -> Result<crate::PropertyObject, Error> {
29        Ok(crate::PropertyObject::new(
30            self.dispatch
31                .call(thread::AS_PROPERTY_OBJECT, &[])?
32                .into_object()?,
33        ))
34    }
35
36    /// The thread's identifier within its execution (`Thread.Id`).
37    ///
38    /// # Errors
39    /// [`Error`] if the COM call fails or returns an unexpected type.
40    pub fn id(&self) -> Result<i32, Error> {
41        Ok(self.dispatch.get(thread::ID)?.as_i32()?)
42    }
43
44    /// An identifier unique across the whole session (`Thread.UniqueThreadId`).
45    ///
46    /// [`id`](Self::id) only distinguishes threads within one execution, so a
47    /// host serving several executions keys on this instead.
48    ///
49    /// # Errors
50    /// [`Error`] if the COM call fails or returns an unexpected type.
51    pub fn unique_thread_id(&self) -> Result<String, Error> {
52        Ok(self.dispatch.get(thread::UNIQUE_THREAD_ID)?.into_string()?)
53    }
54
55    /// The name a front end shows for this thread (`Thread.DisplayName`).
56    ///
57    /// # Errors
58    /// [`Error`] if the COM call fails or returns an unexpected type.
59    pub fn display_name(&self) -> Result<String, Error> {
60        Ok(self.dispatch.get(thread::DISPLAY_NAME)?.into_string()?)
61    }
62
63    /// How deep the call stack currently is (`Thread.CallStackSize`).
64    ///
65    /// Index `0` is the innermost frame, which is what
66    /// [`get_sequence_context`](Self::get_sequence_context) usually wants.
67    ///
68    /// # Errors
69    /// [`Error`] if the COM call fails or returns an unexpected type.
70    pub fn call_stack_size(&self) -> Result<i32, Error> {
71        Ok(self.dispatch.get(thread::CALL_STACK_SIZE)?.as_i32()?)
72    }
73
74    /// Whether a requested suspend has actually taken effect
75    /// (`Thread.ExternallySuspended`).
76    ///
77    /// [`Execution::suspend`](crate::Execution::suspend) only *asks*. This is
78    /// how a caller learns the engine has acted, and the reason a suspend
79    /// followed immediately by a resume is a race: the resume can arrive before
80    /// the suspend takes hold, leaving the run stopped for good.
81    ///
82    /// # Errors
83    /// [`Error`] if the COM call fails or returns an unexpected type.
84    pub fn externally_suspended(&self) -> Result<bool, Error> {
85        Ok(self.dispatch.get(thread::EXTERNALLY_SUSPENDED)?.as_bool()?)
86    }
87
88    /// The execution this thread belongs to (`Thread.Execution`).
89    ///
90    /// # Errors
91    /// [`Error`] if the COM call fails or returns an unexpected type.
92    pub fn execution(&self) -> Result<crate::Execution, Error> {
93        Ok(crate::Execution::new(
94            self.dispatch.get(thread::EXECUTION)?.into_object()?,
95        ))
96    }
97
98    /// The sequence context at a call-stack frame (`Thread.GetSequenceContext`).
99    ///
100    /// Index `0` is the innermost frame, the sequence running right now.
101    ///
102    /// This is the route to `RunState`, `Locals`, `FileGlobals` and
103    /// `StationGlobals` for a live run. **Mind what may outlive the run:** NI
104    /// documents `StationGlobals`, `RunState.InitialSelection`,
105    /// `RunState.SequenceFile` and `RunState.ProcessModelClient` as existing
106    /// before and persisting after the execution, and everything else in the
107    /// context as belonging to it. `FileGlobals` in particular is the run's own
108    /// copy, so keeping one past the execution holds a reference to a finished
109    /// run, read what is needed while it is alive, or take the edit-time
110    /// defaults from
111    /// [`SequenceFile::file_globals_default_values`](crate::SequenceFile::file_globals_default_values)
112    /// instead.
113    ///
114    /// The engine declares **two** parameters: the call stack index, and an
115    /// `[out]` frame id (`VT_BYREF | VT_I4`). Both must be present in the call
116    /// even though only the first carries information, supplying one gives
117    /// `DISP_E_BADPARAMCOUNT`. The second is passed empty, which the engine
118    /// accepts as "no output wanted"; reading the frame id back would need
119    /// byref support this crate does not have yet.
120    ///
121    /// # Errors
122    /// [`Error`] if the index is out of range or the COM call fails.
123    pub fn get_sequence_context(
124        &self,
125        call_stack_index: i32,
126    ) -> Result<crate::execution::SequenceContext, Error> {
127        Ok(crate::execution::SequenceContext::new(
128            self.dispatch
129                .call(
130                    thread::GET_SEQUENCE_CONTEXT,
131                    &[Value::I32(call_stack_index), Value::Empty],
132                )?
133                .into_object()?,
134        ))
135    }
136
137    /// Asks this thread to stop again once the next step finishes
138    /// (`Thread.SetStepOver`).
139    ///
140    /// Arms a one-shot stop; it does not start the thread moving. Pair it with
141    /// [`resume`](Self::resume) to actually step.
142    ///
143    /// The engine also has `Execution.StepOver`, which arms and resumes in one
144    /// call but always acts on the foreground thread. This crate does not wrap
145    /// it yet. Going through the thread is the only way to say which thread to
146    /// step, which is what a host serving a panel with several threads needs.
147    ///
148    /// # Errors
149    /// [`Error`] if the COM call fails.
150    pub fn set_step_over(&self) -> Result<(), Error> {
151        self.dispatch.call(thread::SET_STEP_OVER, &[])?;
152        Ok(())
153    }
154
155    /// Arms a stop at the first step inside whatever the next step calls
156    /// (`Thread.SetStepInto`).
157    ///
158    /// Does not resume the thread. See [`set_step_over`](Self::set_step_over).
159    ///
160    /// # Errors
161    /// [`Error`] if the COM call fails.
162    pub fn set_step_into(&self) -> Result<(), Error> {
163        self.dispatch.call(thread::SET_STEP_INTO, &[])?;
164        Ok(())
165    }
166
167    /// Arms a stop once the current sequence returns to its caller
168    /// (`Thread.SetStepOut`).
169    ///
170    /// Does not resume the thread. See [`set_step_over`](Self::set_step_over).
171    ///
172    /// # Errors
173    /// [`Error`] if the COM call fails.
174    pub fn set_step_out(&self) -> Result<(), Error> {
175        self.dispatch.call(thread::SET_STEP_OUT, &[])?;
176        Ok(())
177    }
178
179    /// Clears a stop armed by one of the `set_step_*` members
180    /// (`Thread.ClearTemporaryBreakpoint`).
181    ///
182    /// Only the temporary one. Breakpoints set on a step are untouched.
183    ///
184    /// # Errors
185    /// [`Error`] if the COM call fails.
186    pub fn clear_temporary_breakpoint(&self) -> Result<(), Error> {
187        self.dispatch
188            .call(thread::CLEAR_TEMPORARY_BREAKPOINT, &[])?;
189        Ok(())
190    }
191
192    /// Clears the run-time error sitting on the current step
193    /// (`Thread.ClearCurrentRTE`).
194    ///
195    /// Resets the step's recorded error so the thread carries on as though it
196    /// had not happened. This is how a host answers a run-time error by
197    /// ignoring it, rather than letting the station's configured response
198    /// decide.
199    ///
200    /// Only meaningful while a run-time error is actually outstanding. Called
201    /// on a thread that has none, a live engine answers
202    /// `TS_Err_UnexpectedType` rather than doing nothing, so a host should call
203    /// this in response to a run-time error and not speculatively.
204    ///
205    /// # Errors
206    /// [`Error`] if there is no run-time error to clear, or the COM call fails.
207    pub fn clear_current_rte(&self) -> Result<(), Error> {
208        self.dispatch.call(thread::CLEAR_CURRENT_RTE, &[])?;
209        Ok(())
210    }
211
212    /// Hands whatever results have piled up to the post-results callbacks now
213    /// (`Thread.FlushPostResults`).
214    ///
215    /// Results are normally batched. A host that wants a client to see them
216    /// sooner can force the handover; with nothing accumulated the call does
217    /// nothing, so it is safe to make on a timer.
218    ///
219    /// # Errors
220    /// [`Error`] if the COM call fails.
221    pub fn flush_post_results(&self) -> Result<(), Error> {
222        self.dispatch.call(thread::FLUSH_POST_RESULTS, &[])?;
223        Ok(())
224    }
225
226    /// Whether the run is about to step into the current step's code module
227    /// (`Thread.WillStepIntoModule`).
228    ///
229    /// Read this only from a pre-step substep. That is the one place it means
230    /// anything: it reports true there when the run will suspend inside the
231    /// module belonging to the step that owns the substep. Read from anywhere
232    /// else, an expression or a step's own code module, the engine answers
233    /// false regardless of what the run is doing, so a false here is not
234    /// evidence that stepping is off.
235    ///
236    /// # Errors
237    /// [`Error`] if the COM call fails or returns an unexpected type.
238    pub fn will_step_into_module(&self) -> Result<bool, Error> {
239        Ok(self
240            .dispatch
241            .get(thread::WILL_STEP_INTO_MODULE)?
242            .as_bool()?)
243    }
244
245    /// Waits for this thread to finish (`Thread.WaitForEnd`).
246    ///
247    /// Returns `true` when the thread ended and `false` when the wait ran out
248    /// first. Pass `-1` for `milliseconds` to wait with no limit, which is
249    /// worth avoiding in a host that has to stay answerable.
250    ///
251    /// `process_windows_messages` decides whether the calling thread keeps
252    /// pumping while it waits. A host on a COM apartment should pass `true`:
253    /// stop pumping and the engine cannot deliver into this apartment, so a
254    /// wait meant to end can sit until the timeout instead.
255    ///
256    /// Waiting is not the only obligation. This pumps but does not drain the
257    /// engine's message queue, so a sequence posting a synchronous message
258    /// stays blocked on a host that only waits here. Draining the queue is
259    /// separate work.
260    ///
261    /// The two optional arguments the engine accepts, a step to store results
262    /// in and a calling sequence context, are not exposed. Both take an object
263    /// this crate has no route to from outside a running sequence.
264    ///
265    /// # Errors
266    /// [`Error`] if the COM call fails or returns an unexpected type.
267    pub fn wait_for_end(
268        &self,
269        milliseconds: i32,
270        process_windows_messages: bool,
271    ) -> Result<bool, Error> {
272        Ok(self
273            .dispatch
274            .call(
275                thread::WAIT_FOR_END,
276                &[
277                    Value::I32(milliseconds),
278                    Value::Bool(process_windows_messages),
279                    // The two optional arguments, left absent.
280                    Value::Empty,
281                    Value::Empty,
282                ],
283            )?
284            .as_bool()?)
285    }
286
287    /// How this thread answers a request to terminate its execution
288    /// (`Thread.TerminationOption`).
289    ///
290    /// The inner [`Result`] carries the raw number when the engine names an
291    /// option this build does not, rather than mapping it onto a neighbour.
292    ///
293    /// # Errors
294    /// [`Error`] if the COM call fails or returns an unexpected type.
295    pub fn termination_option(&self) -> Result<Result<crate::ThreadTerminationOption, i32>, Error> {
296        let raw = self.dispatch.get(thread::TERMINATION_OPTION)?.as_i32()?;
297        Ok(crate::ThreadTerminationOption::from_bits(raw))
298    }
299
300    /// Chooses how this thread answers a terminate request
301    /// (`Thread.TerminationOption`).
302    ///
303    /// # Errors
304    /// [`Error`] if the COM call fails.
305    pub fn set_termination_option(
306        &self,
307        option: crate::ThreadTerminationOption,
308    ) -> Result<(), Error> {
309        self.dispatch
310            .put(thread::TERMINATION_OPTION, Value::I32(option.bits()))?;
311        Ok(())
312    }
313
314    /// Starts this thread running (`Thread.Resume`).
315    ///
316    /// Releases a thread created suspended, which is how a sequence call step
317    /// can hand one back before it runs. It is also the second half of a step,
318    /// once `set_step_over`, `set_step_into` or `set_step_out` has armed one.
319    ///
320    /// **This does not continue a run stopped at a breakpoint.** Measured
321    /// against a live engine: after a breakpoint stop, calling this leaves the
322    /// run where it is and the execution never ends. Use
323    /// [`Execution::resume`](crate::Execution::resume) for that, which
324    /// continued the same run in about 200 ms.
325    ///
326    /// # Errors
327    /// [`Error`] if the COM call fails.
328    pub fn resume(&self) -> Result<(), Error> {
329        self.dispatch.call(thread::RESUME, &[])?;
330        Ok(())
331    }
332
333    /// Sends a message to whatever is watching this execution
334    /// (`Thread.PostUIMessageEx`).
335    ///
336    /// The outbound half of a two-way bridge: the sequence reports, a host
337    /// forwards.
338    ///
339    /// Pass `synchronous = true` in the ordinary case. It blocks the posting
340    /// thread until the host acknowledges, which is what applies backpressure:
341    /// posting faster than the host drains grows the queue without bound and
342    /// eventually makes the host unresponsive. The cost is that a host which
343    /// never drains its queue stalls the sequence instead, so a host owes the
344    /// engine an [`acknowledge`](crate::UIMessage::acknowledge) for every
345    /// message it takes.
346    ///
347    /// `activex_data` is the structured payload. Pass a container and the host
348    /// reads the whole tree back from
349    /// [`UIMessage::activex_data`](crate::UIMessage::activex_data), instead of
350    /// the two of them agreeing on how to pack fields into `string_data`. Pass
351    /// `None` to leave the slot empty, which is a null object reference rather
352    /// than an absent argument.
353    ///
354    /// A message a host defines for itself should use a code at or above
355    /// [`UIMessageCode::USER_MESSAGE_BASE`](crate::UIMessageCode::USER_MESSAGE_BASE),
356    /// which is the range the engine reserves for callers.
357    ///
358    /// # Errors
359    /// [`Error`] if the COM call fails.
360    pub fn post_ui_message_ex(
361        &self,
362        event_code: i32,
363        numeric_data: f64,
364        string_data: &str,
365        activex_data: Option<&crate::PropertyObject>,
366        synchronous: bool,
367    ) -> Result<(), Error> {
368        // The fourth parameter is an object reference, so "no data" is a null
369        // *object*, not an absent argument or a boolean. A boolean in its place
370        // corrupts the call, the same trap `Engine.NewUser` has.
371        let payload = object_argument(activex_data)?;
372        self.dispatch.call(
373            thread::POST_UI_MESSAGE_EX,
374            &[
375                Value::I32(event_code),
376                Value::F64(numeric_data),
377                Value::Str(string_data.to_owned()),
378                payload,
379                Value::Bool(synchronous),
380            ],
381        )?;
382        Ok(())
383    }
384}
385
386impl Thread {
387    /// Lends the underlying handle for a call that takes a thread reference.
388    pub(crate) fn duplicate_dispatch(&self) -> Option<Box<dyn Dispatch>> {
389        self.dispatch.duplicate()
390    }
391}
392
393/// Turns an optional wrapper into the argument its slot expects.
394///
395/// The type library declares this parameter `VT_UNKNOWN` rather than
396/// `VT_DISPATCH`. Passing a dispatch object is accepted because every
397/// `IDispatch` is an `IUnknown`, and it is what the engine hands back when the
398/// message is read again.
399pub(crate) fn object_argument(object: Option<&crate::PropertyObject>) -> Result<Value, Error> {
400    object.map_or_else(
401        || Ok(Value::NullObject),
402        |property_object| {
403            property_object
404                .duplicate_dispatch()
405                .map(Value::Object)
406                .ok_or(Error::UnexpectedType {
407                    expected: "a live property object",
408                    actual: "a test fake with no COM identity",
409                })
410        },
411    )
412}