Skip to main content

workflow_node/
process.rs

1//!
2//! Module encapsulating [`Process`] API for running child process daemons under Node.js and NWJS
3//!
4use crate::child_process::{
5    ChildProcess, KillSignal, SpawnArgs, SpawnOptions, spawn_with_args_and_options,
6};
7use crate::error::Error;
8use crate::node_sys::*;
9use crate::result::Result;
10use borsh::{BorshDeserialize, BorshSerialize};
11use futures::{FutureExt, select};
12use serde::{Deserialize, Serialize};
13use std::collections::VecDeque;
14use std::path::PathBuf;
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::sync::{Arc, Mutex};
17use std::time::Duration;
18use wasm_bindgen::prelude::*;
19use workflow_core::channel::{Channel, Receiver, Sender, oneshot};
20use workflow_core::task::*;
21use workflow_core::time::Instant;
22use workflow_log::*;
23use workflow_task::*;
24use workflow_wasm::callback::*;
25use workflow_wasm::jserror::*;
26
27/// Version struct for standard version extraction from executables via `--version` output
28pub struct Version {
29    /// Major version component.
30    pub major: u64,
31    /// Minor version component.
32    pub minor: u64,
33    /// Patch version component.
34    pub patch: u64,
35    /// `true` if no version could be extracted (i.e. the version is unavailable).
36    pub none: bool,
37}
38
39impl Version {
40    /// Creates a new [`Version`] from its `major`, `minor`, and `patch` components.
41    pub fn new(major: u64, minor: u64, patch: u64) -> Version {
42        Version {
43            major,
44            minor,
45            patch,
46            none: false,
47        }
48    }
49
50    /// Creates a [`Version`] representing an unavailable/unknown version.
51    pub fn none() -> Version {
52        Version {
53            major: 0,
54            minor: 0,
55            patch: 0,
56            none: true,
57        }
58    }
59}
60
61impl std::fmt::Display for Version {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        if self.none {
64            write!(f, "n/a")
65        } else {
66            write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
67        }
68    }
69}
70
71/// Child process execution result
72pub struct ExecutionResult {
73    /// How the process ended (graceful exit or error).
74    pub termination: Termination,
75    /// Captured standard output of the process.
76    pub stdout: String,
77    /// Captured standard error of the process.
78    pub stderr: String,
79}
80
81impl ExecutionResult {
82    /// Returns `true` if the process terminated due to an error.
83    pub fn is_error(&self) -> bool {
84        matches!(self.termination, Termination::Error(_))
85    }
86}
87
88/// Describes how a child process execution ended.
89pub enum Termination {
90    /// The process exited gracefully with the given exit code.
91    Exit(u32),
92    /// The process terminated with an error described by the message.
93    Error(String),
94}
95
96/// Event emitted by a running [`Process`] and relayed over its event channel.
97#[derive(Debug, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize)]
98pub enum Event {
99    /// The child process has started.
100    Start,
101    /// The child process exited gracefully with the given exit code.
102    Exit(u32),
103    /// The child process terminated with an error described by the message.
104    Error(String),
105    /// A line of standard output emitted by the child process.
106    Stdout(String),
107    /// A line of standard error emitted by the child process.
108    Stderr(String),
109}
110
111/// Options for [`Process`] daemon runner
112pub struct Options {
113    /// Process arguments (the first element is the process binary file name / executable)
114    argv: Vec<String>,
115    /// Current working directory
116    cwd: Option<PathBuf>,
117    /// Automatic restart on exit
118    restart: bool,
119    /// Delay between automatic restarts
120    restart_delay: Duration,
121    /// This flag triggers forceful process termination after a given period of time.
122    /// At the termination, the process is issued a `SIGTERM` signal. If the process fails
123    /// to exit after a given period of time and `use_force` is enabled, the process
124    /// will be issued a `SIGKILL` signal, triggering it's immediate termination.
125    use_force: bool,
126    /// Delay period after which to issue a `SIGKILL` signal.
127    use_force_delay: Duration,
128    /// Events relay [`Event`] enum that carries events emitted by the child process
129    /// this includes stdout and stderr output, [`Event::Exit`] in case of a graceful
130    /// termination and [`Event::Error`] in case of an error.
131    events: Channel<Event>,
132    muted_buffer_capacity: Option<usize>,
133    mute: bool,
134}
135
136#[allow(clippy::too_many_arguments)]
137impl Options {
138    /// Creates a new set of [`Process`] daemon [`Options`] from the given
139    /// argument vector, working directory, restart and force-termination
140    /// settings, event channel, and mute configuration.
141    pub fn new(
142        argv: &[&str],
143        cwd: Option<PathBuf>,
144        restart: bool,
145        restart_delay: Option<Duration>,
146        use_force: bool,
147        use_force_delay: Option<Duration>,
148        events: Channel<Event>,
149        muted_buffer_capacity: Option<usize>,
150        mute: bool,
151    ) -> Options {
152        let argv = argv.iter().map(|s| s.to_string()).collect::<Vec<_>>();
153
154        Options {
155            argv,
156            cwd,
157            restart,
158            restart_delay: restart_delay.unwrap_or_default(),
159            use_force,
160            use_force_delay: use_force_delay.unwrap_or(Duration::from_millis(10_000)),
161            events,
162            muted_buffer_capacity,
163            mute,
164        }
165    }
166}
167
168impl Default for Options {
169    fn default() -> Self {
170        Self {
171            argv: Vec::new(),
172            cwd: None,
173            restart: true,
174            restart_delay: Duration::from_millis(3_000),
175            use_force: false,
176            use_force_delay: Duration::from_millis(10_000),
177            events: Channel::unbounded(),
178            muted_buffer_capacity: None,
179            mute: false,
180        }
181    }
182}
183
184struct Inner {
185    argv: Mutex<Vec<String>>,
186    cwd: Mutex<Option<PathBuf>>,
187    running: AtomicBool,
188    restart: AtomicBool,
189    restart_delay: Mutex<Duration>,
190    use_force: AtomicBool,
191    use_force_delay: Mutex<Duration>,
192    events: Channel<Event>,
193    proc: Arc<Mutex<Option<Arc<ChildProcess>>>>,
194    callbacks: CallbackMap,
195    start_time: Arc<Mutex<Option<Instant>>>,
196    mute: Arc<AtomicBool>,
197    muted_buffer_capacity: Option<usize>,
198    muted_buffer_stdout: Arc<Mutex<VecDeque<String>>>,
199    muted_buffer_stderr: Arc<Mutex<VecDeque<String>>>,
200}
201
202unsafe impl Send for Inner {}
203unsafe impl Sync for Inner {}
204
205impl Inner {
206    pub fn new(options: Options) -> Inner {
207        Inner {
208            argv: Mutex::new(options.argv),
209            cwd: Mutex::new(options.cwd),
210            running: AtomicBool::new(false),
211            restart: AtomicBool::new(options.restart),
212            restart_delay: Mutex::new(options.restart_delay),
213            use_force: AtomicBool::new(options.use_force),
214            use_force_delay: Mutex::new(options.use_force_delay),
215            events: options.events,
216            proc: Arc::new(Mutex::new(None)),
217            callbacks: CallbackMap::new(),
218            start_time: Arc::new(Mutex::new(None)),
219            mute: Arc::new(AtomicBool::new(options.mute)),
220            muted_buffer_capacity: options.muted_buffer_capacity,
221            muted_buffer_stdout: Arc::new(Mutex::new(VecDeque::default())),
222            muted_buffer_stderr: Arc::new(Mutex::new(VecDeque::default())),
223        }
224    }
225
226    fn program(&self) -> String {
227        self.argv.lock().unwrap().first().unwrap().clone()
228    }
229
230    fn args(&self) -> Vec<String> {
231        self.argv.lock().unwrap()[1..].to_vec()
232    }
233
234    fn cwd(&self) -> Option<PathBuf> {
235        self.cwd.lock().unwrap().clone()
236    }
237
238    pub fn uptime(&self) -> Option<Duration> {
239        if self.running.load(Ordering::SeqCst) {
240            self.start_time.lock().unwrap().map(|ts| ts.elapsed())
241        } else {
242            None
243        }
244    }
245
246    fn buffer_muted(&self, data: buffer::Buffer, muted_buffer: &Arc<Mutex<VecDeque<String>>>) {
247        let muted_buffer_capacity = self.muted_buffer_capacity.unwrap_or_default();
248        if muted_buffer_capacity > 0 {
249            let mut muted_buffer = muted_buffer.lock().unwrap();
250            let buffer = String::from(data.to_string(None, None, None));
251            let lines = buffer.split('\n').collect::<Vec<_>>();
252            for line in lines {
253                let line = line.trim();
254                if !line.is_empty() {
255                    muted_buffer.push_back(trim(line.to_string()));
256                }
257            }
258            while muted_buffer.len() > muted_buffer_capacity {
259                muted_buffer.pop_front();
260            }
261        }
262    }
263
264    fn drain_muted(
265        &self,
266        acc: &Arc<Mutex<VecDeque<String>>>,
267        sender: &Sender<Event>,
268        stdout: bool,
269    ) -> Result<()> {
270        let mut acc = acc.lock().unwrap();
271        if stdout {
272            acc.drain(..).for_each(|line| {
273                sender.try_send(Event::Stdout(line)).unwrap();
274            });
275        } else {
276            acc.drain(..).for_each(|line| {
277                sender.try_send(Event::Stderr(line)).unwrap();
278            });
279        }
280        Ok(())
281    }
282
283    pub fn toggle_mute(&self) -> Result<bool> {
284        if self.mute.load(Ordering::SeqCst) {
285            self.mute.store(false, Ordering::SeqCst);
286            self.drain_muted(&self.muted_buffer_stdout, &self.events.sender, true)?;
287            self.drain_muted(&self.muted_buffer_stderr, &self.events.sender, false)?;
288            Ok(false)
289        } else {
290            self.mute.store(true, Ordering::SeqCst);
291            Ok(true)
292        }
293    }
294
295    pub fn mute(&self, mute: bool) -> Result<()> {
296        if mute != self.mute.load(Ordering::SeqCst) {
297            self.mute.store(mute, Ordering::SeqCst);
298            if !mute {
299                self.drain_muted(&self.muted_buffer_stdout, &self.events.sender, true)?;
300                self.drain_muted(&self.muted_buffer_stderr, &self.events.sender, false)?;
301            }
302        }
303
304        Ok(())
305    }
306
307    pub async fn run(self: &Arc<Self>, stop: Receiver<()>) -> Result<()> {
308        if self.running.load(Ordering::SeqCst) {
309            return Err(Error::AlreadyRunning);
310        }
311
312        'outer: loop {
313            let termination = Channel::<Termination>::oneshot();
314
315            self.start_time.lock().unwrap().replace(Instant::now());
316
317            let proc = {
318                let program = self.program();
319                let args = &self.args();
320
321                let args: SpawnArgs = args.as_slice().into();
322                let options = SpawnOptions::new();
323                if let Some(cwd) = &self.cwd() {
324                    options.cwd(cwd.as_os_str().to_str().unwrap_or_else(|| {
325                        panic!("Process::exec_with_args(): invalid path: {}", cwd.display())
326                    }));
327                }
328
329                Arc::new(spawn_with_args_and_options(&program, &args, &options))
330            };
331
332            let this = self.clone();
333            let exit_sender = termination.sender.clone();
334            let exit = callback!(move |code: JsValue| {
335                let code = code.as_f64().unwrap_or_default() as u32;
336                this.events.sender.try_send(Event::Exit(code)).ok();
337                exit_sender
338                    .try_send(Termination::Exit(code))
339                    .expect("unable to send close notification");
340            });
341            proc.on("exit", exit.as_ref());
342            self.callbacks.retain(exit.clone())?;
343
344            let this = self.clone();
345            let error_sender = termination.sender.clone();
346            let error = callback!(move |err: JsValue| {
347                let msg = JsErrorData::from(err);
348                this.events
349                    .sender
350                    .try_send(Event::Error(msg.to_string()))
351                    .ok();
352                error_sender
353                    .try_send(Termination::Error(msg.to_string()))
354                    .expect("unable to send close notification");
355            });
356            proc.on("error", error.as_ref());
357            self.callbacks.retain(error.clone())?;
358
359            let this = self.clone();
360            let stdout_cb = callback!(move |data: buffer::Buffer| {
361                if this.mute.load(Ordering::SeqCst) {
362                    this.buffer_muted(data, &this.muted_buffer_stdout);
363                } else {
364                    this.events
365                        .sender
366                        .try_send(Event::Stdout(String::from(
367                            data.to_string(None, None, None),
368                        )))
369                        .unwrap();
370                }
371            });
372            proc.stdout().on("data", stdout_cb.as_ref());
373            self.callbacks.retain(stdout_cb)?;
374
375            let this = self.clone();
376            let stderr_cb = callback!(move |data: buffer::Buffer| {
377                if this.mute.load(Ordering::SeqCst) {
378                    this.buffer_muted(data, &this.muted_buffer_stderr);
379                } else {
380                    this.events
381                        .sender
382                        .try_send(Event::Stderr(String::from(
383                            data.to_string(None, None, None),
384                        )))
385                        .unwrap();
386                }
387            });
388            proc.stderr().on("data", stderr_cb.as_ref());
389            self.callbacks.retain(stderr_cb)?;
390
391            *self.proc.lock().unwrap() = Some(proc.clone());
392            self.running.store(true, Ordering::SeqCst);
393
394            self.events.sender.try_send(Event::Start).unwrap();
395
396            let kill = select! {
397                // process exited
398                e = termination.receiver.recv().fuse() => {
399
400                    // if exited with error, abort...
401                    if matches!(e,Ok(Termination::Error(_))) {
402                        break;
403                    }
404
405                    // if restart is not required, break
406                    if !self.restart.load(Ordering::SeqCst) {
407                        break;
408                    } else {
409                        // sleep and then restart
410                        let restart_delay = *self.restart_delay.lock().unwrap();
411                        select! {
412                            // slept well, aim to restart
413                            _ = sleep(restart_delay).fuse() => {
414                                false
415                            },
416                            // stop received while sleeping, break
417                            _ = stop.recv().fuse() => {
418                                break;
419                            }
420                        }
421                    }
422                },
423                // manual shutdown while the process is running
424                _ = stop.recv().fuse() => {
425                    true
426                }
427            };
428
429            if kill {
430                // start process termination
431                self.restart.store(false, Ordering::SeqCst);
432                proc.kill_with_signal(KillSignal::SIGTERM);
433                // if not using force, wait for process termination on SIGTERM
434                if !self.use_force.load(Ordering::SeqCst) {
435                    termination.receiver.recv().await?;
436                    break;
437                } else {
438                    // if using force, sleep and kill with SIGKILL
439                    let use_force_delay = sleep(*self.use_force_delay.lock().unwrap());
440                    select! {
441                        // process exited normally, break
442                        _ = termination.receiver.recv().fuse() => {
443                            break 'outer;
444                        },
445                        // post SIGKILL and wait for exit
446                        _ = use_force_delay.fuse() => {
447                            proc.kill_with_signal(KillSignal::SIGKILL);
448                            termination.receiver.recv().await?;
449                            break 'outer;
450                        },
451                    }
452                }
453            }
454        }
455
456        self.callbacks.clear();
457        *self.proc.lock().unwrap() = None;
458        self.running.store(false, Ordering::SeqCst);
459
460        Ok(())
461    }
462}
463
464/// The [`Process`] class facilitating execution of a Child Process in Node.js or NWJS
465/// environments. This wrapper runs the child process as a daemon, restarting it if
466/// it fails.  The process provides `stdout` and `stderr` output as channel [`Receiver`]
467/// channels, allowing for a passive capture of the process console output.
468#[derive(Clone)]
469pub struct Process {
470    inner: Arc<Inner>,
471    task: Arc<Task<Arc<Inner>, ()>>,
472}
473
474unsafe impl Send for Process {}
475unsafe impl Sync for Process {}
476
477impl Process {
478    /// Create new process instance
479    pub fn new(options: Options) -> Process {
480        let inner = Arc::new(Inner::new(options));
481
482        let task = task!(|inner: Arc<Inner>, stop| async move {
483            inner.run(stop).await.ok();
484        });
485
486        Process {
487            inner,
488            task: Arc::new(task),
489        }
490    }
491
492    /// Creates a [`Process`] that runs the executable at `path` a single time
493    /// (no automatic restart) with default options.
494    pub fn new_once(path: &str) -> Process {
495        let options = Options::new(
496            &[path],
497            None,
498            false,
499            None,
500            false,
501            // None,
502            // None,
503            None,
504            Channel::unbounded(),
505            None,
506            false,
507        );
508
509        Self::new(options)
510    }
511
512    /// Runs the executable at `path` with `--version` and parses the resulting [`Version`].
513    pub async fn version(path: &str) -> Result<Version> {
514        version(path).await
515    }
516
517    /// Returns `true` if the child process is currently running.
518    pub fn is_running(&self) -> bool {
519        self.inner.running.load(Ordering::SeqCst)
520    }
521
522    /// Enables or disables muting of the process stdout/stderr output relay.
523    pub fn mute(&self, mute: bool) -> Result<()> {
524        self.inner.mute(mute)
525    }
526
527    /// Toggles muting of the process output buffer, returning the new mute state.
528    pub fn toggle_mute(&self) -> Result<bool> {
529        self.inner.toggle_mute()
530    }
531
532    /// Returns how long the process has been running, or `None` if it is not running.
533    pub fn uptime(&self) -> Option<Duration> {
534        self.inner.uptime()
535    }
536
537    /// Obtain a clone of the channel [`Receiver`] that captures
538    /// [`Event`] of the underlying process.
539    pub fn events(&self) -> Receiver<Event> {
540        self.inner.events.receiver.clone()
541    }
542
543    /// Replace the process arguments used for the next process (re)start.
544    pub fn replace_argv(&self, argv: Vec<String>) {
545        *self.inner.argv.lock().unwrap() = argv;
546    }
547
548    /// Run the process in the background.  Spawns an async task that
549    /// monitors the process, capturing its output and restarting
550    /// the process if it exits prematurely.
551    pub fn run(&self) -> Result<()> {
552        self.task.run(self.inner.clone())?;
553        Ok(())
554    }
555
556    /// Issue a `SIGKILL` signal, terminating the process immediately.
557    pub fn kill(&self) -> Result<()> {
558        if !self.inner.running.load(Ordering::SeqCst) {
559            Err(Error::NotRunning)
560        } else if let Some(proc) = self.inner.proc.lock().unwrap().as_ref() {
561            self.inner.restart.store(false, Ordering::SeqCst);
562            proc.kill_with_signal(KillSignal::SIGKILL);
563            Ok(())
564        } else {
565            Err(Error::ProcIsAbsent)
566        }
567    }
568
569    /// Issue a `SIGTERM` signal causing the process to exit. The process
570    /// will be restarted by the monitoring task.
571    pub fn restart(&self) -> Result<()> {
572        if !self.inner.running.load(Ordering::SeqCst) {
573            Err(Error::NotRunning)
574        } else if let Some(proc) = self.inner.proc.lock().unwrap().as_ref() {
575            proc.kill_with_signal(KillSignal::SIGTERM);
576            Ok(())
577        } else {
578            Err(Error::ProcIsAbsent)
579        }
580    }
581
582    /// Stop the process by disabling auto-restart and issuing
583    /// a `SIGTERM` signal. Returns `Ok(())` if the process
584    /// is not running.
585    pub fn stop(&self) -> Result<()> {
586        if self.inner.running.load(Ordering::SeqCst) {
587            self.inner.restart.store(false, Ordering::SeqCst);
588            self.task.stop()?;
589        }
590
591        Ok(())
592    }
593
594    /// Join the process like you would a thread - this async
595    /// function blocks until the process exits.
596    pub async fn join(&self) -> Result<()> {
597        if self.task.is_running() {
598            self.task.join().await?;
599        }
600        Ok(())
601    }
602
603    /// Stop the process and block until it exits.
604    pub async fn stop_and_join(&self) -> Result<()> {
605        self.stop()?;
606        self.join().await?;
607        Ok(())
608    }
609}
610
611/// Execute the process single time with custom command-line arguments.
612/// Useful to obtain a version via `--version` or perform single-task
613/// executions - not as a daemon.
614pub async fn exec(
615    // &self,
616    argv: &[&str],
617    cwd: Option<PathBuf>,
618) -> Result<ExecutionResult> {
619    let proc = *argv.first().unwrap();
620
621    let args: SpawnArgs = argv[1..].into();
622    let options = SpawnOptions::new();
623    if let Some(cwd) = cwd {
624        options.cwd(cwd.as_os_str().to_str().unwrap_or_else(|| {
625            panic!("Process::exec_with_args(): invalid path: {}", cwd.display())
626        }));
627    }
628
629    let termination = Channel::<Termination>::oneshot();
630    let (stdout_tx, stdout_rx) = oneshot();
631    let (stderr_tx, stderr_rx) = oneshot();
632
633    let cp = spawn_with_args_and_options(proc, &args, &options);
634
635    let exit = termination.sender.clone();
636    let exit = callback!(move |code: u32| {
637        exit.try_send(Termination::Exit(code))
638            .expect("unable to send close notification");
639    });
640    cp.on("exit", exit.as_ref());
641
642    let error = termination.sender.clone();
643    let error = callback!(move |err: JsValue| {
644        error
645            .try_send(Termination::Error(format!("{:?}", err)))
646            .expect("unable to send close notification");
647    });
648    cp.on("error", error.as_ref());
649
650    let stdout_cb = callback!(move |data: buffer::Buffer| {
651        stdout_tx
652            .try_send(String::from(data.to_string(None, None, None)))
653            .expect("unable to send stdout data");
654    });
655    cp.stdout().on("data", stdout_cb.as_ref());
656
657    let stderr_cb = callback!(move |data: buffer::Buffer| {
658        stderr_tx
659            .try_send(String::from(data.to_string(None, None, None)))
660            .expect("unable to send stderr data");
661    });
662    cp.stderr().on("data", stderr_cb.as_ref());
663
664    let termination = termination.recv().await?;
665
666    let mut stdout = String::new();
667    for _ in 0..stdout_rx.len() {
668        stdout.push_str(&stdout_rx.try_recv()?);
669    }
670
671    let mut stderr = String::new();
672    for _ in 0..stderr_rx.len() {
673        stderr.push_str(&stdout_rx.try_recv()?);
674    }
675
676    Ok(ExecutionResult {
677        termination,
678        stdout,
679        stderr,
680    })
681}
682
683/// Obtain the process version information by running it with `--version` argument.
684pub async fn version(proc: &str) -> Result<Version> {
685    let text = exec([proc, "--version"].as_slice(), None).await?.stdout;
686    let vstr = if let Some(vstr) = text.split_whitespace().last() {
687        vstr
688    } else {
689        return Ok(Version::none());
690    };
691
692    let v = vstr
693        .split('.')
694        .flat_map(|v| v.parse::<u64>())
695        .collect::<Vec<_>>();
696
697    if v.len() != 3 {
698        return Ok(Version::none());
699    }
700
701    Ok(Version::new(v[0], v[1], v[2]))
702}
703
704/// Strips a single trailing newline (`\n` or `\r\n`) from the given string.
705pub fn trim(mut s: String) -> String {
706    // let mut s = String::from(self);
707    if s.ends_with('\n') {
708        s.pop();
709        if s.ends_with('\r') {
710            s.pop();
711        }
712    }
713    s
714}
715
716// #[wasm_bindgen]
717/// Manual test harness that spawns and runs a sample child process daemon.
718pub async fn test_child_process() {
719    log_info!("running rust test() fn");
720    workflow_wasm::panic::init_console_panic_hook();
721
722    let proc = Process::new(Options::new(
723        &["/Users/aspect/dev/kaspa-dev/kaspad/kaspad"],
724        None,
725        true,
726        Some(Duration::from_millis(3000)),
727        true,
728        Some(Duration::from_millis(100)),
729        Channel::unbounded(),
730        None,
731        false,
732    ));
733    // futures::task
734    let task = task!(|events: Receiver<Event>, stop: Receiver<()>| async move {
735        loop {
736            select! {
737                v = events.recv().fuse() => {
738                    if let Ok(v) = v {
739                        log_info!("| {:?}",v);
740                    }
741                },
742                _ = stop.recv().fuse() => {
743                    log_info!("stop...");
744                    break;
745                }
746            }
747            log_info!("in loop");
748        }
749    });
750    task.run(proc.events()).expect("task.run()");
751
752    proc.run().expect("proc.run()");
753
754    sleep(Duration::from_millis(5_000)).await;
755
756    proc.stop_and_join()
757        .await
758        .expect("proc.stop_and_join() failure");
759    task.stop_and_join()
760        .await
761        .expect("task.stop_and_join() failure");
762}