From f17f857b1f2a0a844f30037af2a2583f564a476c Mon Sep 17 00:00:00 2001
From: Jack Wright <56345+ayax79@users.noreply.github.com>
Date: Thu, 22 Feb 2024 10:14:10 -0800
Subject: [PATCH] wrapping run_repl with catch_unwind and restarting the repl
on panic (#11860)
Provides the ability to cleanly recover from panics, falling back to the
last known good state of EngineState and Stack. This pull request also
utilizes miette's panic handler for better formatting of panics.
---------
Co-authored-by: Jack Wright
---
Cargo.lock | 11 +
Cargo.toml | 2 +-
crates/nu-cli/src/repl.rs | 807 ++++++++++--------
crates/nu-command/src/default_context.rs | 1 +
crates/nu-command/src/misc/mod.rs | 2 +
crates/nu-command/src/misc/panic.rs | 43 +
crates/nu-protocol/src/engine/engine_state.rs | 14 +
src/command.rs | 1 +
src/main.rs | 1 +
9 files changed, 521 insertions(+), 361 deletions(-)
create mode 100644 crates/nu-command/src/misc/panic.rs
diff --git a/Cargo.lock b/Cargo.lock
index 163938624e..c915ee145d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -324,6 +324,15 @@ dependencies = [
"rustc-demangle",
]
+[[package]]
+name = "backtrace-ext"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50"
+dependencies = [
+ "backtrace",
+]
+
[[package]]
name = "base64"
version = "0.13.1"
@@ -2552,6 +2561,8 @@ version = "7.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baed61d13cc3723ee6dbed730a82bfacedc60a85d81da2d77e9c3e8ebc0b504a"
dependencies = [
+ "backtrace",
+ "backtrace-ext",
"miette-derive",
"owo-colors",
"supports-color",
diff --git a/Cargo.toml b/Cargo.toml
index 4ba2e275b6..1cfa667782 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -84,7 +84,7 @@ reedline = { version = "0.29.0", features = ["bashisms", "sqlite"] }
crossterm = "0.27"
ctrlc = "3.4"
log = "0.4"
-miette = { version = "7.1", features = ["fancy-no-backtrace"] }
+miette = { version = "7.1", features = ["fancy-no-backtrace", "fancy"] }
mimalloc = { version = "0.1.37", default-features = false, optional = true }
serde_json = "1.0"
simplelog = "0.12"
diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs
index a6c2aff1c5..06e4423517 100644
--- a/crates/nu-cli/src/repl.rs
+++ b/crates/nu-cli/src/repl.rs
@@ -28,6 +28,8 @@ use reedline::{
use std::{
env::temp_dir,
io::{self, IsTerminal, Write},
+ panic::{catch_unwind, AssertUnwindSafe},
+ path::Path,
path::PathBuf,
sync::atomic::Ordering,
time::{Duration, Instant},
@@ -55,8 +57,6 @@ pub fn evaluate_repl(
load_std_lib: Option>,
entire_start_time: Instant,
) -> Result<()> {
- use nu_cmd_base::hook;
- use reedline::Signal;
let config = engine_state.get_config();
let use_color = config.use_ansi_coloring;
@@ -64,7 +64,7 @@ pub fn evaluate_repl(
let mut entry_num = 0;
- let mut nu_prompt = NushellPrompt::new(config.shell_integration);
+ let nu_prompt = NushellPrompt::new(config.shell_integration);
let start_time = std::time::Instant::now();
// Translate environment variables from Strings to Values
@@ -88,36 +88,9 @@ pub fn evaluate_repl(
stack.add_env_var("LAST_EXIT_CODE".into(), Value::int(0, Span::unknown()));
- let mut start_time = std::time::Instant::now();
- let mut line_editor = Reedline::create();
+ let mut line_editor = get_line_editor(engine_state, nushell_path, use_color)?;
let temp_file = temp_dir().join(format!("{}.nu", uuid::Uuid::new_v4()));
- // Now that reedline is created, get the history session id and store it in engine_state
- store_history_id_in_engine(engine_state, &line_editor);
- perf(
- "setup reedline",
- start_time,
- file!(),
- line!(),
- column!(),
- use_color,
- );
-
- if let Some(history) = engine_state.history_config() {
- start_time = std::time::Instant::now();
-
- line_editor = setup_history(nushell_path, engine_state, line_editor, history)?;
-
- perf(
- "setup history",
- start_time,
- file!(),
- line!(),
- column!(),
- use_color,
- );
- }
-
if let Some(s) = prerun_command {
eval_source(
engine_state,
@@ -149,353 +122,273 @@ pub fn evaluate_repl(
kitty_protocol_healthcheck(engine_state);
+ // Setup initial engine_state and stack state
+ let mut previous_engine_state = engine_state.clone();
+ let mut previous_stack = stack.clone();
loop {
- let loop_start_time = std::time::Instant::now();
+ // clone these values so that they can be moved by AssertUnwindSafe
+ // If there is a panic within this iteration the last engine_state and stack
+ // will be used
+ let mut current_engine_state = previous_engine_state.clone();
+ let mut current_stack = previous_stack.clone();
+ let temp_file_cloned = temp_file.clone();
+ let mut nu_prompt_cloned = nu_prompt.clone();
- let cwd = get_guaranteed_cwd(engine_state, stack);
-
- start_time = std::time::Instant::now();
- // Before doing anything, merge the environment from the previous REPL iteration into the
- // permanent state.
- if let Err(err) = engine_state.merge_env(stack, cwd) {
- report_error_new(engine_state, &err);
- }
- perf(
- "merge env",
- start_time,
- file!(),
- line!(),
- column!(),
- use_color,
- );
-
- start_time = std::time::Instant::now();
- //Reset the ctrl-c handler
- if let Some(ctrlc) = &mut engine_state.ctrlc {
- ctrlc.store(false, Ordering::SeqCst);
- }
- perf(
- "reset ctrlc",
- start_time,
- file!(),
- line!(),
- column!(),
- use_color,
- );
-
- start_time = std::time::Instant::now();
- let config = engine_state.get_config();
-
- let engine_reference = std::sync::Arc::new(engine_state.clone());
-
- // Find the configured cursor shapes for each mode
- let cursor_config = CursorConfig {
- vi_insert: map_nucursorshape_to_cursorshape(config.cursor_shape_vi_insert),
- vi_normal: map_nucursorshape_to_cursorshape(config.cursor_shape_vi_normal),
- emacs: map_nucursorshape_to_cursorshape(config.cursor_shape_emacs),
- };
- perf(
- "get config/cursor config",
- start_time,
- file!(),
- line!(),
- column!(),
- use_color,
- );
-
- start_time = std::time::Instant::now();
-
- line_editor = line_editor
- .use_kitty_keyboard_enhancement(config.use_kitty_protocol)
- // try to enable bracketed paste
- // It doesn't work on windows system: https://github.com/crossterm-rs/crossterm/issues/737
- .use_bracketed_paste(cfg!(not(target_os = "windows")) && config.bracketed_paste)
- .with_highlighter(Box::new(NuHighlighter {
- engine_state: engine_reference.clone(),
- stack: std::sync::Arc::new(stack.clone()),
- config: config.clone(),
- }))
- .with_validator(Box::new(NuValidator {
- engine_state: engine_reference.clone(),
- }))
- .with_completer(Box::new(NuCompleter::new(
- engine_reference.clone(),
- stack.clone(),
- )))
- .with_quick_completions(config.quick_completions)
- .with_partial_completions(config.partial_completions)
- .with_ansi_colors(config.use_ansi_coloring)
- .with_cursor_config(cursor_config);
- perf(
- "reedline builder",
- start_time,
- file!(),
- line!(),
- column!(),
- use_color,
- );
-
- let style_computer = StyleComputer::from_config(engine_state, stack);
-
- start_time = std::time::Instant::now();
- line_editor = if config.use_ansi_coloring {
- line_editor.with_hinter(Box::new({
- // As of Nov 2022, "hints" color_config closures only get `null` passed in.
- let style = style_computer.compute("hints", &Value::nothing(Span::unknown()));
- CwdAwareHinter::default().with_style(style)
- }))
- } else {
- line_editor.disable_hints()
- };
- perf(
- "reedline coloring/style_computer",
- start_time,
- file!(),
- line!(),
- column!(),
- use_color,
- );
-
- start_time = std::time::Instant::now();
- line_editor = add_menus(line_editor, engine_reference, stack, config).unwrap_or_else(|e| {
- report_error_new(engine_state, &e);
- Reedline::create()
- });
- perf(
- "reedline menus",
- start_time,
- file!(),
- line!(),
- column!(),
- use_color,
- );
-
- start_time = std::time::Instant::now();
- let buffer_editor = get_editor(engine_state, stack, Span::unknown());
-
- line_editor = if let Ok((cmd, args)) = buffer_editor {
- let mut command = std::process::Command::new(&cmd);
- command
- .args(args)
- .envs(env_to_strings(engine_state, stack)?);
- line_editor.with_buffer_editor(command, temp_file.clone())
- } else {
- line_editor
- };
- perf(
- "reedline buffer_editor",
- start_time,
- file!(),
- line!(),
- column!(),
- use_color,
- );
-
- if let Some(history) = engine_state.history_config() {
- start_time = std::time::Instant::now();
- if history.sync_on_enter {
- if let Err(e) = line_editor.sync_history() {
- warn!("Failed to sync history: {}", e);
- }
- }
- perf(
- "sync_history",
- start_time,
- file!(),
- line!(),
- column!(),
+ match catch_unwind(AssertUnwindSafe(move || {
+ match loop_iteration(
+ &mut current_engine_state,
+ &mut current_stack,
+ line_editor,
+ &mut nu_prompt_cloned,
+ &temp_file_cloned,
use_color,
- );
- }
-
- start_time = std::time::Instant::now();
- // Changing the line editor based on the found keybindings
- line_editor = setup_keybindings(engine_state, line_editor);
- perf(
- "keybindings",
- start_time,
- file!(),
- line!(),
- column!(),
- use_color,
- );
-
- start_time = std::time::Instant::now();
- // Right before we start our prompt and take input from the user,
- // fire the "pre_prompt" hook
- if let Some(hook) = config.hooks.pre_prompt.clone() {
- if let Err(err) = eval_hook(engine_state, stack, None, vec![], &hook, "pre_prompt") {
- report_error_new(engine_state, &err);
- }
- }
- perf(
- "pre-prompt hook",
- start_time,
- file!(),
- line!(),
- column!(),
- use_color,
- );
-
- start_time = std::time::Instant::now();
- // Next, check all the environment variables they ask for
- // fire the "env_change" hook
- let config = engine_state.get_config();
- if let Err(error) =
- hook::eval_env_change_hook(config.hooks.env_change.clone(), engine_state, stack)
- {
- report_error_new(engine_state, &error)
- }
- perf(
- "env-change hook",
- start_time,
- file!(),
- line!(),
- column!(),
- use_color,
- );
-
- start_time = std::time::Instant::now();
- let config = &engine_state.get_config().clone();
- prompt_update::update_prompt(config, engine_state, stack, &mut nu_prompt);
- let transient_prompt =
- prompt_update::make_transient_prompt(config, engine_state, stack, &nu_prompt);
- perf(
- "update_prompt",
- start_time,
- file!(),
- line!(),
- column!(),
- use_color,
- );
-
- entry_num += 1;
-
- start_time = std::time::Instant::now();
- line_editor = line_editor.with_transient_prompt(transient_prompt);
- let input = line_editor.read_line(&nu_prompt);
- let shell_integration = config.shell_integration;
-
- match input {
- Ok(Signal::Success(s)) => {
- let hostname = System::host_name();
- let history_supports_meta = matches!(
- engine_state.history_config().map(|h| h.file_format),
- Some(HistoryFileFormat::Sqlite)
- );
-
- if history_supports_meta {
- prepare_history_metadata(&s, &hostname, engine_state, &mut line_editor)?;
+ &mut entry_num,
+ ) {
+ // pass the most recent version of the line_editor back
+ Ok((continue_loop, line_editor)) => (
+ Ok(continue_loop),
+ current_engine_state,
+ current_stack,
+ line_editor,
+ ),
+ Err(e) => {
+ current_engine_state.recover_from_panic();
+ (
+ Err(e),
+ current_engine_state,
+ current_stack,
+ Reedline::create(),
+ )
}
-
- // Right before we start running the code the user gave us, fire the `pre_execution`
- // hook
- if let Some(hook) = config.hooks.pre_execution.clone() {
- // Set the REPL buffer to the current command for the "pre_execution" hook
- let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
- repl.buffer = s.to_string();
- drop(repl);
-
- if let Err(err) =
- eval_hook(engine_state, stack, None, vec![], &hook, "pre_execution")
- {
- report_error_new(engine_state, &err);
+ }
+ })) {
+ Ok((result, es, s, le)) => {
+ // setup state for the next iteration of the repl loop
+ previous_engine_state = es;
+ previous_stack = s;
+ line_editor = le;
+ match result {
+ Ok(false) => {
+ break;
}
- }
-
- let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
- repl.cursor_pos = line_editor.current_insertion_point();
- repl.buffer = line_editor.current_buffer_contents().to_string();
- drop(repl);
-
- if shell_integration {
- run_ansi_sequence(PRE_EXECUTE_MARKER)?;
- }
-
- // Actual command execution logic starts from here
- let start_time = Instant::now();
-
- match parse_operation(s.clone(), engine_state, stack)? {
- ReplOperation::AutoCd { cwd, target, span } => {
- do_auto_cd(target, cwd, stack, engine_state, span);
+ Err(e) => {
+ return Err(e);
}
- ReplOperation::RunCommand(cmd) => {
- line_editor = do_run_cmd(
- &cmd,
- stack,
- engine_state,
- line_editor,
- shell_integration,
- entry_num,
- )?;
- }
- // as the name implies, we do nothing in this case
- ReplOperation::DoNothing => {}
- }
-
- let cmd_duration = start_time.elapsed();
-
- stack.add_env_var(
- "CMD_DURATION_MS".into(),
- Value::string(format!("{}", cmd_duration.as_millis()), Span::unknown()),
- );
-
- if history_supports_meta {
- fill_in_result_related_history_metadata(
- &s,
- engine_state,
- cmd_duration,
- stack,
- &mut line_editor,
- )?;
- }
-
- if shell_integration {
- do_shell_integration_finalize_command(hostname, engine_state, stack)?;
- }
-
- flush_engine_state_repl_buffer(engine_state, &mut line_editor);
- }
- Ok(Signal::CtrlC) => {
- // `Reedline` clears the line content. New prompt is shown
- if shell_integration {
- run_ansi_sequence(&get_command_finished_marker(stack, engine_state))?;
+ _ => (),
}
}
- Ok(Signal::CtrlD) => {
- // When exiting clear to a new line
- if shell_integration {
- run_ansi_sequence(&get_command_finished_marker(stack, engine_state))?;
- }
- println!();
- break;
- }
- Err(err) => {
- let message = err.to_string();
- if !message.contains("duration") {
- eprintln!("Error: {err:?}");
- // TODO: Identify possible error cases where a hard failure is preferable
- // Ignoring and reporting could hide bigger problems
- // e.g. https://github.com/nushell/nushell/issues/6452
- // Alternatively only allow that expected failures let the REPL loop
- }
- if shell_integration {
- run_ansi_sequence(&get_command_finished_marker(stack, engine_state))?;
- }
+ Err(_) => {
+ // line_editor is lost in the error case so reconstruct a new one
+ line_editor = get_line_editor(engine_state, nushell_path, use_color)?;
}
}
+ }
+
+ Ok(())
+}
+
+fn get_line_editor(
+ engine_state: &mut EngineState,
+ nushell_path: &str,
+ use_color: bool,
+) -> Result {
+ let mut start_time = std::time::Instant::now();
+ let mut line_editor = Reedline::create();
+
+ // Now that reedline is created, get the history session id and store it in engine_state
+ store_history_id_in_engine(engine_state, &line_editor);
+ perf(
+ "setup reedline",
+ start_time,
+ file!(),
+ line!(),
+ column!(),
+ use_color,
+ );
+
+ if let Some(history) = engine_state.history_config() {
+ start_time = std::time::Instant::now();
+
+ line_editor = setup_history(nushell_path, engine_state, line_editor, history)?;
+
perf(
- "processing line editor input",
+ "setup history",
start_time,
file!(),
line!(),
column!(),
use_color,
);
+ }
+ Ok(line_editor)
+}
+///
+/// Perform one iteration of the REPL loop
+/// Result is bool: continue loop, current reedline
+#[inline]
+fn loop_iteration(
+ engine_state: &mut EngineState,
+ stack: &mut Stack,
+ line_editor: Reedline,
+ nu_prompt: &mut NushellPrompt,
+ temp_file: &Path,
+ use_color: bool,
+ entry_num: &mut usize,
+) -> Result<(bool, Reedline)> {
+ use nu_cmd_base::hook;
+ use reedline::Signal;
+ let loop_start_time = std::time::Instant::now();
+
+ let cwd = get_guaranteed_cwd(engine_state, stack);
+
+ let mut start_time = std::time::Instant::now();
+ // Before doing anything, merge the environment from the previous REPL iteration into the
+ // permanent state.
+ if let Err(err) = engine_state.merge_env(stack, cwd) {
+ report_error_new(engine_state, &err);
+ }
+ perf(
+ "merge env",
+ start_time,
+ file!(),
+ line!(),
+ column!(),
+ use_color,
+ );
+
+ start_time = std::time::Instant::now();
+ //Reset the ctrl-c handler
+ if let Some(ctrlc) = &mut engine_state.ctrlc {
+ ctrlc.store(false, Ordering::SeqCst);
+ }
+ perf(
+ "reset ctrlc",
+ start_time,
+ file!(),
+ line!(),
+ column!(),
+ use_color,
+ );
+
+ start_time = std::time::Instant::now();
+ let config = engine_state.get_config();
+
+ let engine_reference = std::sync::Arc::new(engine_state.clone());
+
+ // Find the configured cursor shapes for each mode
+ let cursor_config = CursorConfig {
+ vi_insert: map_nucursorshape_to_cursorshape(config.cursor_shape_vi_insert),
+ vi_normal: map_nucursorshape_to_cursorshape(config.cursor_shape_vi_normal),
+ emacs: map_nucursorshape_to_cursorshape(config.cursor_shape_emacs),
+ };
+ perf(
+ "get config/cursor config",
+ start_time,
+ file!(),
+ line!(),
+ column!(),
+ use_color,
+ );
+
+ start_time = std::time::Instant::now();
+
+ let mut line_editor = line_editor
+ .use_kitty_keyboard_enhancement(config.use_kitty_protocol)
+ // try to enable bracketed paste
+ // It doesn't work on windows system: https://github.com/crossterm-rs/crossterm/issues/737
+ .use_bracketed_paste(cfg!(not(target_os = "windows")) && config.bracketed_paste)
+ .with_highlighter(Box::new(NuHighlighter {
+ engine_state: engine_reference.clone(),
+ stack: std::sync::Arc::new(stack.clone()),
+ config: config.clone(),
+ }))
+ .with_validator(Box::new(NuValidator {
+ engine_state: engine_reference.clone(),
+ }))
+ .with_completer(Box::new(NuCompleter::new(
+ engine_reference.clone(),
+ stack.clone(),
+ )))
+ .with_quick_completions(config.quick_completions)
+ .with_partial_completions(config.partial_completions)
+ .with_ansi_colors(config.use_ansi_coloring)
+ .with_cursor_config(cursor_config);
+ perf(
+ "reedline builder",
+ start_time,
+ file!(),
+ line!(),
+ column!(),
+ use_color,
+ );
+
+ let style_computer = StyleComputer::from_config(engine_state, stack);
+
+ start_time = std::time::Instant::now();
+ line_editor = if config.use_ansi_coloring {
+ line_editor.with_hinter(Box::new({
+ // As of Nov 2022, "hints" color_config closures only get `null` passed in.
+ let style = style_computer.compute("hints", &Value::nothing(Span::unknown()));
+ CwdAwareHinter::default().with_style(style)
+ }))
+ } else {
+ line_editor.disable_hints()
+ };
+ perf(
+ "reedline coloring/style_computer",
+ start_time,
+ file!(),
+ line!(),
+ column!(),
+ use_color,
+ );
+
+ start_time = std::time::Instant::now();
+ line_editor = add_menus(line_editor, engine_reference, stack, config).unwrap_or_else(|e| {
+ report_error_new(engine_state, &e);
+ Reedline::create()
+ });
+ perf(
+ "reedline menus",
+ start_time,
+ file!(),
+ line!(),
+ column!(),
+ use_color,
+ );
+
+ start_time = std::time::Instant::now();
+ let buffer_editor = get_editor(engine_state, stack, Span::unknown());
+
+ line_editor = if let Ok((cmd, args)) = buffer_editor {
+ let mut command = std::process::Command::new(cmd);
+ command
+ .args(args)
+ .envs(env_to_strings(engine_state, stack)?);
+ line_editor.with_buffer_editor(command, temp_file.to_path_buf())
+ } else {
+ line_editor
+ };
+ perf(
+ "reedline buffer_editor",
+ start_time,
+ file!(),
+ line!(),
+ column!(),
+ use_color,
+ );
+
+ if let Some(history) = engine_state.history_config() {
+ start_time = std::time::Instant::now();
+ if history.sync_on_enter {
+ if let Err(e) = line_editor.sync_history() {
+ warn!("Failed to sync history: {}", e);
+ }
+ }
perf(
- "finished repl loop",
- loop_start_time,
+ "sync_history",
+ start_time,
file!(),
line!(),
column!(),
@@ -503,7 +396,201 @@ pub fn evaluate_repl(
);
}
- Ok(())
+ start_time = std::time::Instant::now();
+ // Changing the line editor based on the found keybindings
+ line_editor = setup_keybindings(engine_state, line_editor);
+ perf(
+ "keybindings",
+ start_time,
+ file!(),
+ line!(),
+ column!(),
+ use_color,
+ );
+
+ start_time = std::time::Instant::now();
+ // Right before we start our prompt and take input from the user,
+ // fire the "pre_prompt" hook
+ if let Some(hook) = config.hooks.pre_prompt.clone() {
+ if let Err(err) = eval_hook(engine_state, stack, None, vec![], &hook, "pre_prompt") {
+ report_error_new(engine_state, &err);
+ }
+ }
+ perf(
+ "pre-prompt hook",
+ start_time,
+ file!(),
+ line!(),
+ column!(),
+ use_color,
+ );
+
+ start_time = std::time::Instant::now();
+ // Next, check all the environment variables they ask for
+ // fire the "env_change" hook
+ let config = engine_state.get_config();
+ if let Err(error) =
+ hook::eval_env_change_hook(config.hooks.env_change.clone(), engine_state, stack)
+ {
+ report_error_new(engine_state, &error)
+ }
+ perf(
+ "env-change hook",
+ start_time,
+ file!(),
+ line!(),
+ column!(),
+ use_color,
+ );
+
+ start_time = std::time::Instant::now();
+ let config = &engine_state.get_config().clone();
+ prompt_update::update_prompt(config, engine_state, stack, nu_prompt);
+ let transient_prompt =
+ prompt_update::make_transient_prompt(config, engine_state, stack, nu_prompt);
+ perf(
+ "update_prompt",
+ start_time,
+ file!(),
+ line!(),
+ column!(),
+ use_color,
+ );
+
+ *entry_num += 1;
+
+ start_time = std::time::Instant::now();
+ line_editor = line_editor.with_transient_prompt(transient_prompt);
+ let input = line_editor.read_line(nu_prompt);
+ let shell_integration = config.shell_integration;
+
+ match input {
+ Ok(Signal::Success(s)) => {
+ let hostname = System::host_name();
+ let history_supports_meta = matches!(
+ engine_state.history_config().map(|h| h.file_format),
+ Some(HistoryFileFormat::Sqlite)
+ );
+
+ if history_supports_meta {
+ prepare_history_metadata(&s, &hostname, engine_state, &mut line_editor)?;
+ }
+
+ // Right before we start running the code the user gave us, fire the `pre_execution`
+ // hook
+ if let Some(hook) = config.hooks.pre_execution.clone() {
+ // Set the REPL buffer to the current command for the "pre_execution" hook
+ let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
+ repl.buffer = s.to_string();
+ drop(repl);
+
+ if let Err(err) =
+ eval_hook(engine_state, stack, None, vec![], &hook, "pre_execution")
+ {
+ report_error_new(engine_state, &err);
+ }
+ }
+
+ let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
+ repl.cursor_pos = line_editor.current_insertion_point();
+ repl.buffer = line_editor.current_buffer_contents().to_string();
+ drop(repl);
+
+ if shell_integration {
+ run_ansi_sequence(PRE_EXECUTE_MARKER)?;
+ }
+
+ // Actual command execution logic starts from here
+ let start_time = Instant::now();
+
+ match parse_operation(s.clone(), engine_state, stack)? {
+ ReplOperation::AutoCd { cwd, target, span } => {
+ do_auto_cd(target, cwd, stack, engine_state, span);
+ }
+ ReplOperation::RunCommand(cmd) => {
+ line_editor = do_run_cmd(
+ &cmd,
+ stack,
+ engine_state,
+ line_editor,
+ shell_integration,
+ *entry_num,
+ )?;
+ }
+ // as the name implies, we do nothing in this case
+ ReplOperation::DoNothing => {}
+ }
+
+ let cmd_duration = start_time.elapsed();
+
+ stack.add_env_var(
+ "CMD_DURATION_MS".into(),
+ Value::string(format!("{}", cmd_duration.as_millis()), Span::unknown()),
+ );
+
+ if history_supports_meta {
+ fill_in_result_related_history_metadata(
+ &s,
+ engine_state,
+ cmd_duration,
+ stack,
+ &mut line_editor,
+ )?;
+ }
+
+ if shell_integration {
+ do_shell_integration_finalize_command(hostname, engine_state, stack)?;
+ }
+
+ flush_engine_state_repl_buffer(engine_state, &mut line_editor);
+ }
+ Ok(Signal::CtrlC) => {
+ // `Reedline` clears the line content. New prompt is shown
+ if shell_integration {
+ run_ansi_sequence(&get_command_finished_marker(stack, engine_state))?;
+ }
+ }
+ Ok(Signal::CtrlD) => {
+ // When exiting clear to a new line
+ if shell_integration {
+ run_ansi_sequence(&get_command_finished_marker(stack, engine_state))?;
+ }
+ println!();
+ return Ok((false, line_editor));
+ }
+ Err(err) => {
+ let message = err.to_string();
+ if !message.contains("duration") {
+ eprintln!("Error: {err:?}");
+ // TODO: Identify possible error cases where a hard failure is preferable
+ // Ignoring and reporting could hide bigger problems
+ // e.g. https://github.com/nushell/nushell/issues/6452
+ // Alternatively only allow that expected failures let the REPL loop
+ }
+ if shell_integration {
+ run_ansi_sequence(&get_command_finished_marker(stack, engine_state))?;
+ }
+ }
+ }
+ perf(
+ "processing line editor input",
+ start_time,
+ file!(),
+ line!(),
+ column!(),
+ use_color,
+ );
+
+ perf(
+ "finished repl loop",
+ loop_start_time,
+ file!(),
+ line!(),
+ column!(),
+ use_color,
+ );
+
+ Ok((true, line_editor))
}
///
diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs
index 3eb2d5659a..12815e0fda 100644
--- a/crates/nu-command/src/default_context.rs
+++ b/crates/nu-command/src/default_context.rs
@@ -93,6 +93,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
// Misc
bind_command! {
+ Panic,
Source,
Tutor,
};
diff --git a/crates/nu-command/src/misc/mod.rs b/crates/nu-command/src/misc/mod.rs
index 5c632e00e2..bd140e3f36 100644
--- a/crates/nu-command/src/misc/mod.rs
+++ b/crates/nu-command/src/misc/mod.rs
@@ -1,5 +1,7 @@
+mod panic;
mod source;
mod tutor;
+pub use panic::Panic;
pub use source::Source;
pub use tutor::Tutor;
diff --git a/crates/nu-command/src/misc/panic.rs b/crates/nu-command/src/misc/panic.rs
new file mode 100644
index 0000000000..16294561d3
--- /dev/null
+++ b/crates/nu-command/src/misc/panic.rs
@@ -0,0 +1,43 @@
+use nu_engine::CallExt;
+use nu_protocol::ast::Call;
+use nu_protocol::engine::{Command, EngineState, Stack};
+use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type};
+
+#[derive(Clone)]
+pub struct Panic;
+
+impl Command for Panic {
+ fn name(&self) -> &str {
+ "panic"
+ }
+
+ fn usage(&self) -> &str {
+ "Executes a rust panic, useful only for testing."
+ }
+
+ fn signature(&self) -> nu_protocol::Signature {
+ Signature::build("panic")
+ .input_output_types(vec![(Type::Nothing, Type::Table(vec![]))])
+ // LsGlobPattern is similar to string, it won't auto-expand
+ // and we use it to track if the user input is quoted.
+ .optional("msg", SyntaxShape::String, "The glob pattern to use.")
+ .category(Category::Experimental)
+ }
+
+ fn run(
+ &self,
+ engine_state: &EngineState,
+ stack: &mut Stack,
+ call: &Call,
+ _input: PipelineData,
+ ) -> Result {
+ let maybe_msg: String = call
+ .opt(engine_state, stack, 0)?
+ .unwrap_or("Panic!".to_string());
+ panic!("{}", maybe_msg)
+ }
+
+ fn examples(&self) -> Vec {
+ vec![]
+ }
+}
diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs
index a26a13cdbd..a904467c5d 100644
--- a/crates/nu-protocol/src/engine/engine_state.rs
+++ b/crates/nu-protocol/src/engine/engine_state.rs
@@ -915,6 +915,20 @@ impl EngineState {
pub fn set_startup_time(&mut self, startup_time: i64) {
self.startup_time = startup_time;
}
+
+ pub fn recover_from_panic(&mut self) {
+ if Mutex::is_poisoned(&self.repl_state) {
+ self.repl_state = Arc::new(Mutex::new(ReplState {
+ buffer: "".to_string(),
+ cursor_pos: 0,
+ }));
+ }
+ if Mutex::is_poisoned(&self.regex_cache) {
+ self.regex_cache = Arc::new(Mutex::new(LruCache::new(
+ NonZeroUsize::new(REGEX_CACHE_SIZE).expect("tried to create cache of size zero"),
+ )));
+ }
+ }
}
impl Default for EngineState {
diff --git a/src/command.rs b/src/command.rs
index 10fe383fed..9e7a5f2908 100644
--- a/src/command.rs
+++ b/src/command.rs
@@ -216,6 +216,7 @@ pub(crate) fn parse_commandline_args(
std::process::exit(1);
}
+#[derive(Clone)]
pub(crate) struct NushellCliArgs {
pub(crate) redirect_stdin: Option>,
pub(crate) login_shell: Option>,
diff --git a/src/main.rs b/src/main.rs
index 387e5c4fcc..a97b2042be 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -53,6 +53,7 @@ fn get_engine_state() -> EngineState {
fn main() -> Result<()> {
let entire_start_time = std::time::Instant::now();
let mut start_time = std::time::Instant::now();
+ miette::set_panic_hook();
let miette_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |x| {
crossterm::terminal::disable_raw_mode().expect("unable to disable raw mode");