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:
parent
544cb4fe7d
commit
355681ddab
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user