package xserver import ( "context" "errors" "fmt" "net/http" "os" "time" "github.com/go-logr/logr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "edge-infra.dev/pkg/sds/display/constants" "edge-infra.dev/pkg/sds/display/displaymanager/manager" xserverconfig "edge-infra.dev/pkg/sds/display/k8s/controllers/xserver/config" "edge-infra.dev/pkg/sds/lib/os/env" "edge-infra.dev/pkg/sds/lib/process/processmanager" ) const ( xinitName = "xinit" xinitPath = "/usr/bin/xinit" noCursorFlag = "-nocursor" waitingForConfigMessage = "waiting to receive config... (at least one ConfigMap must be present)" timeout = time.Second * 10 minWaitTime = time.Second * 3 ) var baseXinitArgs = []string{ "/etc/X11/xinit/Xsession", "openbox-session", "--", "/usr/bin/X", ":0", "vt7", "-logfile", "/dev/stdout", "-logverbose", env.New().Get("LOGLVL", "3"), } var ( errUseGetForHealthz = fmt.Errorf("expected GET for healthz check") errSocketNotReady = fmt.Errorf("%s socket is not ready", constants.X11Socket) ) func configMapNamespaceNames(hostname string) []string { return []string{ fmt.Sprintf("%s/%s", constants.Namespace, xserverconfig.GlobalOpenboxConfig), fmt.Sprintf("%s/%s", constants.Namespace, xserverconfig.ConfigMapNameFromHostname(hostname)), } } // Runnable is a thread which manages the X process. It will start the xinit process // and restart it on any changes to the configuration sent from the config controller. type Runnable struct { Name string Hostname string // Handles the lifecycle of the xinit process. processmanager.Process // Config configures the X server. *xserverconfig.Config // configChan is used to receive configuration from the X server config controller. configChan configChannel client client.Client healthz healthz.Checker } func NewXServerRunnable(displayManager manager.DisplayManager, configChan configChannel, c client.Client, log logr.Logger) (*Runnable, error) { config := &xserverconfig.Config{} proc, err := processmanager.NewProcess(xinitName, xinitPath) if err != nil { return nil, err } healthz := createHealthzCheck(log) readyCheck := createReadyCheck(displayManager) proc.WithLogger(log, true) proc.WithPreStartHooks(config.UpdateXorgConf, config.UpdateOpenboxConf) proc.WithReadyCheck(readyCheck) proc.WithWaitUntilReady(timeout) proc.WithExpectNoExit() return &Runnable{ Name: constants.XServerManagerName, Hostname: displayManager.Hostname(), Process: proc, Config: config, configChan: configChan, client: c, healthz: healthz, }, nil } func createReadyCheck(displayManager manager.DisplayManager) processmanager.ReadyCheckFunc { return func(ctx context.Context) (bool, error) { if err := displayManager.Wait(ctx); err != nil { return false, nil } return true, nil } } func createHealthzCheck(log logr.Logger) healthz.Checker { return func(req *http.Request) error { if req.Method != http.MethodGet { return fmt.Errorf("%w, got: %s", errUseGetForHealthz, req.Method) } if _, err := os.Stat(constants.X11Socket); err != nil { err = fmt.Errorf("%w: %w", errSocketNotReady, err) log.Error(err, "healthz check failed") return err } return nil } } func (r *Runnable) SetupWithManager(mgr ctrl.Manager) error { if err := mgr.Add(r); err != nil { return err } return mgr.AddHealthzCheck("xserver-status", r.healthz) } // +kubebuilder:rbac:groups=display.edge.ncr.com,resources=nodedisplayconfigs,verbs=create;get;list;watch // Runs the xinit process, restarting it on any changes to X server configuration. // // Exits when context is cancelled or any errors are returned from the process manager. func (r *Runnable) Start(ctx context.Context) (err error) { log := ctrl.LoggerFrom(ctx).WithName(r.Name) log.Info("starting xserver") log.Info(waitingForConfigMessage, "configMaps", configMapNamespaceNames(r.Hostname)) defer func() { log.Info("waiting for X to shutdown") stopCtx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() err = errors.Join(err, r.WaitUntilStopped(stopCtx)) }() for { select { case config := <-r.configChan: log.Info("received new config, restarting X") config.DeepCopyInto(r.Config) if err := r.restart(ctx); err != nil { return err } case result := <-r.Result(): return result case <-ctx.Done(): return nil } } } // Configures xinit arguments and restarts the xinit process. func (r *Runnable) restart(ctx context.Context) error { args := xinitArgs(r.CursorEnabled()) r.WithArgs(args...) return r.Restart(ctx) } func xinitArgs(cursorEnabled bool) []string { args := baseXinitArgs if !cursorEnabled { args = append(args, noCursorFlag) } return args }