• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jtmoon79 / super-speedy-syslog-searcher / 30326158672

28 Jul 2026 03:31AM UTC coverage: 70.857% (+1.9%) from 68.982%
30326158672

push

github

jtmoon79
(LIB) bump macro-string 0.3.0

19964 of 28175 relevant lines covered (70.86%)

1699356.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.8
/src/python/pyrunner.rs
1
// src/python/pyrunner.rs
2

3
//! Runs a Python process instance. It communicates with the Python process
4
//! over threaded `PipeStreamReader`s connected to stdout, stderr, and stdin.
5
//! It uses `std::process::Child` to start and manage the Python process.
6

7
use std::cmp::{
8
    max,
9
    min,
10
};
11
use std::collections::{
12
    HashSet,
13
    VecDeque,
14
};
15
use std::env;
16
use std::io::{
17
    Error,
18
    ErrorKind,
19
    Read,
20
    Result,
21
    Write,
22
    stderr,
23
    stdout,
24
};
25
use std::path::PathBuf;
26
use std::process::{
27
    Child,
28
    Command,
29
    Stdio,
30
};
31
use std::sync::{
32
    atomic::{
33
        AtomicBool,
34
        Ordering,
35
    },
36
    RwLock,
37
};
38
use std::thread;
39
use std::time::{
40
    Duration,
41
    Instant,
42
};
43

44
use ::crossbeam_channel::{
45
    Sender,
46
    Receiver,
47
    RecvError,
48
    RecvTimeoutError,
49
    Select,
50
};
51
use ::lazy_static::lazy_static;
52
use ::memchr::memmem::Finder as memchr_Finder;
53
use ::once_cell::sync::OnceCell;
54
use ::pathsearch::find_executable_in_path;
55
use ::shell_escape::escape;
56
#[allow(unused_imports)]
57
use ::si_trace_print::{
58
    defñ,
59
    defn,
60
    defo,
61
    defx,
62
    def1ñ,
63
    def1n,
64
    def1o,
65
    def1x,
66
    def2ñ,
67
    def2n,
68
    def2o,
69
    def2x,
70
    e,
71
    ef1n,
72
    ef1o,
73
    ef1x,
74
    ef1ñ,
75
    ef2n,
76
    ef2o,
77
    ef2x,
78
    ef2ñ,
79
};
80

81
use crate::{
82
    de_err,
83
    de_wrn,
84
    debug_assert_none,
85
    debug_panic,
86
};
87
use crate::common::{
88
    Bytes,
89
    Count,
90
    FPath,
91
    PathId,
92
    threadid_to_u64,
93
    summary_stat,
94
};
95
#[cfg(any(debug_assertions, test))]
96
use crate::debug::printers::buffer_to_string_noraw;
97
use crate::readers::filehandlemanager::{
98
    FILE_HANDLE_MANAGER,
99
    FileHandleUnmanaged,
100
    FileHandleRole,
101
};
102
use crate::readers::helpers::path_to_fpath;
103
use crate::python::venv::venv_path;
104

105
/// Python process exit result
106
pub type ExitStatus = std::process::ExitStatus;
107

108
/// Size of pipe read/write buffers in bytes
109
pub type PipeSz = usize;
110

111
/// Delimiter byte used to separate chunks of data read from the Python process
112
pub type ChunkDelimiter = u8;
113

114
/// Names of possible Python executables that could be found in path
115
pub const PYTHON_NAMES: [&str; 13] = [
116
    "python3",
117
    "python",
118
    "python3.exe",
119
    "python.exe",
120
    "python37",
121
    "python38",
122
    "python39",
123
    "python310",
124
    "python311",
125
    "python312",
126
    "python313",
127
    "pypy3",
128
    "pypy",
129
];
130
/// Possible subdirectories within a Python installation where the Python
131
/// interpreter executable may be found
132
pub const PYTHON_SUBDIRS: [&str; 3] = [
133
    "bin",
134
    "Scripts",
135
    "",
136
];
137

138
pub const PROMPT_DEFAULT: &str = "$ ";
139

140
pub const CHANNEL_CAPACITY: usize = 16;
141

142
/// Environment variable that refers to the exact path to a Python interpreter
143
/// executable
144
pub const PYTHON_ENV: &str = "S4_PYTHON";
145

146
/// default timeout for Pipe `recv_timeout` when reading from the child Python processes
147
pub const RECV_TIMEOUT: Duration = Duration::from_millis(5);
148

149
/// cached Python path found in environment variable `S4_PYTHON`.
150
/// set in `find_python_executable`
151
#[allow(non_upper_case_globals)]
152
pub static PythonPathEnv: OnceCell<Option<FPath>> = OnceCell::new();
153
/// cached Python path found in path, set in `find_python_executable`
154
#[allow(non_upper_case_globals)]
155
pub static PythonPathPath: OnceCell<Option<FPath>> = OnceCell::new();
156
/// cached Python path in s4 venv, set in `find_python_executable`
157
#[allow(non_upper_case_globals)]
158
pub static PythonPathVenv: OnceCell<Option<FPath>> = OnceCell::new();
159

160
lazy_static! {
161
    /// Summary statistic.
162
    /// Record which Python interpreters ran.
163
    /// only intended for summary printing
164
    pub static ref PythonPathsRan: RwLock<HashSet<FPath>> = {
165
        defñ!("init PythonPathsRan");
166

167
        RwLock::new(HashSet::<FPath>::new())
168
    };
169
}
170

171
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
172
pub enum PythonToUse {
173
    /// only use Python referred to by environment variable `S4_PYTHON`
174
    Env,
175
    /// only use Python found in the `PATH`
176
    Path,
177
    /// use Python referred to by environment variable `S4_PYTHON` if set
178
    /// but if not set then use Python found in the `PATH`
179
    EnvPath,
180
    /// only use Python in predetermined s4 .venv
181
    Venv,
182
    /// use Python referred to by environment variable `S4_PYTHON` if set
183
    /// but if not set then use Python in predetermined s4 .venv
184
    EnvVenv,
185
    /// use a passed value
186
    Value,
187
}
188

189
/// find a Python executable.
190
/// `python_to_use` instructs how to find the Python executable.
191
/// Does not check if the found Python executable is valid.
192
/// Caches found paths for reliability among threads.
193
/// Returns `None` when passed `PythonToUse::Value`.
194
pub fn find_python_executable(python_to_use: PythonToUse) -> &'static Option<FPath> {
86✔
195
    defn!("{:?}", python_to_use);
86✔
196

197
    match python_to_use {
86✔
198
        PythonToUse::Env => {
199
            let ret: &Option<FPath> = PythonPathEnv.get_or_init(||
12✔
200
                // check process environment variable
201
                match env::var(PYTHON_ENV) {
1✔
202
                    Ok(val) => {
×
203
                        defo!("env::var found {}={:?}", PYTHON_ENV, val);
×
204
                        if ! val.is_empty() {
×
205
                            Some(val)
×
206
                        } else {
207
                            None
×
208
                        }
209
                    }
210
                    Err (_err) => {
1✔
211
                        defo!("env::var did not find {:?}; {:?}", PYTHON_ENV, _err);
1✔
212
                        None
1✔
213
                    }
214
                }
1✔
215
            );
216
            defx!("{:?}, return {:?}", python_to_use, ret);
12✔
217

218
            ret
12✔
219
        }
220
        PythonToUse::Path => {
221
            let ret: &Option<FPath> = PythonPathPath.get_or_init(||{
43✔
222
                let mut python_path: Option<PathBuf> = None;
1✔
223
                // check PATH for python executable
224
                for name in PYTHON_NAMES.iter() {
1✔
225
                    defo!("find_executable_in_path({:?})", name);
1✔
226
                    if let Some(p) = find_executable_in_path(name) {
1✔
227
                        defo!("find_executable_in_path returned {:?}", p);
1✔
228
                        python_path = Some(p);
1✔
229
                        break;
1✔
230
                    };
×
231
                }
232
                if let Some(p) = python_path {
1✔
233
                    Some(path_to_fpath(p.as_path()))
1✔
234
                } else {
235
                    None
×
236
                }
237
            });
1✔
238
            defx!("{:?}, return {:?}", python_to_use, ret);
43✔
239

240
            ret
43✔
241
        }
242
        PythonToUse::EnvPath => {
243
            // try Env then try Path
244
            let p = find_python_executable(PythonToUse::Env);
1✔
245
            if p.is_some() {
1✔
246
                defx!("{:?}, return {:?}", python_to_use, p);
×
247
                return p;
×
248
            }
1✔
249
            let p = find_python_executable(PythonToUse::Path);
1✔
250
            defx!("{:?}, return {:?}", python_to_use, p);
1✔
251

252
            p
1✔
253
        }
254
        PythonToUse::Venv => {
255
            let ret: &Option<FPath> = PythonPathVenv.get_or_init(||{
19✔
256
                // get the venv path
257
                let venv: PathBuf = venv_path();
1✔
258
                defo!("venv={:?}", venv);
1✔
259
                // look for common subdirectories of Python virtual environments where the
260
                // Python executable may be found
261
                // XXX: we could try to do this by platform
262
                //      i.e. on Windows only look in "Scripts", etc.
263
                //      but this is fine
264
                for dir in PYTHON_SUBDIRS.iter() {
1✔
265
                    let mut venv_dir = venv.clone();
1✔
266
                    if ! dir.is_empty() {
1✔
267
                        venv_dir.push(dir);
1✔
268
                    }
1✔
269
                    for name in PYTHON_NAMES.iter() {
1✔
270
                        let mut venv_name = venv_dir.clone();
1✔
271
                        venv_name.push(name);
1✔
272
                        defo!("venv_name.exists?={:?}", venv_name);
1✔
273
                        if venv_name.exists() {
1✔
274
                            let fp = path_to_fpath(venv_name.as_path());
1✔
275
                            defo!("found venv python executable: {:?}", fp);
1✔
276
                            return Some(fp);
1✔
277
                        }
×
278
                    }
279
                }
280
                None
×
281
            });
1✔
282
            defx!("{:?}, return {:?}", python_to_use, ret);
19✔
283

284
            ret
19✔
285
        }
286
        PythonToUse::EnvVenv => {
287
            // try Env then try Venv
288
            let p = find_python_executable(PythonToUse::Env);
11✔
289
            if p.is_some() {
11✔
290
                defx!("{:?}, return {:?}", python_to_use, p);
×
291
                return p;
×
292
            }
11✔
293
            let p = find_python_executable(PythonToUse::Venv);
11✔
294
            defx!("{:?}, return {:?}", python_to_use, p);
11✔
295

296
            p
11✔
297
        }
298
        PythonToUse::Value => {
299
            debug_panic!("PythonToUse::Value should not be used in find_python_executable");
×
300

301
            &None
×
302
        }
303
    }
