...

Source file src/edge-infra.dev/pkg/sds/lib/xorg/xrandr/xrandr.go

Documentation: edge-infra.dev/pkg/sds/lib/xorg/xrandr

     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  // Xrandr provides methods for querying the X server with Xrandr.
    20  type Xrandr interface {
    21  	// WaitUntilReady() returns once the connection to Xrandr is
    22  	// ready, erroring if the timeout is reached.
    23  	WaitUntilReady(minWaitTime, timeout time.Duration) error
    24  	// GetOutputs() returns the outputs from Xrandr.
    25  	GetOutputs() (Outputs, error)
    26  	// GetPoweredOutputs() returns the powered outputs from Xrandr.
    27  	GetPoweredOutputs() (Outputs, error)
    28  }
    29  
    30  type xrandr struct {
    31  	log logr.Logger
    32  }
    33  
    34  // NewXrandr() returns a new Xrandr instance for querying the X server with Xrandr.
    35  func NewXrandr() Xrandr {
    36  	xLogger := logr.Discard()
    37  	xgb.Logger.SetOutput(io.Discard)
    38  	return &xrandr{
    39  		log: xLogger,
    40  	}
    41  }
    42  
    43  // TODO: don't log, return better errors
    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  	// xgb xrandr fails to update available screens
   115  	// after re-plugs. refreshXrandr() forces it to update.
   116  	if err = refreshXrandr(); err != nil {
   117  		return nil, nil, err
   118  	}
   119  
   120  	return xgbConn, &rootWindow, nil
   121  }
   122  
   123  // Refresh screens by running Xrandr directly on the pod. The process is
   124  // killed after 3 seconds if it has not already exited.
   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  	// send exit response to exit channel
   132  	exitChan := make(chan error)
   133  	go func() {
   134  		exitChan <- cmd.Wait()
   135  	}()
   136  
   137  	// kill the process after 3 seconds and send exit response to kill channel
   138  	killChan := make(chan error)
   139  	timer := time.AfterFunc(3*time.Second, func() {
   140  		killChan <- cmd.Process.Kill()
   141  	})
   142  
   143  	// handle first result from either exit or kill channel
   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