rivo / tview

Terminal UI library with rich, interactive widgets — written in Golang
MIT License
11.11k stars 576 forks source link

App becomes unresponsive sometimes if something inside panics #1017

Closed freak12techno closed 3 months ago

freak12techno commented 3 months ago

So I am building a tool called tmtop, and sometimes it may panic (that's another topic why). Problem is, once it panics, the terminal becomes unresponsive, like what you can see on a screen, and the terminal tab becomes that much messed up so it's easier to close it and open a new one.

image

On the other hand, if it panics before app.Run() is called, its output is not messed up. I assume the app somehow fails to close properly or fails to render it back to usable state.

Here's how I start it:

func (w *Wrapper) Start() {
    // some text rendering and key events bindings

    w.App.SetBeforeDrawFunc(func(screen tcell.Screen) bool {
        screen.Clear()
        return false
    })

    if err := w.App.Run(); err != nil {
        w.Logger.Panic().Err(err).Msg("Could not draw screen") // this calls panic() internally
    }
}

How can I fix this so the terminal tab would fallback to the usable state and the panic stacktrace would be displayed properly?

kivattt commented 3 months ago

I work around this issue by piping the stderr output into a file, like

./program 2> tempfile.txt

that way i can read the stacktrace by looking at the file

freak12techno commented 3 months ago

@kivattt yeah, but it's ugly for me as an app developer to ask the users of my app to use this approach to avoid their terminal tab getting messed up. I'd rather do something in my app so it'd quit properly to avoid that, if it's possible.

rivo commented 3 months ago

It's unfortunate that you didn't post any code that we can run ourselves to reproduce this. Generally, panicking while your application is running is not a problem. It doesn't lead to the effect you're seeing. This is because panics are recovered here:

https://github.com/rivo/tview/blob/e4c497cc59ed2f1b8eea43ef37456edc63e21749/application.go#L287

Here's an example:

func main() {
    box := tview.NewBox()

    box.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
        panic("panic!")
    })

    if err := tview.NewApplication().SetRoot(box, true).Run(); err != nil {
        fmt.Println("Error:", err)
    }
}

This will lead to a properly formatted stack trace:

image

If you panic after Run() returns, as your code above suggests, that's also not a problem. See here:

func main() {
    app := tview.NewApplication()
    box := tview.NewBox()

    box.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
        // Cause Run() to return with an error.
        app.QueueEvent(tcell.NewEventError(errors.New("error")))
        return nil
    })

    if err := app.SetRoot(box, true).Run(); err != nil {
        panic("panic!")
    }
}

Again, properly formatted stack trace:

image

In both cases, the screen is "finalized" (see here for details) before the stack trace is printed.

So I'm guessing that you're panicking in a separate goroutine, before Run() returns. In that case, yes, the terminal will still be set up for tcell's screen rendering and simply printing out a stack trace to stdout/stderr will lead to the issues you're seeing.

You don't want to do that. Be sure to exit your application first before causing a panic or ensure that the panic occurs in the main thread instead of a goroutine.

freak12techno commented 3 months ago

@rivo okay, I managed to write a minimal example that reproduces it:

package main

import (
    "errors"
    "github.com/rivo/tview"
    "time"
)

func main() {
    grid := tview.NewGrid().
        SetRows(0, 0, 0, 0, 0, 0, 0, 0, 0, 0).
        SetColumns(0, 0, 0, 0, 0, 0).
        SetBorders(true)

    pages := tview.NewPages().AddPage("grid", grid, true, true)

    app := tview.NewApplication().SetRoot(pages, true)

    go func() {
        time.Sleep(2 * time.Second)
        panic(errors.New("Panic!"))
    }()

    if err := app.Run(); err != nil {
        panic(err)
    }
}

So I'm guessing that you're panicking in a separate goroutine, before Run() returns.

so basically yes. I need to do a bunch of queries in my app asynchronously, and sometimes it might panic, if for example the response is malformed or something similar. How can I make it panic properly so it won't mess up my terminal with this?

rivo commented 3 months ago

How can I make it panic properly so it won't mess up my terminal with this?

What I wrote before:

Be sure to exit your application first before causing a panic or ensure that the panic occurs in the main thread instead of a goroutine.

Or don't panic at all. (Which I think is generally good advice for Go anyway.)

If it's absolutely unavoidable, catch the panic, then stop the application, then panic again. Basically, again, "...exit your application first before causing a panic".

In general, you cannot write to stdout or stderr while a tview application is running. Well, you can, but it will look ugly.

freak12techno commented 3 months ago

@rivo gotcha. I suggest adding this info you outlined above to the docs/wiki (unless I overlooked it if it's already present), so others would know about it in advance, and I basically got my answer so I think you can close this issue as there's no action points to be done (except documenting it). Thanks!

freak12techno commented 2 months ago

@rivo one more case I found out, somewhat related to the one outlined in the issue. If I run the app in an ssh session, and somehow it disconnects, it doesn't "finalize" the screen so the terminal tab becomes a mess. Do you think there's something that can be done to avoid it?

rivo commented 2 months ago

I don't think so. To my understanding, the terminal is set to some special mode which allows us to draw characters anywhere on screen. If the application doesn't terminate orderly, it can't reset the terminal to normal mode again.

In any case, this is all a topic for tcell: https://github.com/gdamore/tcell If you want to know the details about how this works or what can be done, please ask there.