304
}
86✔
305

306
#[derive(Debug)]
307
enum PipedChunk {
308
    /// a chunk of bytes read from the child process
309
    Chunk(Bytes),
310
    /// process not sending but still running
311
    Continue,
312
    /// process exited or no more data to read
313
    /// contains number of reads performed and remaining bytes
314
    Done(u64, Bytes),
315
}
316

317
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
318
enum ProcessStatus {
319
    #[default]
320
    Running,
321
    Exited,
322
}
323

324
/// Reads data from the pipe and returns chunks of data to the caller.
325
/// Churks are delimited by the passed `chunk_delimiter_opt` if set.
326
/// If `chunk_delimiter_opt` is `None` then each read immediately returns any data read.
327
///
328
/// Inspired by gist [ArtemGr/db40ae04b431a95f2b78](https://gist.github.com/ArtemGr/db40ae04b431a95f2b78).
329
struct PipeStreamReader {
330
    chunk_receiver: Receiver<core::result::Result<PipedChunk, Error>>,
331
    exit_sender: Sender<ProcessStatus>,
332
    #[allow(dead_code)]
333
    pid: u32,
334
    #[allow(dead_code)]
335
    name: String,
336
}
337

338
impl PipeStreamReader {
339
    /// Starts a thread reading bytes from a child process pipe.
340
    ///
341
    /// `pipe_sz` is the size of the Pipe chunk buffer in bytes.
342
    ///
343
    /// `recv_timeout` is the timeout duration for calls to `recv_timeout()`.
344
    ///
345
    /// `chunk_delimiter_opt` is an optional byte delimiter used to separate chunks of data.
346
    /// If `None` then each read immediately returns any data read.
347
    ///
348
    /// `stream_child_proc` is the `Read` stream of the child process to read from.
349
    ///
350
    /// `name` and `pid` are used for debugging messages.
351
    /// `name` is the name of the pipe.
352
    /// `pid` is the process ID of the child process.
353
    fn new(
140✔
354
        name: String,
140✔
355
        pid: u32,
140✔
356
        pipe_sz: PipeSz,
140✔
357
        recv_timeout: Duration,
140✔
358
        chunk_delimiter_opt: Option<ChunkDelimiter>,
140✔
359
        mut stream_child_proc: Box<dyn Read + Send>
140✔
360
    ) -> PipeStreamReader
140✔
361
    {
362
        def1n!("PipeStreamReader new(pipe_sz={}, name={:?}, chunk_delimiter_opt={:?})",
140✔
363
               pipe_sz, name, chunk_delimiter_opt);
364
        def1o!("PipeStreamReader {:?} create bounded({}) channel", name, CHANNEL_CAPACITY);
140✔
365
        let (tx_exit, rx_exit) =
140✔
366
            ::crossbeam_channel::bounded(CHANNEL_CAPACITY);
140✔
367
        let name_: String = name.clone();
140✔
368

369
        PipeStreamReader {
370
            chunk_receiver: {
371
                let thread_name: String = format!("{}_PipeStreamReader", name);
140✔
372
                let _thread_name2: String = thread_name.clone();
140✔
373
                // parent thread ID
374
                let _tid_p: u64 = threadid_to_u64(thread::current().id());
140✔
375
                // debug message prepend
376
                let _d_p = format!(
140✔
377
                    "PipeStreamReader {:?} PID {:?} PTID {:?}",
378
                    name, pid, _tid_p
379
                );
380
                def1o!("{_d_p} create unbounded() channel");
140✔
381
                let (tx_parent, rx_parent) =
140✔
382
                    ::crossbeam_channel::unbounded();
140✔
383

384
                let thread_pipe = thread::Builder::new().name(thread_name.clone());
140✔
385

386
                def1o!("{_d_p} spawn thread {:?}", thread_name);
140✔
387
                let result = thread_pipe.spawn(move ||
140✔
388
                {
140✔
389
                    // debug message prepend
390
                    let _d_p = format!(
140✔
391
                        "PipeStreamReader {:?} PID {:?} PTID {:?} TID {:?}",
392
                        name, pid, _tid_p, threadid_to_u64(thread::current().id()));
140✔
393
                    def2n!("{_d_p} start, pipe_sz {}", pipe_sz);
140✔
394
                    let mut _recv_bytes: usize = 0;
140✔
395
                    let mut reads: usize = 0;
140✔
396
                    let mut _sends: usize = 0;
140✔
397
                    let mut delim_found: bool = false;
140✔
398
                    let mut buf = Bytes::with_capacity(pipe_sz);
140✔
399
                    let buf_chunk1_sz: usize = match chunk_delimiter_opt {
140✔
400
                        Some(_delim) => pipe_sz,
80✔
401
                        None => 0, // `buf_chunk1` not used
60✔
402
                    };
403
                    let mut buf_chunk1: Bytes = Bytes::with_capacity(buf_chunk1_sz);
140✔
404
                    //let mut buf_chunk2: Bytes = Bytes::with_capacity(pipe_sz);
405
                    loop {
406
                        reads += 1;
45,080✔
407
                        buf.clear();
45,080✔
408
                        buf.resize(pipe_sz, 0);
45,080✔
409

410
                        def2o!("{_d_p} stream_child_proc.read(buf capacity {}, len {})…", buf.capacity(), buf.len());
45,080✔
411
                        /*
412
                        From the docs regarding read():
413

414
                            This function does not provide any guarantees about whether it blocks
415
                            waiting for data, but if an object needs to block for a read and cannot,
416
                            it will typically signal this via an Err return value.
417

418
                        See https://doc.rust-lang.org/1.83.0/std/io/trait.Read.html#tymethod.read
419
                        */
420
                        match stream_child_proc.read(&mut buf) {
45,080✔
421
                            Ok(0) => {
422
                                def2o!("{_d_p} read zero bytes of {} total", _recv_bytes);
161✔
423
                                /*
424
                                From the docs regarding read() returning Ok(0):
425

426
                                    This reader has reached its "end of file" and will likely no
427
                                    longer be able to produce bytes. Note that this does not mean
428
                                    that the reader will always no longer be able to produce bytes.
429
                                    As an example, on Linux, this method will call the recv syscall
430
                                    for a [TcpStream], where returning zero indicates the connection
431
                                    was shut down correctly. While for [File], it is possible to
432
                                    reach the end of file and get zero as result, but if more data
433
                                    is appended to the file, future calls to read will return more
434
                                    data.
435

436
                                See https://doc.rust-lang.org/1.83.0/std/io/trait.Read.html#tymethod.read
437
                                */
438
                                if delim_found {
161✔
439
                                    delim_found = false;
46✔
440
                                }
115✔
441
                                // XXX: if the child python process has exited then this may become
442
                                //      a busy loop until the parent thread notices the python
443
                                //      process has exited and then can send a
444
                                //      `ProcessStatus::Exited`.
445
                                //      Using `recv_timeout(5ms)` softens this busy loop.
446
                                //      It's ugly but it works.
447
                                def2o!("{_d_p} rx_exit.recv_timeout({:?}) (len {})…", recv_timeout, rx_exit.len());
161✔
448
                                let rx_result = rx_exit.recv_timeout(recv_timeout);
161✔
449
                                match rx_result {
139✔
450
                                    Ok(ProcessStatus::Exited) => {
451
                                        def2o!("{_d_p} rx_exit ProcessStatus::Exited; send Done({}, buf_chunk1 {} bytes) and break",
139✔
452
                                               reads, buf_chunk1.len());
139✔
453
                                        _sends += 1;
139✔
454
                                        def2o!("{_d_p} tx_parent.send(Ok(PipedChunk::Done({}, buf_chunk1 {} bytes))) (channel len {})…",
139✔
455
                                               reads, buf_chunk1.len(), tx_parent.len());
139✔
456
                                        match tx_parent.send(Ok(PipedChunk::Done(reads as u64, buf_chunk1))) {
139✔
457
                                            Ok(_) => {}
113✔
458
                                            Err(_err) => {
26✔
459
                                                def2o!("{_d_p} tx send error: {:?}", _err);
26✔
460
                                            }
461
                                        }
462
                                        break;
139✔
463
                                    }
464
                                    Ok(ProcessStatus::Running) => {
465
                                        def2o!("{_d_p} rx_exit ProcessStatus::Running; continue reading");
×
466
                                    }
467
                                    Err(RecvTimeoutError::Timeout) => {
468
                                        def2o!("{_d_p} RecvTimeoutError::Timeout; continue reading");
22✔
469
                                    }
470
                                    Err(RecvTimeoutError::Disconnected) => {
471
                                        def2o!("{_d_p} RecvTimeoutError::Disconnected; break");
×
472
                                        break;
×
473
                                    }
474
                                }
475
                                // send Continue if no more messages to process by parent thread
476
                                if tx_parent.is_empty() {
22✔
477
                                    def2o!("{_d_p} tx_parent.send(Ok(PipedChunk::Continue))…");
22✔
478
                                    match tx_parent.send(Ok(PipedChunk::Continue)) {
22✔
479
                                        Ok(_) => {
22✔
480
                                            _sends += 1;
22✔
481
                                        }
22✔
482
                                        Err(_err) => {
×
483
                                            def2o!("{_d_p} tx send error: {:?}", _err);
×
484
                                            de_err!("{_d_p} tx send error: {:?}", _err);
×
485
                                        }
486
                                    };
487
                                }
×
488
                            }
489
                            Ok(len) => {
44,919✔
490
                                _recv_bytes += len;
44,919✔
491
                                def2o!("{_d_p} (read #{}) read {} bytes of {} total in this pipe", reads, len, _recv_bytes);
44,919✔
492
                                // is there a chunk delimiter in the buffer?
493

494
                                match chunk_delimiter_opt {
44,919✔
495
                                    Some(chunk_delimiter) => {
42,209✔
496
                                        // look for delimiter
497
                                        // TODO: [2025/12] add benchmark to compare different methods
498
                                        //       of finding a delimiter.
499
                                        //       This `find_memchr` creates a new `Finder<'_>`
500
                                        //       which may not be worth the trouble.
501
                                        let needle = &[chunk_delimiter];
42,209✔
502
                                        let finder = memchr_Finder::new(needle);
42,209✔
503
                                        let mut at: usize = 0;
42,209✔
504
                                        let mut _loop: usize = 0;
42,209✔
505
                                        while at < len {
89,377✔
506
                                            _loop += 1;
47,170✔
507
                                            def2o!("{_d_p} (read #{reads} loop {_loop}) searching for delimiter in buf[{at}..{len}] '{}'", 
47,170✔
508
                                                buffer_to_string_noraw(&buf[at..len]));
47,170✔
509
                                            match finder.find(&buf[at..len]) {
47,170✔
510
                                                Some(pos) => {
6,487✔
511
                                                    // delimiter found at pos
512
                                                    def2o!("{_d_p} (read #{reads} loop {_loop}) found delimiter at pos {} (absolute pos {}) among {} returned bytes; buf len {}, buf capacity {}",
6,487✔
513
                                                        pos, at + pos, len, buf.len(), buf.capacity());
6,487✔
514
                                                    debug_assert!(at + pos < buf.len(), "at {} + pos {} >= buf.len {}", at, pos, buf.len());
6,487✔
515
                                                    // send chunks, keep the remainder
516
                                                    def2o!("{_d_p} (read #{reads} loop {_loop}) buf_chunk1.extend_from_slice(buf[{}..{}])", at, at + pos + 1);
6,487✔
517
                                                    buf_chunk1.extend_from_slice(&buf[at..at + pos + 1]);
6,487✔
518
                                                    def2o!("{_d_p} (read #{reads} loop {_loop}) buf_chunk1: '{}'", buffer_to_string_noraw(&buf_chunk1));
6,487✔
519
                                                    let blen = buf_chunk1.len();
6,487✔
520
                                                    let mut chunk_send: Bytes = Vec::<u8>::with_capacity(blen);
6,487✔
521
                                                    def2o!("{_d_p} (read #{reads} loop {_loop}) chunk_send.extend_from_slice(&buf_chunk1 len {}) (chunk_send capacity {})",
6,487✔
522
                                                        buf_chunk1.len(), chunk_send.capacity());
6,487✔
523
                                                    chunk_send.extend_from_slice(&buf_chunk1);
6,487✔
524
                                                    def2o!("{_d_p} (read #{reads} loop {_loop}) chunk_send: '{}' (channel len {})",
6,487✔
525
                                                           buffer_to_string_noraw(&chunk_send), tx_parent.len());
6,487✔
526
                                                    let data_send = PipedChunk::Chunk(chunk_send);
6,487✔
527
                                                    _sends += 1;
6,487✔
528
                                                    match tx_parent.send(Ok(data_send)) {
6,487✔
529
                                                        Ok(_) => {
530
                                                            def2o!("{_d_p} (read #{reads} loop {_loop}) sent chunk_send {} bytes, send #{_sends}", blen);
6,485✔
531
                                                        }
532
                                                        Err(_err) => {
2✔
533
                                                            def2o!("{_d_p} (read #{reads} loop {_loop}) send error: {:?}", _err);
2✔
534
                                                            break;
2✔
535
                                                        }
536
                                                    }
537
                                                    def2o!("{_d_p} (read #{reads} loop {_loop}) buf_chunk1.clear()");
6,485✔
538
                                                    buf_chunk1.clear();
6,485✔
539
                                                    // def2o!("{_d_p} (read #{reads} loop {_loop}) buf_chunk1.extend_from_slice(&buf[{}..{}]) (buf len {}, buf capacity {})",
540
                                                    //     at + pos + 1, len, buf.len(), buf.capacity());
541
                                                    // buf_chunk1.extend_from_slice(&buf[at + pos + 1..len]);
542
                                                    // def2o!("{_d_p} (read #{reads} loop {_loop}) buf_chunk1: len {}, capacity {}; contents: '{}'",
543
                                                    //     buf_chunk1.len(), buf_chunk1.capacity(), buffer_to_string_noraw(&buf_chunk1));
544
                                                    delim_found = true;
6,485✔
545
                                                    at += pos + 1;
6,485✔
546
                                                    def2o!("{_d_p} (read #{reads} loop {_loop}) {} bytes remaining in buf", len - at);
6,485✔
547
                                                }
548
                                                None => {
549
                                                    // delimiter not found, save buffer and then read child process again
550
                                                    def2o!("{_d_p} (read #{reads} loop {_loop}) no delimiter; buf_chunk1.extend_from_slice(&buf[{}..{}]) '{}'",
40,683✔
551
                                                        at, len, buffer_to_string_noraw(&buf[at..len]));
40,683✔
552
                                                    buf_chunk1.extend_from_slice(&buf[at..len]);
40,683✔
553
                                                    def2o!("{_d_p} (read #{reads} loop {_loop}) buf_chunk1: len {}, capacity {}; contents: '{}'",
40,683✔
554
                                                        buf_chunk1.len(), buf_chunk1.capacity(), buffer_to_string_noraw(&buf_chunk1));
40,683✔
555
                                                    delim_found = false;
40,683✔
556
                                                    at += len + 1;
40,683✔
557
                                                }
558
                                            }
559
                                        }
560
                                    }
561
                                    None => {
562
                                        // no delimiter configured, send entire buffer as a chunk
563
                                        let slice_ = &buf[..len];
2,710✔
564
                                        let blen = slice_.len();
2,710✔
565
                                        let mut chunk_send: Bytes = Vec::<u8>::with_capacity(blen);
2,710✔
566
                                        chunk_send.extend_from_slice(slice_);
2,710✔
567
                                        let data_send = PipedChunk::Chunk(chunk_send);
2,710✔
568
                                        delim_found = false;
2,710✔
569
                                        def2o!("{_d_p} (read #{reads}) read {} bytes of {} total; no delimiter configured, send Chunk {} bytes",
2,710✔
570
                                            len, _recv_bytes, blen);
571
                                        _sends += 1;
2,710✔
572
                                        match tx_parent.send(Ok(data_send)) {
2,710✔
573
                                            Ok(_) => {
574
                                                def2o!("{_d_p} (read #{reads}) sent chunk_send {} bytes, send #{_sends}", blen);
2,709✔
575
                                            }
576
                                            Err(_err) => {
1✔
577
                                                def2o!("{_d_p} (read #{reads}) send error: {:?}", _err);
1✔
578
                                                break;
1✔
579
                                            }
580
                                        }
581
                                    }
582
                                }
583
                            }
584
                            Err(error) => {
×
585
                                if error.kind() == ErrorKind::Interrupted {
×
586
                                    def2o!("{_d_p} (read #{reads}) read interrupted; retry");
×
587
                                    continue;
×
588
                                }
×
589
                                def2o!("{_d_p} (read #{reads}) read error {}; send Error", error);
×
590
                                delim_found = false;
×
591
                                _sends += 1;
×
592
                                match tx_parent.send(Err(error)) {
×
593
                                    Ok(_) => {}
×
594
                                    Err(_err) => {
×
595
                                        def2o!("{_d_p} (read #{reads}) send error: {:?}", _err);
×
596
                                        break;
×
597
                                    }
598
                                }
599
                            }
600
                        }
601
                    }
602
                    def2x!(
140✔
603
                        "{_d_p} exit, received {} bytes, child process reads {}, parent thread sends {}",
604
                        _recv_bytes, reads, _sends
605
                    );
606
                });
140✔
607
                match result {
140✔
608
                    Ok(_handle) => {
140✔
609
                        def1o!("{_d_p} spawned thread {:?}", _thread_name2);
140✔
610
                    }
611
                    Err(_err) => {
×
612
                        def1o!("{_d_p} thread spawn error: {}", _err);
×
613
                    }
614
                }
615
                def1x!("{_d_p} return Receiver");
140✔
616

617
                rx_parent
140✔
618
            },
619
            exit_sender: tx_exit,
140✔
620
            pid,
140✔
621
            name: name_,
140✔
622
        }
623
    }
140✔
624
}
625

