1 package xrandr
2
3 import (
4 "fmt"
5 "io"
6 "os/exec"
7 "time"
8
9 "github.com/go-logr/logr"
10 "github.com/jezek/xgb"
11 "github.com/jezek/xgb/randr"
12 "github.com/jezek/xgb/xproto"
13
14 "edge-infra.dev/pkg/sds/lib/xorg"
15 )
16
17 const xrandrCmd = "xrandr"
18
19
20 type Xrandr interface {
21
22
23 WaitUntilReady(minWaitTime, timeout time.Duration) error
24
25 GetOutputs() (Outputs, error)
26
27 GetPoweredOutputs() (Outputs, error)
28 }
29
30 type xrandr struct {
31 log logr.Logger
32 }
33
34
35 func NewXrandr() Xrandr {
36 xLogger := logr.Discard()
37 xgb.Logger.SetOutput(io.Discard)
38 return &xrandr{
39 log: xLogger,
40 }
41 }
42
43
44 func NewXrandrWithLogger(log logr.Logger) Xrandr {
45 xLogger := log.WithName("xrandr")
46 xgb.Logger.SetOutput(io.Discard)
47 return &xrandr{
48 log: xLogger,
49 }
50 }
51
52 func (x *xrandr) WaitUntilReady(minWaitTime, timeout time.Duration) error {
53 time.Sleep(minWaitTime)
54
55 xgbConn, rootWindow, err := x.randrInit()
56 if err != nil {
57 return fmt.Errorf("failed to initialize xrandr: %w", err)
58 }
59
60 return waitForEventSettle(timeout, xgbConn, rootWindow)
61 }
62
63 func (x *xrandr) GetPoweredOutputs() (Outputs, error) {
64 outputs, err := x.GetOutputs()
65 if err != nil {
66 return nil, err
67 }
68
69 var poweredOutputs = Outputs{}
70 for _, output := range outputs {
71 if output.Crtc != nil {
72 poweredOutputs = append(poweredOutputs, output)
73 }
74 }
75
76 return poweredOutputs, nil
77 }
78
79 func (x *xrandr) GetOutputs() (Outputs, error) {
80 xgbConn, rootWindow, err := x.randrInit()
81 if err != nil {
82 return nil, fmt.Errorf("failed to initialize xrandr: %w", err)
83 }
84
85 screenResources, err := randr.GetScreenResourcesCurrent(xgbConn, *rootWindow).Reply()
86 if err != nil {
87 return nil, fmt.Errorf("failed to get screen resources: %w", err)
88 }
89
90 var outputs = Outputs{}
91 for _, randrOutput := range screenResources.Outputs {
92 output, err := createOutputForRandrOutput(xgbConn, randrOutput, screenResources, rootWindow, x.log)
93 if err != nil {
94 x.log.Info("failed to extract output information: " + err.Error())
95 continue
96 }
97 outputs = append(outputs, *output)
98 }
99
100 return outputs, nil
101 }
102
103 func (x *xrandr) randrInit() (*xgb.Conn, *xproto.Window, error) {
104 xgbConn, err := xorg.GetXGBConnection()
105 rootWindow := xproto.Setup(xgbConn).DefaultScreen(xgbConn).Root
106 if err != nil {
107 return nil, nil, err
108 }
109
110 if err := randr.Init(xgbConn); err != nil {
111 return nil, nil, err
112 }
113
114
115
116 if err = refreshXrandr(); err != nil {
117 return nil, nil, err
118 }
119
120 return xgbConn, &rootWindow, nil
121 }
122
123
124
125 func refreshXrandr() error {
126 cmd := exec.Command(xrandrCmd)
127 if err := cmd.Start(); err != nil {
128 return fmt.Errorf("unable to start xrandr: %w", err)
129 }
130
131
132 exitChan := make(chan error)
133 go func() {
134 exitChan <- cmd.Wait()
135 }()
136
137
138 killChan := make(chan error)
139 timer := time.AfterFunc(3*time.Second, func() {
140 killChan <- cmd.Process.Kill()
141 })
142
143
144 select {
145 case err := <-exitChan:
146 timer.Stop()
147 if _, isExitErr := err.(*exec.ExitError); !isExitErr && err != nil {
148 return fmt.Errorf("exit error received from xrandr: %w", err)
149 }
150 case err := <-killChan:
151 if err != nil {
152 return fmt.Errorf("failed to kill xrandr: %w", err)
153 }
154 }
155
156 return nil
157 }
158
View as plain text