fix: terminate dashboard gracefully on & switch back to tty1

- Make dashboard SIGTERM-aware
- Handle panics on dashboard and terminate it gracefully, so it resets the terminal properly
- Switch to TTY2 when it starts and back to TTY1 when it stops.

Closes siderolabs/talos#7516.

Signed-off-by: Utku Ozdemir <utku.ozdemir@siderolabs.com>
This commit is contained in:
Utku Ozdemir 2023-07-27 14:35:32 +02:00
parent 544cb4fe7d
commit 355681ddab
No known key found for this signature in database
GPG Key ID: 65933E76F0549B0D
6 changed files with 75 additions and 28 deletions

View File

@ -10,6 +10,9 @@ import (
"fmt"
"log"
"net/url"
"os"
"os/signal"
"syscall"
"github.com/siderolabs/go-procfs/procfs"
"google.golang.org/grpc"
@ -38,9 +41,13 @@ func dashboardMain() error {
md := metadata.Pairs()
authz.SetMetadata(md, role.MakeSet(role.Admin))
adminCtx := metadata.NewOutgoingContext(context.Background(), md)
c, err := client.New(adminCtx,
ctx, cancel := sigtermAwareContext(context.Background())
defer cancel()
ctx = metadata.NewOutgoingContext(ctx, md)
c, err := client.New(ctx,
client.WithUnixSocket(constants.MachineSocketPath),
client.WithGRPCDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())),
)
@ -60,7 +67,7 @@ func dashboardMain() error {
}
}
return dashboard.Run(adminCtx, c, dashboard.WithAllowExitKeys(false), dashboard.WithScreens(screens...))
return dashboard.Run(ctx, c, dashboard.WithAllowExitKeys(false), dashboard.WithScreens(screens...))
}
func showConfigURLTab() bool {
@ -81,3 +88,20 @@ func showConfigURLTab() bool {
return codeVar.Matches(parsedURL.Query())
}
func sigtermAwareContext(ctx context.Context) (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(ctx)
signalCh := make(chan os.Signal, 1)
signal.Notify(signalCh, syscall.SIGTERM)
go func() {
select {
case <-signalCh:
cancel()
case <-ctx.Done():
}
}()
return ctx, cancel
}

View File

@ -49,7 +49,6 @@ import (
"github.com/siderolabs/talos/internal/app/machined/pkg/system"
"github.com/siderolabs/talos/internal/app/machined/pkg/system/events"
"github.com/siderolabs/talos/internal/app/machined/pkg/system/services"
"github.com/siderolabs/talos/internal/pkg/console"
"github.com/siderolabs/talos/internal/pkg/cri"
"github.com/siderolabs/talos/internal/pkg/environment"
"github.com/siderolabs/talos/internal/pkg/etcd"
@ -620,13 +619,9 @@ func StartMachined(_ runtime.Sequence, _ any) (runtime.TaskExecutionFunc, string
// StartDashboard represents the task to start dashboard.
func StartDashboard(_ runtime.Sequence, _ interface{}) (runtime.TaskExecutionFunc, string) {
return func(_ context.Context, _ *log.Logger, r runtime.Runtime) error {
ttyNumber := constants.DashboardTTY
system.Services(r).LoadAndStart(&services.Dashboard{})
system.Services(r).LoadAndStart(&services.Dashboard{
TTYNumber: ttyNumber,
})
return console.Switch(ttyNumber)
return nil
}, "startDashboard"
}

View File

@ -15,15 +15,14 @@ import (
"github.com/siderolabs/talos/internal/app/machined/pkg/system/runner/process"
"github.com/siderolabs/talos/internal/app/machined/pkg/system/runner/restart"
"github.com/siderolabs/talos/internal/pkg/capability"
"github.com/siderolabs/talos/internal/pkg/console"
"github.com/siderolabs/talos/pkg/conditions"
"github.com/siderolabs/talos/pkg/machinery/constants"
)
// Dashboard implements the Service interface. It serves as the concrete type with
// the required methods.
type Dashboard struct {
TTYNumber int
}
type Dashboard struct{}
// ID implements the Service interface.
func (d *Dashboard) ID(_ runtime.Runtime) string {
@ -32,12 +31,12 @@ func (d *Dashboard) ID(_ runtime.Runtime) string {
// PreFunc implements the Service interface.
func (d *Dashboard) PreFunc(_ context.Context, _ runtime.Runtime) error {
return nil
return console.Switch(constants.DashboardTTY)
}
// PostFunc implements the Service interface.
func (d *Dashboard) PostFunc(_ runtime.Runtime, _ events.ServiceState) error {
return nil
return console.Switch(constants.KernelLogsTTY)
}
// Condition implements the Service interface.
@ -52,7 +51,7 @@ func (d *Dashboard) DependsOn(_ runtime.Runtime) []string {
// Runner implements the Service interface.
func (d *Dashboard) Runner(r runtime.Runtime) (runner.Runner, error) {
tty := fmt.Sprintf("/dev/tty%d", d.TTYNumber)
tty := fmt.Sprintf("/dev/tty%d", constants.DashboardTTY)
return restart.New(process.NewRunner(false, &runner.Args{
ID: d.ID(r),

View File

@ -10,6 +10,8 @@ import (
"os"
"syscall"
"unsafe"
"github.com/siderolabs/talos/pkg/machinery/constants"
)
const (
@ -28,9 +30,9 @@ const (
// Switch switches the active console to the specified tty.
func Switch(ttyNumber int) error {
// redirect the kernel logs to tty1 instead of the currently used one,
// so that dashboard on tty2 does not get flooded with kernel logs
if err := redirectKernelLogs(1); err != nil {
// redirect the kernel logs to their own TTY instead of the currently used one,
// so that other TTYs (e.g., dashboard on tty2) do not get flooded with kernel logs
if err := redirectKernelLogs(constants.KernelLogsTTY); err != nil {
return err
}
@ -40,7 +42,7 @@ func Switch(ttyNumber int) error {
return err
}
defer tty0.Close() //nolint: errcheck
defer tty0.Close() //nolint:errcheck
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, tty0.Fd(), vtActivate, uintptr(ttyNumber)); errno != 0 {
return fmt.Errorf("failed to activate console: %w", errno)

View File

@ -149,6 +149,8 @@ func buildDashboard(ctx context.Context, cli *client.Client, opts ...Option) (*D
dashboard.pages = tview.NewPages().AddPage(pageMain, dashboard.mainGrid, true, true)
dashboard.app.SetRoot(dashboard.pages, true).SetFocus(dashboard.pages)
header := components.NewHeader()
dashboard.mainGrid.AddItem(header, 0, 0, 1, 1, 0, 0, false)
@ -301,25 +303,47 @@ func (d *Dashboard) initScreenConfigs(ctx context.Context, screens []Screen) err
}
// Run starts the dashboard.
func Run(ctx context.Context, cli *client.Client, opts ...Option) error {
func Run(ctx context.Context, cli *client.Client, opts ...Option) (runErr error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
dashboard, err := buildDashboard(ctx, cli, opts...)
if err != nil {
return err
}
// handle panic & stop dashboard gracefully on exit
defer func() {
if r := recover(); r != nil {
runErr = fmt.Errorf("dashboard panic: %v", r)
}
dashboard.app.Stop()
}()
dashboard.selectScreen(ScreenSummary)
eg, ctx := errgroup.WithContext(ctx)
stopFunc := dashboard.startDataHandler(ctx)
defer stopFunc() //nolint:errcheck
if err = dashboard.app.
SetRoot(dashboard.pages, true).
SetFocus(dashboard.pages).
Run(); err != nil {
return err
}
eg.Go(func() error {
defer cancel()
return stopFunc()
return dashboard.app.Run()
})
// stop dashboard when the context is canceled
eg.Go(func() error {
<-ctx.Done()
dashboard.app.Stop()
return nil
})
return eg.Wait()
}
// startDataHandler starts the data and log update handler and returns a function to stop it.

View File

@ -867,6 +867,9 @@ const (
// APIAuthzRoleMetadataKey is the gRPC metadata key used to submit a role with os:impersonator.
APIAuthzRoleMetadataKey = "talos-role"
// KernelLogsTTY is the number of the TTY device (/dev/ttyN) to redirect Kernel logs to.
KernelLogsTTY = 1
// DashboardTTY is the number of the TTY device (/dev/ttyN) for dashboard.
DashboardTTY = 2