626
impl Drop for PipeStreamReader {
627
    fn drop(&mut self) {
140✔
628
        def2ñ!("PipeStreamReader: {} PID {}", self.name, self.pid);
140✔
629
    }
140✔
630
}
631

632
/// `PyRunner` is a struct that represents a Python process instance. It hides
633
/// the complications of starting and communicating with a Python process
634
/// over pipes. It uses `std::process::Child` to start and manage the Python
635
/// process. This handles the complexity of asynchronous inter-process
636
/// communication which is non-trivial to implement decently.
637
///
638
/// _XXX:_ ideally, this would use `PyO3` to communicate with a Python interpreter
639
/// instance. However, [`PyO3::Python::attach`] only creates one
640
/// Python process per Rust process.
641
/// And `PyO3` does not provide a way to create Python subprocesses. So all
642
/// Rust process threads that would use `PyO3` are bottlenecked by this
643
/// one Python process which is of course, in effect, a single-threaded process.
644
/// See [PyO3 Issue #576].
645
/// So instead each `PyRunner` instance creates a new Python process using
646
/// [`std::process::Child`] and communicates over stdout, stderr, and stdin pipes.
647
///
648
/// _XXX:_ I also tried using crate `subprocess` to manage the Python process. However,
649
/// it was not able to handle irregular asynchronous communication.
650
///
651
/// [`PyO3::Python::attach`]: https://docs.rs/pyo3/0.27.1/pyo3/marker/struct.Python.html#method.attach
652
/// [PyO3 Issue #576]: https://github.com/PyO3/pyo3/issues/576
653
pub struct PyRunner {
654
    path_id: PathId,
655
    pub process: Child,
656
    pipe_stdout: PipeStreamReader,
657
    pipe_stderr: PipeStreamReader,
658
    /// arguments of the process
659
    argv: Vec<String>,
660
    /// path to Python exectuable
661
    pub python_path: FPath,
662
    /// save the `ExitStatus`
663
    exit_status: Option<ExitStatus>,
664
    pipe_stdout_eof: bool,
665
    pipe_stderr_eof: bool,
666
    /// protect against sending repeat exit messages to child pipe threads.
667
    /// only used in `pipes_exit_sender()`
668
    pipe_sent_exit: bool,
669
    /// pipe buffer size in bytes for stdout `PipeStreamReader`
670
    pub pipe_sz_stdout: PipeSz,
671
    /// pipe buffer size in bytes for stderr `PipeStreamReader`
672
    pub pipe_sz_stderr: PipeSz,
673
    /// Unmanaged file handle to represent the Python process.
674
    /// When this `PyRunner` is dropped then the `FILE_HANDLE_MANAGER` is
675
    /// updated.
676
    #[allow(dead_code)]
677
    file_handle_python_proc: FileHandleUnmanaged,
678
    /// `Instant` Python process was started.
679
    time_beg: Instant,
680
    /// `Instant` the Python process was first known to be exited.
681
    time_end: Option<Instant>,
682
    /// process ID of the Python process
683
    pid_: u32,
684
    /// this thread ID. For help during debugging.
685
    tid: u64,
686
    /// debug message prepend. For help during debugging.
687
    _d_p: String,
688
    /// all stderr is stored in case the process exits with an error
689
    /// this is because stderr may be `read` and only later calls to
690
    /// `poll` or `wait` may find the process has exited.
691
    ///
692
    /// oldest stderr data nearest the front
693
    stderr_all: Option<VecDeque<u8>>,
694
    /// Summary statistic.
695
    /// Maximum number of messages seen in the pipe_stdout channel.
696
    pub(crate) pipe_channel_max_stdout: Count,
697
    /// Summary statistic.
698
    /// Maximum number of messages seen in the pipe_stderr channel.
699
    pub(crate) pipe_channel_max_stderr: Count,
700
    /// Summary statistic.
701
    /// count of reads performed by pipeline thread reading Python process stdout
702
    pub(crate) count_proc_reads_stdout: Count,
703
    /// Summary statistic.
704
    /// count of reads performed by pipeline thread reading Python process stderr
705
    pub(crate) count_proc_reads_stderr: Count,
706
    /// Summary statistic.
707
    /// count of recv of pipeline thread reading Python process stdout
708
    pub(crate) count_pipe_recv_stdout: Count,
709
    /// Summary statistic.
710
    /// count of recv of pipeline thread reading Python process stderr
711
    pub(crate) count_pipe_recv_stderr: Count,
712
    /// Summary statistic.
713
    /// count of writes to Python process stdin
714
    pub(crate) count_proc_writes: Count,
715
    /// Summary statistic.
716
    /// count of polls of Python process
717
    pub(crate) count_proc_polls: Count,
718
    /// Duration of process waiting
719
    pub(crate) duration_proc_wait: Duration,
720
    /// first seen error
721
    pub(crate) error: Option<Error>,
722
}
723

