* Initial commit for fixing #1353 * adding a new line between the lines_above and the viewport * changes following code review * implementing a test case for the dump screen * implemented test case for dump_screen * better regexp replace * fixes following code review * style(api): remove extraneous method in plugin pane * style(fmt): rustfmt * style(tests): fix method name Co-authored-by: Aram Drevekenin <aram@poor.dev>
348 lines
9.8 KiB
Rust
348 lines
9.8 KiB
Rust
//! Error context system based on a thread-local representation of the call stack, itself based on
|
|
//! the instructions that are sent between threads.
|
|
|
|
use crate::channels::{SenderWithContext, ASYNCOPENCALLS, OPENCALLS};
|
|
use colored::*;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::fmt::{Display, Error, Formatter};
|
|
use std::panic::PanicInfo;
|
|
|
|
use miette::{Diagnostic, GraphicalReportHandler, GraphicalTheme, Report};
|
|
use thiserror::Error as ThisError;
|
|
|
|
/// The maximum amount of calls an [`ErrorContext`] will keep track
|
|
/// of in its stack representation. This is a per-thread maximum.
|
|
const MAX_THREAD_CALL_STACK: usize = 6;
|
|
|
|
pub trait ErrorInstruction {
|
|
fn error(err: String) -> Self;
|
|
}
|
|
|
|
#[derive(Debug, ThisError, Diagnostic)]
|
|
#[error("{0}{}", self.show_backtrace())]
|
|
#[diagnostic(help("{}", self.show_help()))]
|
|
struct Panic(String);
|
|
|
|
impl Panic {
|
|
fn show_backtrace(&self) -> String {
|
|
if let Ok(var) = std::env::var("RUST_BACKTRACE") {
|
|
if !var.is_empty() && var != "0" {
|
|
return format!("\n{:?}", backtrace::Backtrace::new());
|
|
}
|
|
}
|
|
"".into()
|
|
}
|
|
|
|
fn show_help(&self) -> String {
|
|
r#"If you are seeing this message, it means that something went wrong.
|
|
Please report this error to the github issue.
|
|
(https://github.com/zellij-org/zellij/issues)
|
|
|
|
Also, if you want to see the backtrace, you can set the `RUST_BACKTRACE` environment variable to `1`.
|
|
"#.into()
|
|
}
|
|
}
|
|
|
|
fn fmt_report(diag: Report) -> String {
|
|
let mut out = String::new();
|
|
GraphicalReportHandler::new_themed(GraphicalTheme::unicode())
|
|
.render_report(&mut out, diag.as_ref())
|
|
.unwrap();
|
|
out
|
|
}
|
|
|
|
/// Custom panic handler/hook. Prints the [`ErrorContext`].
|
|
pub fn handle_panic<T>(info: &PanicInfo<'_>, sender: &SenderWithContext<T>)
|
|
where
|
|
T: ErrorInstruction + Clone,
|
|
{
|
|
use std::{process, thread};
|
|
let thread = thread::current();
|
|
let thread = thread.name().unwrap_or("unnamed");
|
|
|
|
let msg = match info.payload().downcast_ref::<&'static str>() {
|
|
Some(s) => Some(*s),
|
|
None => info.payload().downcast_ref::<String>().map(|s| &**s),
|
|
}
|
|
.unwrap_or("An unexpected error occurred!");
|
|
|
|
let err_ctx = OPENCALLS.with(|ctx| *ctx.borrow());
|
|
|
|
let mut report: Report = Panic(format!("\u{1b}[0;31m{}\u{1b}[0;0m", msg)).into();
|
|
|
|
if let Some(location) = info.location() {
|
|
report = report.wrap_err(format!(
|
|
"At {}:{}:{}",
|
|
location.file(),
|
|
location.line(),
|
|
location.column()
|
|
));
|
|
}
|
|
|
|
if !err_ctx.is_empty() {
|
|
report = report.wrap_err(format!("{}", err_ctx));
|
|
}
|
|
|
|
report = report.wrap_err(format!(
|
|
"Thread '\u{1b}[0;31m{}\u{1b}[0;0m' panicked.",
|
|
thread
|
|
));
|
|
|
|
if thread == "main" {
|
|
// here we only show the first line because the backtrace is not readable otherwise
|
|
// a better solution would be to escape raw mode before we do this, but it's not trivial
|
|
// to get os_input here
|
|
println!("\u{1b}[2J{}", fmt_report(report));
|
|
process::exit(1);
|
|
} else {
|
|
let _ = sender.send(T::error(fmt_report(report)));
|
|
}
|
|
}
|
|
|
|
pub fn get_current_ctx() -> ErrorContext {
|
|
ASYNCOPENCALLS
|
|
.try_with(|ctx| *ctx.borrow())
|
|
.unwrap_or_else(|_| OPENCALLS.with(|ctx| *ctx.borrow()))
|
|
}
|
|
|
|
/// A representation of the call stack.
|
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
|
pub struct ErrorContext {
|
|
calls: [ContextType; MAX_THREAD_CALL_STACK],
|
|
}
|
|
|
|
impl ErrorContext {
|
|
/// Returns a new, blank [`ErrorContext`] containing only [`Empty`](ContextType::Empty)
|
|
/// calls.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
calls: [ContextType::Empty; MAX_THREAD_CALL_STACK],
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if the calls has all [`Empty`](ContextType::Empty) calls.
|
|
pub fn is_empty(&self) -> bool {
|
|
self.calls.iter().all(|c| c == &ContextType::Empty)
|
|
}
|
|
|
|
/// Adds a call to this [`ErrorContext`]'s call stack representation.
|
|
pub fn add_call(&mut self, call: ContextType) {
|
|
for ctx in &mut self.calls {
|
|
if let ContextType::Empty = ctx {
|
|
*ctx = call;
|
|
break;
|
|
}
|
|
}
|
|
self.update_thread_ctx()
|
|
}
|
|
|
|
/// Updates the thread local [`ErrorContext`].
|
|
pub fn update_thread_ctx(&self) {
|
|
ASYNCOPENCALLS
|
|
.try_with(|ctx| *ctx.borrow_mut() = *self)
|
|
.unwrap_or_else(|_| OPENCALLS.with(|ctx| *ctx.borrow_mut() = *self));
|
|
}
|
|
}
|
|
|
|
impl Default for ErrorContext {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl Display for ErrorContext {
|
|
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
|
|
writeln!(f, "Originating Thread(s)")?;
|
|
for (index, ctx) in self.calls.iter().enumerate() {
|
|
if *ctx == ContextType::Empty {
|
|
break;
|
|
}
|
|
writeln!(f, "\t\u{1b}[0;0m{}. {}", index + 1, ctx)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Different types of calls that form an [`ErrorContext`] call stack.
|
|
///
|
|
/// Complex variants store a variant of a related enum, whose variants can be built from
|
|
/// the corresponding Zellij MSPC instruction enum variants ([`ScreenInstruction`],
|
|
/// [`PtyInstruction`], [`ClientInstruction`], etc).
|
|
#[derive(Copy, Clone, PartialEq, Serialize, Deserialize, Debug)]
|
|
pub enum ContextType {
|
|
/// A screen-related call.
|
|
Screen(ScreenContext),
|
|
/// A PTY-related call.
|
|
Pty(PtyContext),
|
|
/// A plugin-related call.
|
|
Plugin(PluginContext),
|
|
/// An app-related call.
|
|
Client(ClientContext),
|
|
/// A server-related call.
|
|
IPCServer(ServerContext),
|
|
StdinHandler,
|
|
AsyncTask,
|
|
PtyWrite(PtyWriteContext),
|
|
/// An empty, placeholder call. This should be thought of as representing no call at all.
|
|
/// A call stack representation filled with these is the representation of an empty call stack.
|
|
Empty,
|
|
}
|
|
|
|
impl Display for ContextType {
|
|
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
|
|
if let Some((left, right)) = match *self {
|
|
ContextType::Screen(c) => Some(("screen_thread:", format!("{:?}", c))),
|
|
ContextType::Pty(c) => Some(("pty_thread:", format!("{:?}", c))),
|
|
ContextType::Plugin(c) => Some(("plugin_thread:", format!("{:?}", c))),
|
|
ContextType::Client(c) => Some(("main_thread:", format!("{:?}", c))),
|
|
ContextType::IPCServer(c) => Some(("ipc_server:", format!("{:?}", c))),
|
|
ContextType::StdinHandler => Some(("stdin_handler_thread:", "AcceptInput".to_string())),
|
|
ContextType::AsyncTask => Some(("stream_terminal_bytes:", "AsyncTask".to_string())),
|
|
ContextType::PtyWrite(c) => Some(("pty_writer_thread:", format!("{:?}", c))),
|
|
ContextType::Empty => None,
|
|
} {
|
|
write!(f, "{} {}", left.purple(), right.green())
|
|
} else {
|
|
write!(f, "")
|
|
}
|
|
}
|
|
}
|
|
|
|
// FIXME: Just deriving EnumDiscriminants from strum will remove the need for any of this!!!
|
|
/// Stack call representations corresponding to the different types of [`ScreenInstruction`]s.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
pub enum ScreenContext {
|
|
HandlePtyBytes,
|
|
Render,
|
|
NewPane,
|
|
ToggleFloatingPanes,
|
|
TogglePaneEmbedOrFloating,
|
|
HorizontalSplit,
|
|
VerticalSplit,
|
|
WriteCharacter,
|
|
ResizeLeft,
|
|
ResizeRight,
|
|
ResizeDown,
|
|
ResizeUp,
|
|
ResizeIncrease,
|
|
ResizeDecrease,
|
|
SwitchFocus,
|
|
FocusNextPane,
|
|
FocusPreviousPane,
|
|
FocusPaneAt,
|
|
MoveFocusLeft,
|
|
MoveFocusLeftOrPreviousTab,
|
|
MoveFocusDown,
|
|
MoveFocusUp,
|
|
MoveFocusRight,
|
|
MoveFocusRightOrNextTab,
|
|
MovePane,
|
|
MovePaneDown,
|
|
MovePaneUp,
|
|
MovePaneRight,
|
|
MovePaneLeft,
|
|
Exit,
|
|
DumpScreen,
|
|
ScrollUp,
|
|
ScrollUpAt,
|
|
ScrollDown,
|
|
ScrollDownAt,
|
|
ScrollToBottom,
|
|
PageScrollUp,
|
|
PageScrollDown,
|
|
HalfPageScrollUp,
|
|
HalfPageScrollDown,
|
|
ClearScroll,
|
|
CloseFocusedPane,
|
|
ToggleActiveSyncTab,
|
|
ToggleActiveTerminalFullscreen,
|
|
TogglePaneFrames,
|
|
SetSelectable,
|
|
SetInvisibleBorders,
|
|
SetFixedHeight,
|
|
SetFixedWidth,
|
|
ClosePane,
|
|
UpdatePaneName,
|
|
NewTab,
|
|
SwitchTabNext,
|
|
SwitchTabPrev,
|
|
CloseTab,
|
|
GoToTab,
|
|
UpdateTabName,
|
|
TerminalResize,
|
|
TerminalPixelDimensions,
|
|
TerminalBackgroundColor,
|
|
TerminalForegroundColor,
|
|
ChangeMode,
|
|
LeftClick,
|
|
RightClick,
|
|
MouseRelease,
|
|
MouseHold,
|
|
Copy,
|
|
ToggleTab,
|
|
AddClient,
|
|
RemoveClient,
|
|
AddOverlay,
|
|
RemoveOverlay,
|
|
ConfirmPrompt,
|
|
DenyPrompt,
|
|
}
|
|
|
|
/// Stack call representations corresponding to the different types of [`PtyInstruction`]s.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
pub enum PtyContext {
|
|
SpawnTerminal,
|
|
SpawnTerminalVertically,
|
|
SpawnTerminalHorizontally,
|
|
UpdateActivePane,
|
|
GoToTab,
|
|
NewTab,
|
|
ClosePane,
|
|
CloseTab,
|
|
Exit,
|
|
}
|
|
|
|
/// Stack call representations corresponding to the different types of [`PluginInstruction`]s.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
pub enum PluginContext {
|
|
Load,
|
|
Update,
|
|
Render,
|
|
Unload,
|
|
Exit,
|
|
AddClient,
|
|
RemoveClient,
|
|
}
|
|
|
|
/// Stack call representations corresponding to the different types of [`ClientInstruction`]s.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
pub enum ClientContext {
|
|
Exit,
|
|
Error,
|
|
UnblockInputThread,
|
|
Render,
|
|
ServerError,
|
|
SwitchToMode,
|
|
Connected,
|
|
}
|
|
|
|
/// Stack call representations corresponding to the different types of [`ServerInstruction`]s.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
pub enum ServerContext {
|
|
NewClient,
|
|
Render,
|
|
UnblockInputThread,
|
|
ClientExit,
|
|
RemoveClient,
|
|
Error,
|
|
KillSession,
|
|
DetachSession,
|
|
AttachClient,
|
|
ConnStatus,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
pub enum PtyWriteContext {
|
|
Write,
|
|
Exit,
|
|
}
|