1 package xserver
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "net/http"
8 "os"
9 "time"
10
11 "github.com/go-logr/logr"
12 ctrl "sigs.k8s.io/controller-runtime"
13 "sigs.k8s.io/controller-runtime/pkg/client"
14 "sigs.k8s.io/controller-runtime/pkg/healthz"
15
16 "edge-infra.dev/pkg/sds/display/constants"
17 "edge-infra.dev/pkg/sds/display/displaymanager/manager"
18 xserverconfig "edge-infra.dev/pkg/sds/display/k8s/controllers/xserver/config"
19 "edge-infra.dev/pkg/sds/lib/os/env"
20 "edge-infra.dev/pkg/sds/lib/process/processmanager"
21 )
22
23 const (
24 xinitName = "xinit"
25 xinitPath = "/usr/bin/xinit"
26 noCursorFlag = "-nocursor"
27
28 waitingForConfigMessage = "waiting to receive config... (at least one ConfigMap must be present)"
29
30 timeout = time.Second * 10
31 minWaitTime = time.Second * 3
32 )
33
34 var baseXinitArgs = []string{
35 "/etc/X11/xinit/Xsession",
36 "openbox-session",
37 "--",
38 "/usr/bin/X",
39 ":0",
40 "vt7",
41 "-logfile", "/dev/stdout",
42 "-logverbose", env.New().Get("LOGLVL", "3"),
43 }
44
45 var (
46 errUseGetForHealthz = fmt.Errorf("expected GET for healthz check")
47 errSocketNotReady = fmt.Errorf("%s socket is not ready", constants.X11Socket)
48 )
49
50 func configMapNamespaceNames(hostname string) []string {
51 return []string{
52 fmt.Sprintf("%s/%s", constants.Namespace, xserverconfig.GlobalOpenboxConfig),
53 fmt.Sprintf("%s/%s", constants.Namespace, xserverconfig.ConfigMapNameFromHostname(hostname)),
54 }
55 }
56
57
58
59 type Runnable struct {
60 Name string
61 Hostname string
62
63
64 processmanager.Process
65
66
67 *xserverconfig.Config
68
69 configChan configChannel
70
71 client client.Client
72 healthz healthz.Checker
73 }
74
75 func NewXServerRunnable(displayManager manager.DisplayManager, configChan configChannel, c client.Client, log logr.Logger) (*Runnable, error) {
76 config := &xserverconfig.Config{}
77
78 proc, err := processmanager.NewProcess(xinitName, xinitPath)
79 if err != nil {
80 return nil, err
81 }
82
83 healthz := createHealthzCheck(log)
84 readyCheck := createReadyCheck(displayManager)
85
86 proc.WithLogger(log, true)
87 proc.WithPreStartHooks(config.UpdateXorgConf, config.UpdateOpenboxConf)
88 proc.WithReadyCheck(readyCheck)
89 proc.WithWaitUntilReady(timeout)
90 proc.WithExpectNoExit()
91
92 return &Runnable{
93 Name: constants.XServerManagerName,
94 Hostname: displayManager.Hostname(),
95 Process: proc,
96 Config: config,
97 configChan: configChan,
98 client: c,
99 healthz: healthz,
100 }, nil
101 }
102
103 func createReadyCheck(displayManager manager.DisplayManager) processmanager.ReadyCheckFunc {
104 return func(ctx context.Context) (bool, error) {
105 if err := displayManager.Wait(ctx); err != nil {
106 return false, nil
107 }
108 return true, nil
109 }
110 }
111
112 func createHealthzCheck(log logr.Logger) healthz.Checker {
113 return func(req *http.Request) error {
114 if req.Method != http.MethodGet {
115 return fmt.Errorf("%w, got: %s", errUseGetForHealthz, req.Method)
116 }
117 if _, err := os.Stat(constants.X11Socket); err != nil {
118 err = fmt.Errorf("%w: %w", errSocketNotReady, err)
119 log.Error(err, "healthz check failed")
120 return err
121 }
122 return nil
123 }
124 }
125
126 func (r *Runnable) SetupWithManager(mgr ctrl.Manager) error {
127 if err := mgr.Add(r); err != nil {
128 return err
129 }
130 return mgr.AddHealthzCheck("xserver-status", r.healthz)
131 }
132
133
134
135
136
137
138 func (r *Runnable) Start(ctx context.Context) (err error) {
139 log := ctrl.LoggerFrom(ctx).WithName(r.Name)
140 log.Info("starting xserver")
141 log.Info(waitingForConfigMessage, "configMaps", configMapNamespaceNames(r.Hostname))
142
143 defer func() {
144 log.Info("waiting for X to shutdown")
145 stopCtx, cancel := context.WithTimeout(context.Background(), timeout)
146 defer cancel()
147 err = errors.Join(err, r.WaitUntilStopped(stopCtx))
148 }()
149
150 for {
151 select {
152 case config := <-r.configChan:
153 log.Info("received new config, restarting X")
154 config.DeepCopyInto(r.Config)
155 if err := r.restart(ctx); err != nil {
156 return err
157 }
158 case result := <-r.Result():
159 return result
160 case <-ctx.Done():
161 return nil
162 }
163 }
164 }
165
166
167 func (r *Runnable) restart(ctx context.Context) error {
168 args := xinitArgs(r.CursorEnabled())
169 r.WithArgs(args...)
170 return r.Restart(ctx)
171 }
172
173 func xinitArgs(cursorEnabled bool) []string {
174 args := baseXinitArgs
175
176 if !cursorEnabled {
177 args = append(args, noCursorFlag)
178 }
179
180 return args
181 }
182
View as plain text