Skip to main content

callisto_cli/
runner.rs

1use std::io::Read;
2use std::path::Path;
3use std::process::Stdio;
4use std::time::{Duration, Instant};
5
6use callisto_model::{CommandError, CommandOutput, CommandRunner};
7
8/// Caps how much of a subprocess's stdout/stderr this process will retain
9/// in memory. The pipe is still drained in full past this point (bytes
10/// beyond the cap are read and discarded, never accumulated) so a chatty
11/// child can never deadlock waiting on a full OS pipe buffer -- this
12/// bounds memory, not duration (`PUBLISH_TIMEOUT_SECS` bounds that
13/// separately).
14const MAX_CAPTURED_OUTPUT_BYTES: usize = 10 * 1024 * 1024;
15
16pub struct CliCommandRunner;
17
18impl CommandRunner for CliCommandRunner {
19    fn run(&self, program: &str, args: &[&str], cwd: &Path) -> Result<CommandOutput, CommandError> {
20        let output = std::process::Command::new(program)
21            .args(args)
22            .current_dir(cwd)
23            .stdin(Stdio::null())
24            .stdout(Stdio::piped())
25            .stderr(Stdio::piped())
26            .output();
27
28        match output {
29            Ok(o) => {
30                let stderr = String::from_utf8_lossy(&o.stderr).into_owned();
31                // Print stderr to the terminal before constructing the output
32                // so the caller sees it even if they don't inspect the field.
33                if !stderr.is_empty() {
34                    eprint!("{stderr}");
35                }
36                Ok(CommandOutput {
37                    exit_code: o.status.code(),
38                    stdout: String::from_utf8_lossy(&o.stdout).into_owned(),
39                    stderr,
40                })
41            }
42            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(CommandError::NotFound {
43                program: program.to_string(),
44            }),
45            Err(e) => Err(CommandError::Io {
46                program: program.to_string(),
47                message: e.to_string(),
48            }),
49        }
50    }
51
52    fn run_with_timeout(
53        &self,
54        program: &str,
55        args: &[&str],
56        cwd: &Path,
57        timeout: Duration,
58    ) -> Result<CommandOutput, CommandError> {
59        run_with_timeout_impl(program, args, cwd, timeout, StderrMode::Live)
60    }
61
62    fn run_quiet(
63        &self,
64        program: &str,
65        args: &[&str],
66        cwd: &Path,
67        timeout: Duration,
68    ) -> Result<CommandOutput, CommandError> {
69        run_with_timeout_impl(program, args, cwd, timeout, StderrMode::Quiet)
70    }
71}
72
73/// Controls whether [`run_with_timeout_impl`]'s stderr reader thread prints
74/// each line live as it arrives. Captured `CommandOutput.stderr` is
75/// identical either way -- this only affects what's echoed to the
76/// terminal while the subprocess runs.
77#[derive(Clone, Copy, PartialEq, Eq)]
78enum StderrMode {
79    Live,
80    Quiet,
81}
82
83fn run_with_timeout_impl(
84    program: &str,
85    args: &[&str],
86    cwd: &Path,
87    timeout: Duration,
88    stderr_mode: StderrMode,
89) -> Result<CommandOutput, CommandError> {
90    let mut child = std::process::Command::new(program)
91        .args(args)
92        .current_dir(cwd)
93        .stdin(Stdio::null())
94        .stdout(Stdio::piped())
95        .stderr(Stdio::piped())
96        .spawn()
97        .map_err(|e| {
98            if e.kind() == std::io::ErrorKind::NotFound {
99                CommandError::NotFound {
100                    program: program.to_string(),
101                }
102            } else {
103                CommandError::Io {
104                    program: program.to_string(),
105                    message: e.to_string(),
106                }
107            }
108        })?;
109
110    // Read stdout/stderr in separate threads to prevent pipe-buffer deadlock
111    // when the child produces output while we wait for it to exit.
112    let stdout_handle = child.stdout.take().unwrap();
113    let stderr_handle = child.stderr.take().unwrap();
114
115    // The reader threads signal completion over a channel rather than
116    // being joined directly. Killing (or the natural exit of) the
117    // direct child does NOT close pipe fds held open by a descendant
118    // process that inherited them (common for npm/cargo/python publish
119    // lifecycle scripts spawning subprocesses). If that happens, the
120    // blocking `read()` inside these threads never returns. Using
121    // `recv_timeout` below lets us bound how long we wait for them
122    // without ever blocking the caller indefinitely.
123    let (stdout_tx, stdout_rx) = std::sync::mpsc::channel::<String>();
124    let (stderr_tx, stderr_rx) = std::sync::mpsc::channel::<String>();
125
126    std::thread::spawn(move || {
127        let mut reader = stdout_handle;
128        let mut captured: Vec<u8> = Vec::new();
129        let mut truncated = false;
130        let mut chunk = [0u8; 65536];
131        loop {
132            match reader.read(&mut chunk) {
133                Ok(0) => break,
134                Ok(n) => {
135                    // Bytes past the cap are still read (draining the
136                    // pipe so the child never blocks on a full buffer)
137                    // but not retained.
138                    if !truncated {
139                        let remaining = MAX_CAPTURED_OUTPUT_BYTES - captured.len();
140                        let take = n.min(remaining);
141                        captured.extend_from_slice(&chunk[..take]);
142                        if take < n {
143                            truncated = true;
144                        }
145                    }
146                }
147                Err(_) => break,
148            }
149        }
150        let mut text = String::from_utf8_lossy(&captured).into_owned();
151        if truncated {
152            text.push_str("\n...[output truncated]\n");
153        }
154        drop(stdout_tx.send(text));
155    });
156    // Stream stderr line-by-line so publish progress (cargo/npm/twine
157    // write to stderr) appears in real time rather than after the
158    // process exits, while capping the accumulated text returned to
159    // the caller. Deliberately reads raw bytes rather than using
160    // `BufReader::lines()`: that iterator has no size bound on a
161    // single line, so a flood with no newlines at all would still
162    // buffer unboundedly inside it before ever yielding a line to cap.
163    // `stderr_mode` gates only the live eprintln -- captured output is
164    // identical either way.
165    std::thread::spawn(move || {
166        let mut reader = stderr_handle;
167        let mut captured: Vec<u8> = Vec::new();
168        let mut truncated = false;
169        let mut pending_line: Vec<u8> = Vec::new();
170        let mut chunk = [0u8; 65536];
171        loop {
172            match reader.read(&mut chunk) {
173                Ok(0) => break,
174                Ok(n) => {
175                    let data = &chunk[..n];
176                    for &b in data {
177                        if b == b'\n' {
178                            if pending_line.last() == Some(&b'\r') {
179                                pending_line.pop();
180                            }
181                            if stderr_mode == StderrMode::Live {
182                                eprintln!("{}", String::from_utf8_lossy(&pending_line));
183                            }
184                            pending_line.clear();
185                        } else if pending_line.len() < MAX_CAPTURED_OUTPUT_BYTES {
186                            pending_line.push(b);
187                        }
188                    }
189                    if !truncated {
190                        let remaining = MAX_CAPTURED_OUTPUT_BYTES - captured.len();
191                        let take = data.len().min(remaining);
192                        captured.extend_from_slice(&data[..take]);
193                        if take < data.len() {
194                            truncated = true;
195                        }
196                    }
197                }
198                Err(_) => break,
199            }
200        }
201        if !pending_line.is_empty() {
202            if pending_line.last() == Some(&b'\r') {
203                pending_line.pop();
204            }
205            if stderr_mode == StderrMode::Live {
206                eprintln!("{}", String::from_utf8_lossy(&pending_line));
207            }
208        }
209        let mut text = String::from_utf8_lossy(&captured).into_owned();
210        if truncated {
211            text.push_str("\n...[output truncated]\n");
212        }
213        drop(stderr_tx.send(text));
214    });
215
216    // Grace period to wait for the reader threads after the direct
217    // child has exited (or been killed). Bounded so a lingering
218    // descendant holding the pipe open can never hang the caller.
219    const READER_GRACE: Duration = Duration::from_secs(3);
220
221    let deadline = Instant::now() + timeout;
222    let status = loop {
223        match child.try_wait().map_err(|e| CommandError::Io {
224            program: program.to_string(),
225            message: e.to_string(),
226        })? {
227            Some(s) => break s,
228            None => {
229                if Instant::now() >= deadline {
230                    drop(child.kill());
231                    drop(child.wait());
232                    // Reader threads are intentionally not joined here:
233                    // if a descendant process is still holding a pipe
234                    // fd open, join() could block forever. We wait a
235                    // bounded grace period for output, then abandon the
236                    // threads (they leak until the descendant
237                    // eventually exits and closes the fd -- a thread
238                    // leak, not a memory-safety issue).
239                    let stdout = stdout_rx.recv_timeout(READER_GRACE).ok();
240                    let stderr = stderr_rx.recv_timeout(READER_GRACE).ok();
241                    if stdout.is_none() || stderr.is_none() {
242                        eprintln!(
243                            "warning: `{program}` timed out and a descendant process \
244                                 appears to still hold its output pipes open; captured \
245                                 output may be incomplete"
246                        );
247                    }
248                    return Err(CommandError::TimedOut {
249                        program: program.to_string(),
250                        seconds: timeout.as_secs(),
251                    });
252                }
253                std::thread::sleep(Duration::from_millis(50));
254            }
255        }
256    };
257
258    // The direct child has exited on its own. Even so, a descendant
259    // process can still hold the pipe fds open (e.g. a backgrounded
260    // job spawned by a shell script), so bound the wait here too.
261    let stdout_result = stdout_rx.recv_timeout(READER_GRACE);
262    let stderr_result = stderr_rx.recv_timeout(READER_GRACE);
263    if stdout_result.is_err() || stderr_result.is_err() {
264        eprintln!(
265            "warning: `{program}` exited but a descendant process appears to still \
266                 hold its output pipes open; captured output may be incomplete"
267        );
268    }
269
270    Ok(CommandOutput {
271        exit_code: status.code(),
272        stdout: stdout_result.unwrap_or_default(),
273        stderr: stderr_result.unwrap_or_default(),
274    })
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn run_with_timeout_kills_slow_process_and_returns_timed_out() {
283        let runner = CliCommandRunner;
284        // sleep for 1 second with a 100ms timeout — process must be killed.
285        let err = runner
286            .run_with_timeout("sleep", &["1"], std::path::Path::new("."), Duration::from_millis(100))
287            .unwrap_err();
288        assert!(
289            matches!(err, CommandError::TimedOut { .. }),
290            "expected TimedOut, got: {err:?}"
291        );
292    }
293
294    #[test]
295    fn run_with_timeout_returns_output_for_fast_process() {
296        let runner = CliCommandRunner;
297        let out = runner
298            .run_with_timeout("true", &[], std::path::Path::new("."), Duration::from_secs(5))
299            .unwrap();
300        assert!(out.success());
301    }
302
303    /// Regression test for a hung-descendant deadlock: killing the direct
304    /// child does not close pipe fds inherited by a grandchild process that
305    /// outlives it. The direct child (`sh`) exits almost immediately, but a
306    /// backgrounded grandchild (`sleep 30`) keeps stderr open well past
307    /// that. If the reader threads are joined unconditionally, this call
308    /// hangs for ~30s regardless of the requested timeout. The fix bounds
309    /// how long we wait on the reader threads so the call always returns
310    /// promptly.
311    #[test]
312    fn run_with_timeout_bounds_reader_join_when_descendant_holds_pipe_open() {
313        let runner = CliCommandRunner;
314        let start = Instant::now();
315        let result = runner.run_with_timeout(
316            "sh",
317            &["-c", "sleep 30 >&2 & exit 0"],
318            std::path::Path::new("."),
319            Duration::from_secs(2),
320        );
321        let elapsed = start.elapsed();
322        assert!(
323            elapsed < Duration::from_secs(6),
324            "run_with_timeout must not block on a descendant process holding \
325             stdio pipes open; took {elapsed:?}, result: {result:?}"
326        );
327        assert!(
328            result.is_ok(),
329            "expected Ok despite a lingering descendant holding the pipe open, got: {result:?}"
330        );
331    }
332
333    /// F-007: subprocess stderr must be streamed line-by-line rather than
334    /// buffered until process exit. The fix: switch from `read_to_string`
335    /// (accumulates everything) to a `BufReader::lines` loop that emits each
336    /// line to the terminal immediately while still accumulating the full
337    /// text for caller analysis.
338    ///
339    /// We verify that `CommandOutput::stderr` still contains the full stderr
340    /// text after streaming (so callers can still classify errors from it).
341    #[test]
342    fn run_with_timeout_stderr_is_fully_captured_after_streaming() {
343        let runner = CliCommandRunner;
344        // A process that prints multiple lines to stderr, one at a time.
345        // Using sh -c with printf to emit to stderr.
346        let out = runner
347            .run_with_timeout(
348                "sh",
349                &["-c", "echo line1 >&2; echo line2 >&2"],
350                std::path::Path::new("."),
351                Duration::from_secs(5),
352            )
353            .unwrap();
354
355        assert!(out.success());
356        assert!(
357            out.stderr.contains("line1") && out.stderr.contains("line2"),
358            "stderr must contain all emitted lines even when streamed; got: {:?}",
359            out.stderr
360        );
361    }
362
363    /// Regression test for the timeout-KILL branch's grace-period logic
364    /// (runner.rs:124-146), structurally similar to but independently
365    /// written from the normal-exit branch's (runner.rs:153-163, covered
366    /// by `run_with_timeout_bounds_reader_join_when_descendant_holds_pipe_open`).
367    /// That test's child (`sh`) exits almost immediately, only exercising
368    /// the exit branch. Here the child (`sh -c "sleep 30 >&2 & sleep 10"`)
369    /// outlives the timeout, so `run_with_timeout` must actually kill it,
370    /// while a backgrounded grandchild (`sleep 30 >&2`) keeps stderr open
371    /// independently of the kill.
372    ///
373    /// The warning is emitted via `eprintln!` inside `run_with_timeout`
374    /// (not captured in `CommandOutput`), so this test re-execs the binary
375    /// as a child with `--nocapture` to capture real OS-level stderr
376    /// instead of having it swallowed by the outer harness.
377    #[test]
378    fn run_with_timeout_warns_on_timeout_branch_when_descendant_holds_pipe_open() {
379        const CHILD_ENV: &str = "CALLISTO_RUN_TIMEOUT_KILL_CHILD";
380
381        if std::env::var(CHILD_ENV).is_ok() {
382            let runner = CliCommandRunner;
383            // `sh` on this platform forks a separate process for the
384            // foreground `sleep 10` rather than exec-replacing itself (it
385            // must remain able to reap the backgrounded job), so killing
386            // the direct child does not kill either descendant. Both the
387            // backgrounded `sleep 30` and the foreground `sleep 10` inherit
388            // the piped stdout/stderr fds unless redirected away, so each
389            // is redirected explicitly: `sleep 30`'s stdout goes to
390            // /dev/null so only its stderr lingers (the condition under
391            // test), and `sleep 10`'s stdout/stderr both go to /dev/null so
392            // it doesn't *also* hold either pipe open, which would stack a
393            // second sequential 3s grace wait on top of the first.
394            drop(runner.run_with_timeout(
395                "sh",
396                &["-c", "sleep 30 >&2 1>/dev/null & sleep 10 >/dev/null 2>&1"],
397                std::path::Path::new("."),
398                Duration::from_millis(800),
399            ));
400            return;
401        }
402
403        let exe = std::env::current_exe().expect("current_exe should be available in tests");
404        let start = Instant::now();
405        let output = std::process::Command::new(exe)
406            .arg("--exact")
407            .arg("runner::tests::run_with_timeout_warns_on_timeout_branch_when_descendant_holds_pipe_open")
408            .arg("--nocapture")
409            .env(CHILD_ENV, "1")
410            .output()
411            .expect("failed to re-exec test binary");
412        let elapsed = start.elapsed();
413
414        assert!(
415            elapsed < Duration::from_secs(6),
416            "run_with_timeout's timeout-kill branch must not block past its \
417             bounded grace period even when a descendant holds stdio pipes \
418             open; took {elapsed:?}"
419        );
420
421        let stderr = String::from_utf8_lossy(&output.stderr);
422        assert!(
423            stderr.contains("timed out and a descendant process appears to still hold its output pipes open"),
424            "expected the timeout branch's specific warning text in child stderr, got: {stderr}"
425        );
426    }
427
428    /// A subprocess that writes far more than `MAX_CAPTURED_OUTPUT_BYTES`
429    /// to stdout must not have all of it retained in `CommandOutput` (an
430    /// unbounded accumulation is a memory-exhaustion DoS before the
431    /// wall-clock publish timeout ever fires), but the pipe must still be
432    /// drained in full -- proven here by the call completing well within
433    /// its timeout instead of hanging (a reader that stops calling
434    /// `read()` after the cap would leave the child blocked writing to a
435    /// full OS pipe buffer).
436    #[test]
437    fn run_with_timeout_caps_accumulated_stdout_and_still_drains_the_pipe() {
438        let runner = CliCommandRunner;
439        let over_cap = MAX_CAPTURED_OUTPUT_BYTES + 1_000_000;
440        let start = Instant::now();
441        let out = runner
442            .run_with_timeout(
443                "sh",
444                &["-c", &format!("head -c {over_cap} /dev/zero")],
445                std::path::Path::new("."),
446                Duration::from_secs(30),
447            )
448            .unwrap();
449        let elapsed = start.elapsed();
450
451        assert!(out.success());
452        assert!(
453            elapsed < Duration::from_secs(15),
454            "must not hang waiting for the child to finish writing past the cap; took {elapsed:?}"
455        );
456        assert!(
457            out.stdout.len() < over_cap,
458            "captured stdout must be bounded, not the full {over_cap} bytes written; got {} bytes",
459            out.stdout.len()
460        );
461        assert!(
462            out.stdout.contains("[output truncated]"),
463            "truncated output must say so"
464        );
465    }
466
467    /// Same as the stdout case, but for stderr -- and deliberately with NO
468    /// newlines at all, so the flood can't be capped by a per-line
469    /// mechanism (a `BufReader::lines()`-based reader has no size bound on
470    /// a single line and would buffer the entire flood internally before
471    /// ever handing a line back, defeating any cap applied only after a
472    /// line is yielded).
473    #[test]
474    fn run_with_timeout_caps_accumulated_stderr_with_no_newlines_and_still_drains_the_pipe() {
475        let runner = CliCommandRunner;
476        let over_cap = MAX_CAPTURED_OUTPUT_BYTES + 1_000_000;
477        let start = Instant::now();
478        let out = runner
479            .run_with_timeout(
480                "sh",
481                &["-c", &format!("head -c {over_cap} /dev/zero >&2")],
482                std::path::Path::new("."),
483                Duration::from_secs(30),
484            )
485            .unwrap();
486        let elapsed = start.elapsed();
487
488        assert!(out.success());
489        assert!(
490            elapsed < Duration::from_secs(15),
491            "must not hang waiting for the child to finish writing past the cap; took {elapsed:?}"
492        );
493        assert!(
494            out.stderr.len() < over_cap,
495            "captured stderr must be bounded, not the full {over_cap} bytes written; got {} bytes",
496            out.stderr.len()
497        );
498        assert!(
499            out.stderr.contains("[output truncated]"),
500            "truncated output must say so"
501        );
502    }
503
504    /// Output comfortably under the cap must be completely unaffected --
505    /// no truncation marker, byte-for-byte length preserved.
506    #[test]
507    fn run_with_timeout_does_not_truncate_output_under_the_cap() {
508        let runner = CliCommandRunner;
509        let out = runner
510            .run_with_timeout(
511                "sh",
512                &["-c", "printf 'hello stdout'; printf 'hello stderr' >&2"],
513                std::path::Path::new("."),
514                Duration::from_secs(5),
515            )
516            .unwrap();
517
518        assert_eq!(out.stdout, "hello stdout");
519        assert!(!out.stdout.contains("[output truncated]"));
520        assert!(out.stderr.contains("hello stderr"));
521        assert!(!out.stderr.contains("[output truncated]"));
522    }
523
524    /// `run_quiet` must still fully capture stderr into the returned
525    /// `CommandOutput` -- only the live terminal echo is suppressed, not
526    /// the capture callers rely on to classify probe results (e.g.
527    /// `npm view`'s 404-shaped "not published yet" text).
528    #[test]
529    fn run_quiet_still_captures_stderr_in_output() {
530        let runner = CliCommandRunner;
531        let out = runner
532            .run_quiet(
533                "sh",
534                &["-c", "echo captured-probe-text >&2"],
535                std::path::Path::new("."),
536                Duration::from_secs(5),
537            )
538            .unwrap();
539        assert!(
540            out.stderr.contains("captured-probe-text"),
541            "run_quiet must still capture stderr, got: {:?}",
542            out.stderr
543        );
544    }
545
546    /// The actual point of `run_quiet`: it must not echo stderr live to the
547    /// terminal the way `run_with_timeout` does. Because the live echo goes
548    /// via `eprintln!` from inside `run_quiet` itself (not captured in
549    /// `CommandOutput`), this re-execs the current test binary as a child
550    /// process with `--nocapture` so the real OS-level stderr can be
551    /// observed and asserted on -- same pattern as
552    /// `run_with_timeout_warns_on_timeout_branch_when_descendant_holds_pipe_open`.
553    #[test]
554    fn run_quiet_does_not_stream_stderr_live() {
555        const CHILD_ENV: &str = "CALLISTO_RUN_QUIET_CHILD";
556
557        if std::env::var(CHILD_ENV).is_ok() {
558            let runner = CliCommandRunner;
559            let out = runner
560                .run_quiet(
561                    "sh",
562                    &["-c", "echo should-not-appear-live >&2"],
563                    std::path::Path::new("."),
564                    Duration::from_secs(5),
565                )
566                .expect("run_quiet must succeed");
567            // Positive proof the call actually ran and captured correctly,
568            // not just "produced no live output" -- a crashed or no-op
569            // child would leave the negative assertion below vacuously
570            // true. Printed to the child's own real stdout, entirely
571            // separate from anything `run_quiet` itself captures or emits.
572            assert!(
573                out.stderr.contains("should-not-appear-live"),
574                "run_quiet must still capture the text it doesn't stream live"
575            );
576            println!("CHILD_REACHED_AND_VERIFIED_CAPTURE");
577            return;
578        }
579
580        let exe = std::env::current_exe().expect("current_exe should be available in tests");
581        let output = std::process::Command::new(exe)
582            .arg("--exact")
583            .arg("runner::tests::run_quiet_does_not_stream_stderr_live")
584            .arg("--nocapture")
585            .env(CHILD_ENV, "1")
586            .output()
587            .expect("failed to re-exec test binary");
588
589        assert!(
590            output.status.success(),
591            "child process must exit successfully, got: {:?}, stderr: {}",
592            output.status,
593            String::from_utf8_lossy(&output.stderr)
594        );
595        let stdout = String::from_utf8_lossy(&output.stdout);
596        assert!(
597            stdout.contains("CHILD_REACHED_AND_VERIFIED_CAPTURE"),
598            "child must have actually reached and verified the run_quiet call, not crashed \
599             or no-op'd before it; got stdout: {stdout}"
600        );
601
602        let stderr = String::from_utf8_lossy(&output.stderr);
603        assert!(
604            !stderr.contains("should-not-appear-live"),
605            "run_quiet must not stream stderr live to the terminal, got: {stderr}"
606        );
607    }
608
609    /// Sibling regression guard: `run_with_timeout` (the live-streaming
610    /// path `run_quiet` is deliberately different from) must still echo
611    /// live -- otherwise this pair of tests could both pass by accident if
612    /// `stderr_mode` were wired backwards.
613    #[test]
614    fn run_with_timeout_still_streams_stderr_live() {
615        const CHILD_ENV: &str = "CALLISTO_RUN_TIMEOUT_LIVE_CHILD";
616
617        if std::env::var(CHILD_ENV).is_ok() {
618            let runner = CliCommandRunner;
619            drop(runner.run_with_timeout(
620                "sh",
621                &["-c", "echo should-appear-live >&2"],
622                std::path::Path::new("."),
623                Duration::from_secs(5),
624            ));
625            return;
626        }
627
628        let exe = std::env::current_exe().expect("current_exe should be available in tests");
629        let output = std::process::Command::new(exe)
630            .arg("--exact")
631            .arg("runner::tests::run_with_timeout_still_streams_stderr_live")
632            .arg("--nocapture")
633            .env(CHILD_ENV, "1")
634            .output()
635            .expect("failed to re-exec test binary");
636
637        let stderr = String::from_utf8_lossy(&output.stderr);
638        assert!(
639            stderr.contains("should-appear-live"),
640            "run_with_timeout must still stream stderr live, got: {stderr}"
641        );
642    }
643}