724
impl std::fmt::Debug for PyRunner {
725
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
726
        f.debug_struct("PyRunner")
×
727
            .field("process", &self.process)
×
728
            .field("argv", &self.argv)
×
729
            .field("python_path", &self.python_path)
×
730
            .field("exit_status", &self.exit_status)
×
731
            .field("pipe_stdout_eof", &self.pipe_stdout_eof)
×
732
            .field("pipe_stderr_eof", &self.pipe_stderr_eof)
×
733
            .field("pipe_sent_exit", &self.pipe_sent_exit)
×
734
            .field("pipe_sz_stdout", &self.pipe_sz_stdout)
×
735
            .field("pipe_sz_stderr", &self.pipe_sz_stderr)
×
736
            .field("time_beg", &self.time_beg)
×
737
            .field("time_end", &self.time_end)
×
738
            .field("pid_", &self.pid_)
×
739
            .field("tid", &self.tid)
×
740
            .finish()
×
741
    }
×
742
}
743

744
impl PyRunner {
745
    /// Create a new `PyRunner` instance.
746
    ///
747
    /// `python_to_use` indicates which Python executable to use.
748
    /// If `PythonToUse::Value` is used then `python_path` must be `Some(FPath)`.
749
    /// Otherwise `python_path` must be `None`.
750
    ///
751
    /// `argv` is the list of arguments to pass to the Python executable.
752
    pub fn new(
70✔
753
        python_to_use: PythonToUse,
70✔
754
        path_id: PathId,
70✔
755
        pipe_sz: PipeSz,
70✔
756
        recv_timeout: Duration,
70✔
757
        chunk_delimiter_stdout: Option<ChunkDelimiter>,
70✔
758
        chunk_delimiter_stderr: Option<ChunkDelimiter>,
70✔
759
        python_path: Option<FPath>,
70✔
760
        argv: Vec<&str>
70✔
761
    ) -> Result<Self> {
70✔
762
        def1n!("python_to_use {:?}, python_path {:?}, pipe_sz {:?}, chunk_delimiter_stdout {:?}, chunk_delimiter_stderr {:?}, argv {:?}",
70✔
763
            python_to_use, python_path, pipe_sz, chunk_delimiter_stdout, chunk_delimiter_stderr, argv);
764

765
        let python_path_: &FPath;
766
        // get the Python exectuble
767
        if python_to_use == PythonToUse::Value {
70✔
768
            match &python_path {
50✔
769
                Some(val) => python_path_ = val,
50✔
770
                None => {
771
                    let s = format!("PyRunner::new: python_path must be Some(FPath) when python_to_use is Value");
×
772
                    def1x!("Error InvalidInput {}", s);
×
773
                    return Result::Err(
×
774
                        Error::new(ErrorKind::InvalidInput, s)
×
775
                    );
×
776
                }
777
            }
778
        } else {
779
            debug_assert_none!(python_path, "python_path must be None unless python_to_use is Value");
20✔
780
            python_path_ = match find_python_executable(python_to_use) {
20✔
781
                Some(s) => s,
20✔
782
                None => {
783
                    let s = format!(
×
784
                        "failed to find a Python interpreter; create the Python virtual environment with command --venv, or you may specify the Python interpreter path using environment variable {}; failed",
785
                        PYTHON_ENV
786
                    );
787
                    def1x!("{}", s);
×
788
                    return Result::Err(
×
789
                        Error::new(ErrorKind::NotFound, s)
×
790
                    )
×
791
                }
792
            };
793
        }
794
        def1o!("Using Python executable: {:?}", python_path_);
70✔
795

796
        // construct argv_
797
        let mut argv_: Vec<&str> = Vec::with_capacity(argv.len() + 1);
70✔
798
        argv_.push(python_path_.as_str());
70✔
799
        for arg in argv.iter() {
201✔
800
            argv_.push(arg);
201✔
801
        }
201✔
802

803
        summary_stat!(
70✔
804
            // save this python path
805
            match PythonPathsRan.write() {
70✔
806
                Ok(mut set) => {
70✔
807
                    if ! set.contains(python_path_) {
70✔
808
                        set.insert(python_path_.clone());
2✔
809
                    }
68✔
810
                }
811
                Err(err) => {
×
812
                    def1x!("Failed to acquire write lock on PythonPathsRan: {}", err);
×
813
                    return Result::Err(
×
814
                        Error::other(
×
815
                            format!("Failed to acquire write lock on PythonPathsRan: {}", err),
×
816
                        )
×
817
                    );
×
818
                }
819
            }
820
        );
821

822
        // reserve the open file handle for the new Python process
823
        let file_handle_python_proc: FileHandleUnmanaged = match FILE_HANDLE_MANAGER.request_open_unmanaged(
70✔
824
            path_id,
70✔
825
            FileHandleRole::Unmanaged,
70✔
826
            &python_path_,
70✔
827
        ) {
70✔
828
            Result::Ok(val) => val,
70✔
829
            Result::Err(err) => {
×
830
                def1x!("return {:?}", err);
×
831
                return Err(err);
×
832
            }
833
        };
834

835
        def1o!("Command::new({:?}).args({:?}).spawn()", python_path_, Vec::from_iter(argv_.iter().skip(1)));
70✔
836
        let time_beg: Instant = Instant::now();
70✔
837
        let result = Command::new(python_path_.as_str())
70✔
838
            .args(argv_.iter().skip(1))
70✔
839
            .stdin(Stdio::piped())
70✔
840
            .stdout(Stdio::piped())
70✔
841
            .stderr(Stdio::piped())
70✔
842
            .spawn();
70✔
843
        let mut process: Child = match result {
70✔
844
            Ok(p) => p,
70✔
845
            Err(err) => {
×
846
                def1x!("Failed to start Python process: {}", err);
×
847
                return Result::Err(
×
848
                    Error::new(
×
849
                        err.kind(),
×
850
                        format!("Python process failed to start: Python path {:?}; {}",
×
851
                            python_path_, err),
×
852
                    )
×
853
                );
×
854
            }
855
        };
856

857
        // TODO: [2025/11] is there a more rustic one-liner to create Vec<String> from Vec<&str>?
858
        let mut argv: Vec<String> = Vec::with_capacity(argv_.len());
70✔
859
        for a in argv_.into_iter() {
271✔
860
            argv.push(String::from(a));
271✔
861
        }
271✔
862

863
        let pid: u32 = process.id();
70✔
864
        def1o!("Python process PID {}", pid);
70✔
865

866
        let _d_p = format!("Python process {}", pid);
70✔
867

868
        let process_stdout = match process.stdout.take() {
70✔
869
            Some(s) => s,
70✔
870
            None => {
871
                let s = format!("{_d_p} stdout was None");
×
872
                def1x!("{}", s);
×
873
                return Result::Err(
×
874
                    Error::other(s)
×
875
                );
×
876
            }
877
        };
878
        let process_stderr = match process.stderr.take() {
70✔
879
            Some(s) => s,
70✔
880
            None => {
881
                let s = format!("{_d_p} stderr was None");
×
882
                def1x!("{}", s);
×
883
                return Result::Err(
×
884
                    Error::other(s)
×
885
                );
×
886
            }
887
        };
888

889
        // create PipeStreamReaders for stdout, stderr
890
        def1o!("{_d_p} PipeStreamReader::new() stdout");
70✔
891
        let pipe_sz_stdout: usize = pipe_sz;
70✔
892
        let pipe_stdout = PipeStreamReader::new(
70✔
893
            String::from("stdout"),
70✔
894
            pid,
70✔
895
            pipe_sz_stdout,
70✔
896
            recv_timeout,
70✔
897
            chunk_delimiter_stdout,
70✔
898
            Box::new(process_stdout)
70✔
899
        );
900
        def1o!("{_d_p} PipeStreamReader::new() stderr");
70✔
901
        // stderr pipe capped at 5096 bytes
902
        let pipe_sz_stderr: usize = min(pipe_sz, 5096);
70✔
903
        let pipe_stderr = PipeStreamReader::new(
70✔
904
            String::from("stderr"),
70✔
905
            pid,
70✔
906
            pipe_sz_stderr,
70✔
907
            recv_timeout,
70✔
908
            chunk_delimiter_stderr,
70✔
909
            Box::new(process_stderr)
70✔
910
        );
911

912
        let tid: u64 = threadid_to_u64(thread::current().id());
70✔
913
        defx!("{_d_p} PyRunner created for Python process PID {}, TID {}", pid, tid);
70✔
914

915
        Result::Ok(Self {
70✔
916
            path_id,
70✔
917
            process,
70✔
918
            pipe_stdout,
70✔
919
            pipe_stderr,
70✔
920
            argv,
70✔
921
            python_path: python_path_.clone(),
70✔
922
            exit_status: None,
70✔
923
            pipe_stdout_eof: false,
70✔
924
            pipe_stderr_eof: false,
70✔
925
            pipe_sent_exit: false,
70✔
926
            pipe_sz_stdout,
70✔
927
            pipe_sz_stderr,
70✔
928
            file_handle_python_proc,
70✔
929
            pid_: pid,
70✔
930
            tid,
70✔
931
            _d_p,
70✔
932
            stderr_all: None,
70✔
933
            time_beg,
70✔
934
            time_end: None,
70✔
935
            pipe_channel_max_stdout: 0,
70✔
936
            pipe_channel_max_stderr: 0,
70✔
937
            count_proc_reads_stdout: 0,
70✔
938
            count_proc_reads_stderr: 0,
70✔
939
            count_pipe_recv_stdout: 0,
70✔
940
            count_pipe_recv_stderr: 0,
70✔
941
            count_proc_writes: 0,
70✔
942
            count_proc_polls: 0,
70✔
943
            duration_proc_wait: Duration::default(),
70✔
944
            error: None,
70✔
945
        })
70✔
946
    }
70✔
947

948
    #[allow(dead_code)]
949
    pub fn pid(&self) -> u32 {
70✔
950
        self.pid_
70✔
951
    }
70✔
952

953
    #[allow(dead_code)]
954
    pub fn tid(&self) -> u64 {
70✔
955
        self.tid
70✔
956
    }
70✔
957

958
    #[inline(always)]
959
    pub const fn path_id(&self) -> PathId {
9,422✔
960
        self.path_id
9,422✔
961
    }
9,422✔
962

963
    /// Returns the process exit status.
964
    /// If the process has not exited yet, returns `None`.
965
    pub fn exit_status(&self) -> Option<ExitStatus> {
11✔
966
        self.exit_status
11✔
967
    }
11✔
968

969
    /// Returns `true` if the process exited successfully.
970
    pub fn exit_okay(&self) -> bool {
156✔
971
        self.exit_status == Some(ExitStatus::default())
156✔
972
    }
156✔
973

974
    /// convert a `RecvError` into an `Error`
975
    fn new_error_from_recverror(&self, recverror: &RecvError) -> Error {
×
976
        Error::new(
×
977
            ErrorKind::Other,
×
978
            format!("Python process {} RecvError: {}",
×
979
                self.pid_, recverror),
980
        )
981
    }
×
982

983
    /// Returns all stderr data accumulated so far.
984
    /// oldest stderr data nearest the front
985
    pub fn stderr_all(&mut self) -> Option<&[u8]> {
×
986
        match &mut self.stderr_all {
×
987
            Some(v) => {
×
988
                v.make_contiguous();
×
989
                Some(v.as_slices().0)
×
990
            },
991
            None => None,
×
992
        }
993
    }
×
994

995
    /// Poll the Python process to see if it has exited.
996
    /// If the process has exited, returns `Some(ExitStatus)`.
997
    /// If the process is still running, returns `None`.
998
    pub fn poll(&mut self) -> Option<ExitStatus> {
9,352✔
999
        let _d_p: &String = &self._d_p;
9,352✔
1000
        def1n!("PathID {} {_d_p} poll()", self.path_id());
9,352✔
1001

1002
        summary_stat!(self.count_proc_polls += 1);
9,352✔
1003

1004
        match self.process.try_wait() {
9,352✔
1005
            Ok(Some(exit_status)) => {
2,128✔
1006
                if self.time_end.is_none() {
2,128✔
1007
                    self.time_end = Some(Instant::now());
11✔
1008
                    debug_assert_none!(self.exit_status, "exit_status should not be set yet");
11✔
1009
                }
2,117✔
1010
                // XXX: not sure if the subprocess returned ExitStatus would
1011
                //      change on later polls, so only set it once
1012
                let mut _was_exited = true;
2,128✔
1013
                if self.exit_status.is_none() {
2,128✔
1014
                    self.exit_status = Some(exit_status);
11✔
1015
                    _was_exited = false;
11✔
1016
                }
2,117✔
1017
                if exit_status.success() {
2,128✔
1018
                    def1x!("{_d_p} exited successfully{}", if _was_exited { " was" } else { "" });
2,115✔
1019
                } else if let Some(_code) = exit_status.code() {
13✔
1020
                    def1x!("{_d_p} exited with code {}{}", _code, if _was_exited { " was" } else { "" });
12✔
1021
                } else {
1022
                    def1x!("{_d_p} exited with status {:?}", exit_status);
1✔
1023
                }
1024
                self.pipes_exit_sender(ProcessStatus::Exited);
2,128✔
1025

1026
                Some(exit_status)
2,128✔
1027
            },
1028
            Ok(None) => {
1029
                // Process is still alive
1030
                def1x!("{_d_p} is still running");
7,224✔
1031

1032
                None
7,224✔
1033
            },
1034
            Err(err) => {
×
1035
                def1x!("{_d_p} poll error: {}", err);
×
1036
                self.error = Some(err);
×
1037
                self.pipes_exit_sender(ProcessStatus::Exited);
×
1038

1039
                None
×
1040
            }
1041
        }
1042
    }
9,352✔
1043

1044
    /// Accumulate stderr data up to a maximum number of bytes.
1045
    // TODO: [2025/12] isn't there a more rustic way to do this?
1046
    //       or a crate that does this?
1047
    fn stderr_all_add(&mut self, stderr_data: &Bytes) {
3,773✔
1048
        const MAX_STDERR_ALL_BYTES: usize = 1024;
1049
        match self.stderr_all.as_mut() {
3,773✔
1050
            Some(se_prior) => {
3,747✔
1051
                // store as much as possible of prior + new stderr data
1052
                if se_prior.len() + stderr_data.len() <= MAX_STDERR_ALL_BYTES {
3,747✔
1053
                    se_prior.extend(stderr_data.iter());
1,526✔
1054
                } else {
1,526✔
1055
                    // need to drop oldest prior data from the front
1056
                    let mut to_drop: usize = se_prior.len() + stderr_data.len() - MAX_STDERR_ALL_BYTES;
2,221✔
1057
                    while to_drop > 0 {
56,131✔
1058
                        se_prior.pop_front();
53,910✔
1059
                        to_drop -= 1;
53,910✔
1060
                    }
53,910✔
1061
                    // signify the front data has been cut off
1062
                    for b_ in "…".bytes() {
6,663✔
1063
                        se_prior.push_front(b_);
6,663✔
1064
                    }
6,663✔
1065
                    // separate prior data from new data
1066
                    se_prior.push_back(b'\n');
2,221✔
1067
                    se_prior.push_back(b'\n');
2,221✔
1068
                    // append the new data
1069
                    se_prior.extend(stderr_data.iter());
2,221✔
1070
                }
1071
            }
1072
            None => {
1073
                let mut v = VecDeque::<u8>::with_capacity(
26✔
1074
                    if stderr_data.len() > MAX_STDERR_ALL_BYTES {
26✔
1075
                        MAX_STDERR_ALL_BYTES
×
1076
                    } else {
1077
                        stderr_data.len()
26✔
1078
                    }
1079
                );
1080
                v.extend(stderr_data.iter());
26✔
1081
                self.stderr_all = Some(v);
26✔
1082
            }
1083
        }
1084
    }
3,773✔
1085

1086
    /// Send exit message to both stdout and stderr pipe threads.
1087
    /// May be called multiple times but only the first call has effect.
1088
    fn pipes_exit_sender(&mut self, pe: ProcessStatus) {
2,235✔
1089
        if self.pipe_sent_exit {
2,235✔
1090
            return;
2,165✔
1091
        }
70✔
1092
        def2ñ!("{} pipes_exit_sender({:?}) (channels len {}, {})",
70✔
1093
               self._d_p, pe, self.pipe_stdout.exit_sender.len(), self.pipe_stderr.exit_sender.len());
70✔
1094
        self.pipe_stdout.exit_sender.send(pe).unwrap_or(());
70✔
1095
        self.pipe_stderr.exit_sender.send(pe).unwrap_or(());
70✔
1096
        self.pipe_sent_exit = true;
70✔
1097
    }
2,235✔
1098

1099
    /// Write to `input_data` then read from the Python process stdout and
1100
    /// stderr.
1101
    /// Returns (`exited`, `stdout`, `stderr`).
1102
    ///
1103
    /// The stderr_all field accumulates all stderr data read so far. This is to help
1104
    /// when some error occurs in the Python process but the process has not yet exited
1105
    /// and then later calls to `read` find the process has exited. The earlier writes to
1106
    /// stderr are preserved in stderr_all because they often have the crucial error
1107
    /// information e.g. a Python stack trace.
1108
    pub fn write_read(&mut self, input_data: Option<&[u8]>) -> (bool, Option<Bytes>, Option<Bytes>) {
9,317✔
1109
        self.write_read_impl(input_data, None)
9,317✔
1110
            .expect("write_read without cancellation cannot be cancelled")
9,317✔
1111
    }
9,317✔
1112

1113
    /// Cancellation-aware variant of [`Self::write_read`].
1114
    ///
1115
    /// Returns `None` after terminating and reaping the Python process when
1116
    /// `cancel` becomes `true`.
1117
    pub fn write_read_cancel(
1✔
1118
        &mut self,
1✔
1119
        input_data: Option<&[u8]>,
1✔
1120
        cancel: &AtomicBool,
1✔
1121
    ) -> Option<(bool, Option<Bytes>, Option<Bytes>)> {
1✔
1122
        self.write_read_impl(input_data, Some(cancel))
1✔
1123
    }
1✔
1124

1125
    fn write_read_impl(
9,318✔
1126
        &mut self,
9,318✔
1127
        input_data: Option<&[u8]>,
9,318✔
1128
        cancel: Option<&AtomicBool>,
9,318✔
1129
    ) -> Option<(bool, Option<Bytes>, Option<Bytes>)> {
9,318✔
1130
        if cancel.is_some_and(|cancel_| cancel_.load(Ordering::Relaxed)) {
9,318✔
1131
            if let Err(_err) = self.terminate() {
×
1132
                de_err!("Failed to terminate Python process {}: {}", self.pid_, _err);
×
1133
            }
×
1134
            return None;
×
1135
        }
9,318✔
1136

1137
        let _len = input_data.unwrap_or(&[]).len();
9,318✔
1138
        def1n!("{} input_data: {} bytes", self._d_p, _len);
9,318✔
1139

1140
        if let Some(_exit_status) = self.poll() {
9,318✔
1141
            def1o!("{} already exited before read", self._d_p);
2,127✔
1142
        }
7,191✔
1143

1144
        // write string, read from stdout and stderr after poll as there may still be data to read
1145
        // even if the process has exited
1146

1147
        // write to stdin
1148
        if !self.exited() {
9,318✔
1149
            if let Some(input_data_) = input_data {
7,191✔
1150
                if !input_data_.is_empty() {
28✔
1151
                    match self.process.stdin.as_mut() {
28✔
1152
                        Some(stdin) => {
28✔
1153
                            def1o!(
28✔
1154
                                "{} writing {} bytes to stdin (\"{}\")",
1155
                                self._d_p,
1156
                                input_data_.len(),
28✔
1157
                                buffer_to_string_noraw(&input_data_[..input_data_.len().min(10)]).to_string()
28✔
1158
                            );
1159
                            match stdin.write(input_data_) {
28✔
1160
                                Ok(_len) => {
28✔
1161
                                    summary_stat!(self.count_proc_writes += 1);
28✔
1162
                                    def1o!(
28✔
1163
                                        "{} wrote {} bytes to stdin, expected {} bytes",
1164
                                        self._d_p, _len, input_data_.len()
28✔
1165
                                    );
1166
                                }
1167
                                Err(_err) => {
×
1168
                                    de_err!("Error writing to Python process {} stdin: {:?}", self.pid_, _err);
×
1169
                                    self.pipes_exit_sender(ProcessStatus::Exited);
×
1170
                                }
×
1171
                            }
1172
                        }
1173
                        None => {
×
1174
                            de_err!("{} stdin is None", self._d_p);
×
1175
                        }
×
1176
                    }
1177
                } else {
1178
                    def1o!("{} no stdin data to write", self._d_p);
×
1179
                }
1180
            }
7,163✔
1181
        } else {
1182
            def1o!("{} has exited; skip writing to stdin", self._d_p);
2,127✔
1183
        }
1184

1185
        let _d_p: &String = &self._d_p;
9,318✔
1186
        // use select to block until either channel signals data is available
1187
        let mut sel = Select::new();
9,318✔
1188
        let mut _sel_counts: usize = 0;
9,318✔
1189
        let sel_out: usize = if !self.pipe_stdout_eof {
9,318✔
1190
            let id = sel.recv(&self.pipe_stdout.chunk_receiver) + 1; // avoid zero id
9,178✔
1191
            def1o!("{_d_p} select recv(&pipe_stdout.chunk_receiver)");
9,178✔
1192
            _sel_counts += 1;
9,178✔
1193
            id
9,178✔
1194
        } else { 0 };
140✔
1195
        let sel_err: usize = if !self.pipe_stderr_eof {
9,318✔
1196
            let id = sel.recv(&self.pipe_stderr.chunk_receiver) + 1; // avoid zero id
7,325✔
1197
            def1o!("{_d_p} select recv(&pipe_stderr.chunk_receiver)");
7,325✔
1198
            _sel_counts += 1;
7,325✔
1199
            id
7,325✔
1200
        } else { 0 };
1,993✔
1201

1202
        if sel_out == 0 && sel_err == 0 {
9,318✔
1203
            def1o!("{_d_p} both stdout and stderr EOF; return");
86✔
1204
            return Some((self.exited_exhausted(), None, None));
86✔
1205
        }
9,232✔
1206

1207
        def1o!("{_d_p} wait on {} selects…", _sel_counts);
9,232✔
1208
        let d1: Instant = Instant::now();
9,232✔
1209
        let sel_oper = match cancel {
9,232✔
1210
            None => sel.select(),
9,231✔
1211
            Some(cancel_) => loop {
1✔
1212
                if cancel_.load(Ordering::Relaxed) {
7✔
1213
                    summary_stat!(self.duration_proc_wait += d1.elapsed());
1✔
1214
                    drop(sel);
1✔
1215
                    if let Err(_err) = self.terminate() {
1✔
1216
                        de_err!("Failed to terminate Python process {}: {}", self.pid_, _err);
×
1217
                    }
1✔
1218
                    return None;
1✔
1219
                }
6✔
1220
                match sel.select_timeout(RECV_TIMEOUT) {
6✔
1221
                    Ok(sel_oper) => break sel_oper,
×
1222
                    Err(_) => continue,
6✔
1223
                }
1224
            },
1225
        };
1226
        summary_stat!(self.duration_proc_wait += d1.elapsed());
9,231✔
1227
        let sel_index: usize = sel_oper.index() + 1; // avoid zero index
9,231✔
1228
        def1o!("{_d_p} selected {}", sel_index);
9,231✔
1229

1230
        // sanity check `*_stream_eof` is not inconsistent with channel readiness
1231
        if cfg!(any(debug_assertions,test)) {
9,231✔
1232
            if sel_index == sel_out && self.pipe_stdout_eof {
9,231✔
1233
                de_wrn!("pipe_stdout_eof should not be false if sel_out is ready");
×
1234
            }
9,231✔
1235
            if sel_index == sel_err && self.pipe_stderr_eof {
9,231✔
1236
                de_wrn!("pipe_stderr_eof should not be false if sel_err is ready");
×
1237
            }
9,231✔
1238
        }
×
1239

1240
        let mut stdout_data: Option<Bytes> = None;
9,231✔
1241
        let mut stderr_data: Option<Bytes> = None;
9,231✔
1242

1243
        // avoid borrow-checker conflicts
1244
        let _d_p = ();
9,231✔
1245

1246
        match sel_index {
9,231✔
1247
            // TODO: combine these matches since they are nearly identical?
1248
            //       though stdout might be treated differently from stderr?
1249
            //       maybe the messages passed back to main thread should distinguish
1250
            //       between stdout and stderr ?
1251
            i if i == sel_out && sel_out != 0 => {
9,231✔
1252
                // read stdout
1253
                summary_stat!(self.pipe_channel_max_stdout =
5,410✔
1254
                    max(
5,410✔
1255
                        self.pipe_channel_max_stdout,
5,410✔
1256
                        self.pipe_stdout.chunk_receiver.len() as Count
5,410✔
1257
                    )
5,410✔
1258
                );
1259
                def1o!("{} recv(&pipe_stdout.chunk_receiver)…", self._d_p);
5,410✔
1260
                summary_stat!(self.count_pipe_recv_stdout += 1);
5,410✔
1261
                match sel_oper.recv(&self.pipe_stdout.chunk_receiver) {
5,410✔
1262
                    Ok(remote_result) => {
5,410✔
1263
                        match remote_result {
5,410✔
1264
                            Ok(piped_line) => {
5,410✔
1265
                                match piped_line {
5,410✔
1266
                                    PipedChunk::Chunk(chunk) => {
5,362✔
1267
                                        let len_ = chunk.len();
5,362✔
1268
                                        def1o!("{} received {} bytes from stdout", self._d_p, len_);
5,362✔
1269
                                        stdout_data = Some(Vec::with_capacity(len_ + 1));
5,362✔
1270
                                        let data = stdout_data.as_mut().unwrap();
5,362✔
1271
                                        data.extend_from_slice(chunk.as_slice());
5,362✔
1272
                                    }
1273
                                    PipedChunk::Continue => {
1274
                                        def1o!("{} stdout Continue", self._d_p);
11✔
1275
                                    }
1276
                                    PipedChunk::Done(reads, remaining_bytes) => {
37✔
1277
                                        summary_stat!(self.count_proc_reads_stdout = reads);
37✔
1278
                                        def1o!("{} stdout Done({} reads, {} remaining bytes)",
37✔
1279
                                               self._d_p, reads, remaining_bytes.len());
37✔
1280
                                        if !remaining_bytes.is_empty() {
37✔
1281
                                            stdout_data = Some(Vec::with_capacity(remaining_bytes.len() + 1));
×
1282
                                            let data = stdout_data.as_mut().unwrap();
×
1283
                                            data.extend_from_slice(remaining_bytes.as_slice());
×
1284
                                        }
37✔
1285
                                        self.pipe_stdout_eof = true;
37✔
1286
                                        self.pipes_exit_sender(ProcessStatus::Exited);
37✔
1287
                                    }
1288
                                }
1289
                            }
1290
                            Err(error) => {
×
1291
                                de_err!("Error reading from Python process {} stdout: {:?}", self.pid_, error);
×
1292
                                self.error = Some(error);
×
1293
                                self.pipe_stdout_eof = true;
×
1294
                                self.pipes_exit_sender(ProcessStatus::Exited);
×
1295
                            }
×
1296
                        }
1297
                    }
1298
                    Err(recverror) => {
×
1299
                        def1o!("{} stdout channel RecvError {}; set pipe_stdout_eof=true", self._d_p, recverror);
×
1300
                        self.error = Some(self.new_error_from_recverror(&recverror));
×
1301
                        self.pipe_stdout_eof = true;
×
1302
                        self.pipes_exit_sender(ProcessStatus::Exited);
×
1303
                    }
1304
                }
1305
            }
1306
            i if i == sel_err && sel_err != 0 => {
3,821✔
1307
                // read stderr
1308
                summary_stat!(self.pipe_channel_max_stderr =
3,821✔
1309
                    max(
3,821✔
1310
                        self.pipe_channel_max_stderr,
3,821✔
1311
                        self.pipe_stderr.chunk_receiver.len() as Count
3,821✔
1312
                    )
3,821✔
1313
                );
1314
                def1o!("{} recv(&pipe_stderr.chunk_receiver)…", self._d_p);
3,821✔
1315
                summary_stat!(self.count_pipe_recv_stderr += 1);
3,821✔
1316
                match sel_oper.recv(&self.pipe_stderr.chunk_receiver) {
3,821✔
1317
                    Ok(remote_result) => {
3,821✔
1318
                        match remote_result {
3,821✔
1319
                            Ok(piped_line) => {
3,821✔
1320
                                match piped_line {
3,821✔
1321
                                    PipedChunk::Chunk(chunk) => {
3,773✔
1322
                                        let len_ = chunk.len();
3,773✔
1323
                                        def1o!("{} received {} bytes from stderr", self._d_p, len_);
3,773✔
1324
                                        let mut data: Bytes = Bytes::with_capacity(len_);
3,773✔
1325
                                        data.extend_from_slice(chunk.as_slice());
3,773✔
1326
                                        self.stderr_all_add(&data);
3,773✔
1327
                                        stderr_data = Some(data);
3,773✔
1328
                                    }
1329
                                    PipedChunk::Continue => {
1330
                                        def1o!("{} stderr Continue", self._d_p);
11✔
1331
                                    }
1332
                                    PipedChunk::Done(reads, remaining_bytes) => {
37✔
1333
                                        summary_stat!(self.count_proc_reads_stderr = reads);
37✔
1334
                                        def1o!("{} stderr Done({} reads, {} remaining bytes)",
37✔
1335
                                               self._d_p, reads, remaining_bytes.len());
37✔
1336
                                        if !remaining_bytes.is_empty() {
37✔
1337
                                            let mut data: Bytes = Bytes::with_capacity(remaining_bytes.len());
×
1338
                                            data.extend_from_slice(remaining_bytes.as_slice());
×
1339
                                            self.stderr_all_add(&data);
×
1340
                                            stderr_data = Some(data);
×
1341
                                        }
37✔
1342
                                        self.pipe_stderr_eof = true;
37✔
1343
                                        self.pipes_exit_sender(ProcessStatus::Exited);
37✔
1344
                                    }
1345
                                }
1346
                            }
1347
                            Err(error) => {
×
1348
                                de_err!("Error reading from Python process {} stderr: {:?}", self.pid_, error);
×
1349
                                self.error = Some(error);
×
1350
                                self.pipe_stderr_eof = true;
×
1351
                                self.pipes_exit_sender(ProcessStatus::Exited);
×
1352
                            }
×
1353
                        }
1354
                    }
1355
                    Err(_err) => {
×
1356
                        def1o!("{} stderr channel RecvError {}; set pipe_stderr_eof=true", self._d_p, _err);
×
1357
                        self.error = Some(self.new_error_from_recverror(&_err));
×
1358
                        self.pipe_stderr_eof = true;
×
1359
                        self.pipes_exit_sender(ProcessStatus::Exited);
×
1360
                    }
1361
                }
1362
            }
1363
            _i => {
×
1364
                def1o!("{} selected unknown index {}", self._d_p, _i);
×
1365
            }
1366
        }
1367

1368
        def1x!("{} return ({}, stdout bytes {:?} (eof? {}), stderr bytes {:?} (eof? {}))",
9,231✔
1369
                self._d_p,
1370
                self.exited_exhausted(),
9,231✔
1371
                stdout_data.as_ref().unwrap_or(&vec![]).len(),
9,231✔
1372
                self.pipe_stdout_eof,
1373
                stderr_data.as_ref().unwrap_or(&vec![]).len(),
9,231✔
1374
                self.pipe_stderr_eof
1375
        );
1376

1377
        Some((self.exited_exhausted(), stdout_data, stderr_data))
9,231✔
1378
    }
9,318✔
1379

1380
    /// Has a `subprocess::poll` or `subprocess::wait` already returned an `ExitStatus`?
1381
    pub fn exited(&self) -> bool {
9,468✔
1382
        self.exit_status.is_some()
9,468✔
1383
    }
9,468✔
1384

1385
    /// Has a `subprocess::poll` or `subprocess::wait` already returned an `ExitStatus`
1386
    /// *and* have both stdout and stderr streams reached EOF?
1387
    pub fn exited_exhausted(&self) -> bool {
18,698✔
1388
        self.exit_status.is_some() && self.pipe_stdout_eof && self.pipe_stderr_eof
18,698✔
1389
    }
18,698✔
1390

1391
    /// Terminates the Python process if it is still running and waits for it
1392
    /// to be reaped. Calling this after the process has exited is a no-op.
1393
    pub fn terminate(&mut self) -> Result<ExitStatus> {
34✔
1394
        self.process.stdin.take();
34✔
1395

1396
        if let Some(exit_status) = self.poll() {
34✔
1397
            return Ok(exit_status);
1✔
1398
        }
33✔
1399

1400
        let kill_result = self.process.kill();
33✔
1401
        self.pipes_exit_sender(ProcessStatus::Exited);
33✔
1402
        let wait_result = self.wait();
33✔
1403

1404
        match (kill_result, wait_result) {
33✔
1405
            (_, Ok(exit_status)) => Ok(exit_status),
33✔
1406
            (Err(kill_error), Err(wait_error)) => Err(Error::new(
×
1407
                wait_error.kind(),
×
1408
                format!(
×
1409
                    "Python process {} kill() failed: {}; wait() failed: {}",
×
1410
                    self.pid_, kill_error, wait_error
×
1411
                ),
×
1412
            )),
×
1413
            (Ok(_), Err(wait_error)) => Err(wait_error),
×
1414
        }
1415
    }
34✔
1416

1417
    /// Wait for the Python process to exit.
1418
    /// If the process has already exited then return the saved `ExitStatus`.
1419
    pub fn wait(&mut self) -> Result<ExitStatus> {
33✔
1420
        let _d_p: &String = &self._d_p;
33✔
1421
        if self.exited() {
33✔
1422
            def1ñ!("{_d_p} exited; return {:?}",
×
1423
                   self.exit_status.unwrap());
×
1424
            return Ok(self.exit_status.unwrap());
×
1425
        }
33✔
1426
        def1n!("{_d_p} wait()");
33✔
1427
        let d1: Instant = Instant::now();
33✔
1428
        // XXX: should `wait` be passed a timeout?
1429
        let rc = self.process.wait();
33✔
1430
        summary_stat!(self.duration_proc_wait += d1.elapsed());
33✔
1431
        match rc {
33✔
1432
            Ok(exit_status) => {
33✔
1433
                debug_assert_none!(self.time_end, "time_end should not be set since exited() was false");
33✔
1434
                self.time_end = Some(Instant::now());
33✔
1435
                // prefer the first saved `ExitStatus`
1436
                // however setting `self.exit_status` again is never expected to happen
1437
                if self.exit_status.is_none() {
33✔
1438
                    self.exit_status = Some(exit_status);
33✔
1439
                } else {
33✔
1440
                    debug_panic!("Python process {} exit_status is already set! {:?}",
×
1441
                                 self.pid_, self.exit_status)
1442
                }
1443
                def1x!("{_d_p} wait returned {:?}", exit_status);
33✔
1444
                return Ok(self.exit_status.unwrap());
33✔
1445
            }
1446
            Err(error) => {
×
1447
                de_err!("{_d_p} wait returned {:?}", error);
×
1448
                def1x!("{_d_p} error wait returned {:?}", error);
×
1449
                return Result::Err(
×
1450
                    Error::new(
×
1451
                        error.kind(),
×
1452
                        format!("Python process {} wait() failed: {}", self.pid_, error),
×
1453
                    )
×
1454
                );
×
1455
            }
1456
        }
1457
    }
33✔
1458

1459
    /// Total run duration of the process; imprecise as the end time is merely the first `Instant`
1460
    /// a `subprocess::poll` or `subprocess::wait` returned an `ExitStatus`.
1461
    /// Precise enough for most needs.
1462
    ///
1463
    /// Returns `None` if the process is not yet known to have exited.
1464
    pub fn duration(&self) -> Option<Duration> {
26✔
1465
        self.time_end?;
26✔
1466
        match self.time_end {
26✔
1467
            Some(time_end) => Some(time_end - self.time_beg),
26✔
1468
            None => {
1469
                debug_panic!("Python process {} is exited but time_end is None", self.pid_);
×
1470

1471
                None
×
1472
            }
1473
        }
1474
    }
26✔
1475

1476
    /// Read from the `PyRunner`, print the output, and wait for it to finish.
1477
    /// Do not call `read` or `wait` before calling this function.
1478
    /// Helper for simple Python commands that do not require interaction.
1479
    ///
1480
    /// This is not expected to be run as part of normal operation of
1481
    /// `s4`. This is for special operations such as `s4 --venv`. It prints
1482
    /// to stdout. In normal operation of `s4`, only the main
1483
    /// thread should print to stdout.
1484
    pub fn run(&mut self, print_argv: bool, print_stdout: bool, print_stderr: bool) -> Result<(Bytes, Bytes)> {
26✔
1485
        let _d_p: &String = &self._d_p;
26✔
1486
        def1n!("{_d_p}, print_argv={}", print_argv);
26✔
1487

1488
        if self.exited() {
26✔
1489
            debug_panic!("{_d_p} already exited!");
×
1490
            return Result::Ok((Bytes::with_capacity(0), Bytes::with_capacity(0)));
×
1491
        }
26✔
1492

1493
        if print_argv {
26✔
1494
            // print the command executed
1495

1496
            // get the prompt, prefer to use PS4 env var
1497
            let prompt = match env::var("PS4") {
10✔
1498
                Ok(s) => {
×
1499
                    if s.is_empty() {
×
1500
                        String::from(PROMPT_DEFAULT)
×
1501
                    } else {
1502
                        s
×
1503
                    }
1504
                },
1505
                Err(_) => String::from(PROMPT_DEFAULT),
10✔
1506
            };
1507
            // print the command, escaping each argument
1508
            let mut lock = stdout().lock();
10✔
1509
            // TODO: [2025/11/17] handle write returning an error?
1510
            let _ = lock.write(prompt.as_bytes());
10✔
1511
            for arg in self.argv.iter() {
52✔
1512
                let es = escape(arg.into());
52✔
1513
                let _ = lock.write(es.as_bytes());
52✔
1514
                let _ = lock.write(b" ");
52✔
1515
            }
52✔
1516
            let _ = lock.write(b"\n");
10✔
1517
            let _ = lock.flush();
10✔
1518
        }
16✔
1519

1520
        let rc = self.process.wait();
26✔
1521
        def1o!("{_d_p} wait() returned {:?}", rc);
26✔
1522
        match rc {
26✔
1523
            Ok(exit_status) => {
26✔
1524
                debug_assert_none!(self.time_end, "time_end should not be set since exited() was false");
26✔
1525
                self.time_end = Some(Instant::now());
26✔
1526
                // prefer the first saved `ExitStatus`
1527
                // however setting `self.exit_status` again is never expected to happen
1528
                if self.exit_status.is_none() {
26✔
1529
                    self.exit_status = Some(exit_status);
26✔
1530
                } else {
26✔
1531
                    debug_panic!("{_d_p} exit_status is already set! {:?}",
×
1532
                                 self.exit_status)
1533
                }
1534
            }
1535
            Err(error) => {
×
1536
                de_err!("{_d_p} wait returned {:?}", error);
×
1537
                def1x!("{_d_p} error wait returned {:?}", error);
×
1538
                return Result::Err(
×
1539
                    Error::new(
×
1540
                        error.kind(),
×
1541
                        format!("Python process {} wait() failed: {}", self.pid_, error),
×
1542
                    )
×
1543
                );
×
1544
            }
1545
        }
1546

1547
        let mut stdout_data: Bytes = Bytes::with_capacity(2056);
26✔
1548
        let mut stderr_data: Bytes = Bytes::with_capacity(1024);
26✔
1549

1550
        // remove _d_p reference to avoid borrow checker conflict that would occur in the loop
1551
        let _d_p = ();
26✔
1552

1553
        // print remaining stdout and stderr
1554
        loop {
1555
            let (
1556
                _exited,
2,008✔
1557
                out_data,
2,008✔
1558
                err_data,
2,008✔
1559
            ) = self.write_read(None);
2,008✔
1560
            def1o!("{} exited? {:?}", self._d_p, _exited);
2,008✔
1561
            // print stdout to stdout
1562
            if let Some(data) = out_data {
2,008✔
1563
                stdout_data.extend_from_slice(data.as_slice());
1,903✔
1564
                if ! data.is_empty() && print_stdout {
1,903✔
1565
                    let mut lock = stdout().lock();
1,879✔
1566
                    let _ = lock.write(data.as_slice());
1,879✔
1567
                    let _ = lock.flush();
1,879✔
1568
                }
1,879✔
1569
            }
105✔
1570
            // print stderr to stderr
1571
            if let Some(data) = err_data {
2,008✔
1572
                stderr_data.extend_from_slice(data.as_slice());
53✔
1573
                if ! data.is_empty() && print_stderr {
53✔
1574
                    let mut lock = stderr().lock();
27✔
1575
                    let _ = lock.write(data.as_slice());
27✔
1576
                    let _ = lock.flush();
27✔
1577
                }
27✔
1578
            }
1,955✔
1579
            if _exited {
2,008✔
1580
                break;
26✔
1581
            }
1,982✔
1582
        }
1583

1584
        let _d_p: &String = &self._d_p;
26✔
1585

1586
        match self.exit_status {
26✔
1587
            Some(status) => {
26✔
1588
                if ! status.success() {
26✔
1589
                    let s = format!("Python process {} exited with non-zero status {:?}", self.pid_, status);
×
1590
                    def1x!("{_d_p} {}", s);
×
1591
                    return Result::Err(
×
1592
                        Error::other(s)
×
1593
                    )
×
1594
                }
26✔
1595
            }
1596
            None => {
1597
                debug_panic!("{_d_p} exit_status is None after wait()");
×
1598
            }
1599
        }
1600

1601
        if print_argv {
26✔
1602
            let mut lock = stdout().lock();
10✔
1603
            let _ = lock.write(b"\n");
10✔
1604
            let _ = lock.flush();
10✔
1605
        }
16✔
1606

1607
        def1x!("{_d_p} duration {:?}, return Ok(stdout {} bytes, stderr {} bytes)",
26✔
1608
            self.duration(), stdout_data.len(), stderr_data.len());
26✔
1609

1610
        Result::Ok((stdout_data, stderr_data))
26✔
1611
    }
26✔
1612

1613
    /// Create a `PyRunner`, run it, return Ok or Err.
1614
    ///
1615
    /// This calls `PyRunner::new()` and then `PyRunner::run()`.
1616
    /// See `run()` regarding its intended use.
1617
    pub fn run_once(
17✔
1618
        python_to_use: PythonToUse,
17✔
1619
        pipe_sz: PipeSz,
17✔
1620
        recv_timeout: Duration,
17✔
1621
        chunk_delimiter: ChunkDelimiter,
17✔
1622
        python_path: Option<FPath>,
17✔
1623
        argv: Vec<&str>,
17✔
1624
        print_argv: bool
17✔
1625
    ) -> Result<(PyRunner, Bytes, Bytes)> {
17✔
1626
        def1ñ!("({:?}, {:?}, {:?})", python_to_use, python_path, argv);
17✔
1627
        let mut pyrunner = match PyRunner::new(
17✔
1628
            python_to_use,
17✔
1629
            PathId::default(),
17✔
1630
            pipe_sz,
17✔
1631
            recv_timeout,
17✔
1632
            Some(chunk_delimiter),
17✔
1633
            None,
17✔
1634
            python_path,
17✔
1635
            argv,
17✔
1636
        ) {
17✔
1637
            Ok(pyrunner) => pyrunner,
17✔
1638
            Err(err) => {
×
1639
                def1x!("PyRunner::new failed {:?}", err);
×
1640
                return Result::Err(err);
×
1641
            }
1642
        };
1643

1644
        match pyrunner.run(print_argv, true, true) {
17✔
1645
            Ok((stdout_data, stderr_data)) => {
17✔
1646
                def1x!("PyRunner::run Ok");
17✔
1647

1648
                Result::Ok((pyrunner, stdout_data, stderr_data))
17✔
1649
            }
1650
            Err(err) => {
×
1651
                def1x!("PyRunner::run Error {:?}", err);
×
1652

1653
                Result::Err(err)
×
1654
            }
1655
        }
1656
    }
17✔
1657
}
1658

1659
impl Drop for PyRunner {
1660
    fn drop(&mut self) {
70✔
1661
        def2ñ!("PyRunner: PathID {} PID {} TID {}", self.path_id(), self.pid(), self.tid());
70✔
1662
        if !self.exited() && let Err(_err) = self.terminate() {
70✔
1663
            de_err!("Failed to terminate Python process {} during drop: {}", self.pid_, _err);
×
1664
        }
70✔
1665
    }
70✔
1666
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc