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}