Repository: ubuntu/ubuntu-insights
Pull request #85: feat(hardware_linux): Screen collection Wayland support
Author: login: hk21702
Created: 2025-04-09T19:50:07Z | Merged: 2025-04-16T19:21:22Z

Implements Wayland support for screen collection.
Note that this method only provides the physical resolution, not the post scaled resolution. There isn't native support for fractional scaling, so this implementation doesn't try to get the scaled resolution.
- Wayland is prioritized, if it errors or does not return any results, fall back to `xrandr`
- Don't collect screens at all on WSL

Requires `libwayland-dev` to build.

---
[UDENG-6533](https://warthogs.atlassian.net/browse/UDENG-6533)

[UDENG-6533]: https://warthogs.atlassian.net/browse/UDENG-6533?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/.github/workflows/qa.yaml b/.github/workflows/qa.yaml
index 167095ce..cb0b338f 100644
--- a/.github/workflows/qa.yaml
+++ b/.github/workflows/qa.yaml
@@ -10,6 +10,7 @@ on:
 env:
   DEBIAN_FRONTEND: noninteractive
   GO_TESTS_TIMEOUT: 20m
+  apt_deps: "libwayland-dev"
 
 jobs:
   go-sanity:
@@ -45,6 +46,12 @@ jobs:
         with:
           go-version-file: go.mod
 
+      - name: Install dependencies on Linux
+        if: runner.os == 'Linux'
+        run: |
+          sudo apt update
+          sudo apt install -y ${{ env.apt_deps }}
+
       - name: Install gotestfmt and our wrapper script
         uses: canonical/desktop-engineering/gh-actions/go/gotestfmt@main
 
diff --git a/debian/changelog b/debian/changelog
index 8aa57c4d..ae1954ff 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,5 +1,5 @@
-ubuntu-insights (0.0.1~ppa3) plucky; urgency=medium
+ubuntu-insights (0.0.1~ppa5) plucky; urgency=medium
 
   * Initial release
 
- -- Kat Kuo <kat.kuo@canonical.com>  Fri, 21 Mar 2025 15:44:38 -0400
+ -- Kat Kuo <kat.kuo@canonical.com>  Wed, 16 Apr 2025 13:38:25 -0400
diff --git a/debian/control b/debian/control
index bb9fb378..1e9435d3 100644
--- a/debian/control
+++ b/debian/control
@@ -7,6 +7,7 @@ Build-Depends: debhelper-compat (= 13),
                dh-golang,
                golang-go (>= 2:1.24~) | golang-1.24-go,
                dctrl-tools,
+               libwayland-dev,
 Standards-Version: 4.6.2
 Vcs-Browser: https://salsa.debian.org/go-team/packages/ubuntu-insights
 Vcs-Git: https://salsa.debian.org/go-team/packages/ubuntu-insights.git
@@ -17,6 +18,7 @@ Package: ubuntu-insights
 Architecture: any
 Depends: ${misc:Depends},
          ${shlibs:Depends},
+         libwayland-client0,
 Built-Using: ${misc:Built-Using}
 Description: Ubuntu metrics reporting service
  Ubuntu Insights, a user transparent, open, 
diff --git a/internal/collector/sysinfo/hardware/export_linux_test.go b/internal/collector/sysinfo/hardware/export_linux_test.go
index 06ef4872..e1505f28 100644
--- a/internal/collector/sysinfo/hardware/export_linux_test.go
+++ b/internal/collector/sysinfo/hardware/export_linux_test.go
@@ -1,5 +1,8 @@
 package hardware
 
+type Screen = screen
+type CWaylandDisplay = cWaylandDisplay
+
 // WithRoot overrides default root directory of the system.
 func WithRoot(root string) Options {
 	return func(o *options) {
@@ -27,3 +30,10 @@ func WithScreenInfo(cmd []string) Options {
 		o.platform.screenCmd = cmd
 	}
 }
+
+// WithWaylandProvider overrides default wayland provider.
+func WithWaylandProvider(wp waylandProvider) Options {
+	return func(o *options) {
+		o.platform.wayland = wp
+	}
+}
diff --git a/internal/collector/sysinfo/hardware/hardware.go b/internal/collector/sysinfo/hardware/hardware.go
index 83ce09e3..d79f0b6b 100644
--- a/internal/collector/sysinfo/hardware/hardware.go
+++ b/internal/collector/sysinfo/hardware/hardware.go
@@ -57,7 +57,7 @@ type disk struct {
 	Children []disk `json:"children,omitempty"`
 }
 
-// screen contains information for a screen.
+// screen contains information for a Screen.
 type screen struct {
 	PhysicalResolution string `json:"physicalResolution,omitempty"`
 	Size               string `json:"size,omitempty"`
diff --git a/internal/collector/sysinfo/hardware/hardware_linux.go b/internal/collector/sysinfo/hardware/hardware_linux.go
index 474595f5..919eb9f1 100644
--- a/internal/collector/sysinfo/hardware/hardware_linux.go
+++ b/internal/collector/sysinfo/hardware/hardware_linux.go
@@ -1,5 +1,11 @@
 package hardware
 
+/*
+#cgo LDFLAGS: -lwayland-client
+#include "wayland_displays_linux.h"
+*/
+import "C"
+
 import (
 	"context"
 	"fmt"
@@ -9,7 +15,9 @@ import (
 	"regexp"
 	"strconv"
 	"strings"
+	"sync"
 	"time"
+	"unsafe"
 
 	"github.com/ubuntu/ubuntu-insights/internal/cmdutils"
 	"github.com/ubuntu/ubuntu-insights/internal/collector/sysinfo/platform"
@@ -21,6 +29,7 @@ type platformOptions struct {
 	cpuInfoCmd []string
 	lsblkCmd   []string
 	screenCmd  []string
+	wayland    waylandProvider
 }
 
 // defaultOptions returns options for when running under a normal environment.
@@ -30,9 +39,16 @@ func defaultPlatformOptions() platformOptions {
 		cpuInfoCmd: []string{"lscpu", "-J"},
 		lsblkCmd:   []string{"lsblk", "-o", "NAME,SIZE,TYPE,RM", "--tree", "-J"},
 		screenCmd:  []string{"xrandr"},
+		wayland:    &realWaylandProvider{},
 	}
 }
 
+type waylandProvider interface {
+	InitWayland() int
+}
+
+var waylandMutex sync.Mutex
+
 // collectProduct reads sysfs to find information about the system.
 func (h Collector) collectProduct(pi platform.Info) (product, error) {
 	if pi.WSL.SubsystemVersion != 0 {
@@ -378,15 +394,21 @@ var screenHeaderRegex = regexp.MustCompile(`(?m)^(\S+)\s+connected\s+(?:(primary
 // However: "720x480 60.00+ 120.00" does not match.
 var screenConfigRegex = regexp.MustCompile(`(?m)^\s*([0-9]+x[0-9]+)\s.*?([0-9]+\.[0-9]+)\+?\*\+?.*$`)
 
-// collectScreens uses xrandr to collect information about screens.
+// collectScreens collects screen information. Skips collection on WSL.
 func (h Collector) collectScreens(pi platform.Info) (info []screen, err error) {
+	if pi.WSL.SubsystemVersion != 0 {
+		h.log.Debug("skipping screen info collection on WSL")
+		return []screen{}, nil
+	}
+
+	info, err = h.cScreensWayland()
+	if err == nil && len(info) > 0 {
+		return info, nil
+	}
+
+	// Fall back to xrandr if Wayland fails.
 	stdout, stderr, err := cmdutils.RunWithTimeout(context.Background(), 15*time.Second, h.platform.screenCmd[0], h.platform.screenCmd[1:]...)
 	if err != nil {
-		if pi.WSL.SubsystemVersion != 0 {
-			h.log.Debug("skipping screen info collection on WSL")
-			return []screen{}, nil
-		}
-
 		return nil, fmt.Errorf("failed to run xrandr: %v", err)
 	}
 	if stderr.Len() > 0 {
@@ -424,6 +446,43 @@ func (h Collector) collectScreens(pi platform.Info) (info []screen, err error) {
 			RefreshRate: v[2],
 		})
 	}
-
 	return info, nil
 }
+
+type realWaylandProvider struct{}
+
+func (*realWaylandProvider) InitWayland() int {
+	return int(C.init_wayland())
+}
+
+func (h Collector) cScreensWayland() (screens []screen, err error) {
+	waylandMutex.Lock()
+	defer waylandMutex.Unlock()
+	if h.platform.wayland.InitWayland() != 0 {
+		return nil, fmt.Errorf("failed to connect to Wayland display")
+	}
+	defer C.cleanup()
+
+	if bool(C.had_memory_error()) {
+		h.log.Warn("Memory error while trying to get displays")
+		return nil, fmt.Errorf("failed to get displays")
+	}
+
+	count := int(C.get_output_count())
+	screens = make([]screen, count)
+	displays := C.get_displays()
+	for i := range count {
+		display := *(**C.struct_wayland_display)(unsafe.Pointer(uintptr(unsafe.Pointer(displays)) + uintptr(i)*unsafe.Sizeof((*C.struct_wayland_display)(nil))))
+		if display.width != 0 && display.height != 0 {
+			screens[i].PhysicalResolution = fmt.Sprintf("%dx%d", display.width, display.height)
+		}
+		if display.refresh != 0 {
+			screens[i].RefreshRate = fmt.Sprintf("%.2f", float64(display.refresh)/1000)
+		}
+		if display.phys_width != 0 && display.phys_height != 0 {
+			screens[i].Size = fmt.Sprintf("%dmm x %dmm", display.phys_width, display.phys_height)
+		}
+	}
+
+	return screens, nil
+}
diff --git a/internal/collector/sysinfo/hardware/hardware_linux_test.go b/internal/collector/sysinfo/hardware/hardware_linux_test.go
index 3adad372..89759bee 100644
--- a/internal/collector/sysinfo/hardware/hardware_linux_test.go
+++ b/internal/collector/sysinfo/hardware/hardware_linux_test.go
@@ -47,6 +47,15 @@ func TestNewLinux(t *testing.T) {
 func TestCollectLinux(t *testing.T) {
 	t.Parallel()
 
+	type waylandType int
+	const (
+		WaylandNone waylandType = iota
+		WaylandMemoryError
+		WaylandNoDisplay
+		WaylandSingleDisplay
+		WaylandMultipleDisplays
+	)
+
 	tests := map[string]struct {
 		root         string
 		cpuInfo      string
@@ -54,6 +63,7 @@ func TestCollectLinux(t *testing.T) {
 		screenInfo   string
 		missingFiles []string
 		pinfo        platform.Info
+		wayland      waylandType
 
 		logs    map[slog.Level]uint
 		wantErr bool
@@ -246,6 +256,18 @@ func TestCollectLinux(t *testing.T) {
 			},
 		},
 
+		"Error no exit Screen warns": {
+			root:       "regular",
+			cpuInfo:    "regular",
+			blkInfo:    "regular",
+			screenInfo: "error no exit",
+
+			logs: map[slog.Level]uint{
+				slog.LevelWarn: 1,
+				slog.LevelInfo: 1,
+			},
+		},
+
 		"Garbage Screen information is empty": {
 			root:       "regular",
 			cpuInfo:    "regular",
@@ -268,6 +290,45 @@ func TestCollectLinux(t *testing.T) {
 			},
 		},
 
+		"Wayland MemoryError warns and falls back": {
+			root:       "regular",
+			cpuInfo:    "regular",
+			blkInfo:    "regular",
+			screenInfo: "regular",
+
+			wayland: WaylandMemoryError,
+			logs: map[slog.Level]uint{
+				slog.LevelWarn: 1,
+			},
+		},
+
+		"Wayland NoDisplay falls back": {
+			root:       "regular",
+			cpuInfo:    "regular",
+			blkInfo:    "regular",
+			screenInfo: "regular",
+
+			wayland: WaylandNoDisplay,
+		},
+
+		"Wayland SingleDisplay is sane": {
+			root:       "regular",
+			cpuInfo:    "regular",
+			blkInfo:    "regular",
+			screenInfo: "regular",
+
+			wayland: WaylandSingleDisplay,
+		},
+
+		"Wayland MultipleDisplays is sane": {
+			root:       "regular",
+			cpuInfo:    "regular",
+			blkInfo:    "regular",
+			screenInfo: "regular",
+
+			wayland: WaylandMultipleDisplays,
+		},
+
 		"Missing hardware information is empty": {
 			root:       "withoutinfo",
 			cpuInfo:    "",
@@ -320,7 +381,7 @@ func TestCollectLinux(t *testing.T) {
 				},
 			},
 		},
-		"WSL hardware information with xrandr": {
+		"WSL hardware information ignores xrandr": {
 			root:       "regular",
 			cpuInfo:    "regular",
 			blkInfo:    "regular",
@@ -369,32 +430,6 @@ func TestCollectLinux(t *testing.T) {
 				slog.LevelWarn: 3,
 			},
 		},
-		"Missing WSL hardware information warns when xrandr is installed but returns empty": {
-			root:       "withoutinfo",
-			cpuInfo:    "",
-			blkInfo:    "",
-			screenInfo: "",
-			missingFiles: []string{
-				// Product
-				"sys/class/dmi/id/product_family",
-				"sys/class/dmi/id/product_name",
-				"sys/class/dmi/id/sys_vendor",
-				// GPU
-				"sys/class/drm/c0",
-				"sys/class/drm/c1",
-				"sys/class/drm/card0-DP-1",
-				"sys/class/drm/card0-DP-2",
-				"sys/class/drm/card1",
-			},
-			pinfo: platform.Info{
-				WSL: platform.WSL{
-					SubsystemVersion: 2,
-				},
-			},
-			logs: map[slog.Level]uint{
-				slog.LevelWarn: 4,
-			},
-		},
 	}
 	for name, tc := range tests {
 		t.Run(name, func(t *testing.T) {
@@ -431,6 +466,27 @@ func TestCollectLinux(t *testing.T) {
 				options = append(options, hardware.WithScreenInfo(cmdArgs))
 			}
 
+			var wm waylandMock
+			switch tc.wayland {
+			case WaylandNone:
+				wm = waylandMock{initReturn: -1}
+			case WaylandMemoryError:
+				wm = waylandMock{initReturn: 0, memoryError: true}
+			case WaylandNoDisplay:
+				wm = waylandMock{initReturn: 0, displays: []hardware.CWaylandDisplay{}}
+			case WaylandSingleDisplay:
+				wm = waylandMock{initReturn: 0,
+					displays: []hardware.CWaylandDisplay{{Width: 1, Height: 1, Refresh: 10001, PhysWidth: 1, PhysHeight: 1}}}
+			case WaylandMultipleDisplays:
+				wm = waylandMock{initReturn: 0,
+					displays: []hardware.CWaylandDisplay{{Width: 2, Height: 2, Refresh: 10001, PhysWidth: 1, PhysHeight: 1},
+						{}, {Width: 3, Height: 3, Refresh: 10002, PhysWidth: 2, PhysHeight: 2}}}
+			default:
+				t.Fatalf("Setup: Wayland type not implemented, %d", tc.wayland)
+			}
+			wm.t = t
+			options = append(options, hardware.WithWaylandProvider(&wm))
+
 			l := testutils.NewMockHandler(slog.LevelDebug)
 			s := hardware.New(slog.New(&l), options...)
 
@@ -855,6 +911,8 @@ func TestFakeScreenList(_ *testing.T) {
 	case "error":
 		fmt.Fprint(os.Stderr, "Error requested in fake xrandr")
 		os.Exit(1)
+	case "error no exit":
+		fmt.Fprint(os.Stderr, "Error requested in fake xrandr")
 	case "regular":
 		fmt.Println(`Screen 0: minimum 8 x 8, current 6912 x 2160, maximum 32767 x 32767
 HDMI-0 connected primary 3840x2160+3072+0 (normal left inverted right x axis y axis) 598mm x 336mm
@@ -926,3 +984,15 @@ HDMI-0 connected primary 3840x2160+3072+0 (normal left inverted right x axis y a
 		os.Exit(0)
 	}
 }
+
+type waylandMock struct {
+	t           *testing.T
+	initReturn  int
+	memoryError bool
+	displays    []hardware.CWaylandDisplay
+}
+
+func (w *waylandMock) InitWayland() int {
+	hardware.TestingInitWayland(w.t, w.displays, w.memoryError)
+	return w.initReturn
+}
diff --git a/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/error_no_exit_screen_warns b/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/error_no_exit_screen_warns
new file mode 100644
index 00000000..70479c9f
--- /dev/null
+++ b/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/error_no_exit_screen_warns
@@ -0,0 +1,55 @@
+product:
+    family: Framework DIY
+    name: DIY Framework Laptop 16
+    vendor: Framework
+cpu:
+    name: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+    vendor: GenuineIntel
+    arch: x86_64
+    cpus: 12
+    sockets: 1
+    cores: 6
+    threads: 2
+gpus:
+    - name: Onboard - Video
+      device: 0xdevice0
+      vendor: "0x8086"
+      driver: i915
+    - name: NVIDIA GeForce GTX 1050 Ti
+      device: 0xdevice1
+      vendor: "0x10de"
+      driver: nvidia
+mem:
+    total: 31941
+blks:
+    - size: 953856
+      type: disk
+      children:
+        - size: 1024
+          type: part
+          children: []
+        - size: 2048
+          type: part
+          children: []
+        - size: 950784
+          type: part
+          children:
+            - size: 950681
+              type: crypt
+              children:
+                - size: 950681
+                  type: lvm
+                  children: []
+        - size: 358400
+          type: part
+          children:
+            - size: 179200
+              type: part
+              children: []
+            - size: 102400
+              type: part
+              children: []
+            - size: 76800
+              type: part
+              children: []
+screens: []
diff --git a/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/missing_wsl_hardware_information_warns_when_xrandr_is_installed_but_returns_empty b/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/missing_wsl_hardware_information_warns_when_xrandr_is_installed_but_returns_empty
deleted file mode 100644
index 7070a1a9..00000000
--- a/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/missing_wsl_hardware_information_warns_when_xrandr_is_installed_but_returns_empty
+++ /dev/null
@@ -1,17 +0,0 @@
-product:
-    family: ""
-    name: ""
-    vendor: ""
-cpu:
-    name: ""
-    vendor: ""
-    arch: amd64
-    cpus: 0
-    sockets: 0
-    cores: 0
-    threads: 0
-gpus: []
-mem:
-    total: 0
-blks: []
-screens: []
diff --git a/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/wayland_memoryerror_warns_and_falls_back b/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/wayland_memoryerror_warns_and_falls_back
new file mode 100644
index 00000000..1310a27a
--- /dev/null
+++ b/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/wayland_memoryerror_warns_and_falls_back
@@ -0,0 +1,63 @@
+product:
+    family: Framework DIY
+    name: DIY Framework Laptop 16
+    vendor: Framework
+cpu:
+    name: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+    vendor: GenuineIntel
+    arch: x86_64
+    cpus: 12
+    sockets: 1
+    cores: 6
+    threads: 2
+gpus:
+    - name: Onboard - Video
+      device: 0xdevice0
+      vendor: "0x8086"
+      driver: i915
+    - name: NVIDIA GeForce GTX 1050 Ti
+      device: 0xdevice1
+      vendor: "0x10de"
+      driver: nvidia
+mem:
+    total: 31941
+blks:
+    - size: 953856
+      type: disk
+      children:
+        - size: 1024
+          type: part
+          children: []
+        - size: 2048
+          type: part
+          children: []
+        - size: 950784
+          type: part
+          children:
+            - size: 950681
+              type: crypt
+              children:
+                - size: 950681
+                  type: lvm
+                  children: []
+        - size: 358400
+          type: part
+          children:
+            - size: 179200
+              type: part
+              children: []
+            - size: 102400
+              type: part
+              children: []
+            - size: 76800
+              type: part
+              children: []
+screens:
+    - physicalresolution: ""
+      size: 598mm x 336mm
+      resolution: 1920x1080
+      refreshrate: "60.00"
+    - physicalresolution: ""
+      size: 344mm x 193mm
+      resolution: 1920x1080
+      refreshrate: "60.03"
diff --git a/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/wayland_multipledisplays_is_sane b/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/wayland_multipledisplays_is_sane
new file mode 100644
index 00000000..7e7248a4
--- /dev/null
+++ b/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/wayland_multipledisplays_is_sane
@@ -0,0 +1,67 @@
+product:
+    family: Framework DIY
+    name: DIY Framework Laptop 16
+    vendor: Framework
+cpu:
+    name: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+    vendor: GenuineIntel
+    arch: x86_64
+    cpus: 12
+    sockets: 1
+    cores: 6
+    threads: 2
+gpus:
+    - name: Onboard - Video
+      device: 0xdevice0
+      vendor: "0x8086"
+      driver: i915
+    - name: NVIDIA GeForce GTX 1050 Ti
+      device: 0xdevice1
+      vendor: "0x10de"
+      driver: nvidia
+mem:
+    total: 31941
+blks:
+    - size: 953856
+      type: disk
+      children:
+        - size: 1024
+          type: part
+          children: []
+        - size: 2048
+          type: part
+          children: []
+        - size: 950784
+          type: part
+          children:
+            - size: 950681
+              type: crypt
+              children:
+                - size: 950681
+                  type: lvm
+                  children: []
+        - size: 358400
+          type: part
+          children:
+            - size: 179200
+              type: part
+              children: []
+            - size: 102400
+              type: part
+              children: []
+            - size: 76800
+              type: part
+              children: []
+screens:
+    - physicalresolution: 2x2
+      size: 1mm x 1mm
+      resolution: ""
+      refreshrate: "10.00"
+    - physicalresolution: ""
+      size: ""
+      resolution: ""
+      refreshrate: ""
+    - physicalresolution: 3x3
+      size: 2mm x 2mm
+      resolution: ""
+      refreshrate: "10.00"
diff --git a/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/wayland_nodisplay_falls_back b/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/wayland_nodisplay_falls_back
new file mode 100644
index 00000000..1310a27a
--- /dev/null
+++ b/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/wayland_nodisplay_falls_back
@@ -0,0 +1,63 @@
+product:
+    family: Framework DIY
+    name: DIY Framework Laptop 16
+    vendor: Framework
+cpu:
+    name: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+    vendor: GenuineIntel
+    arch: x86_64
+    cpus: 12
+    sockets: 1
+    cores: 6
+    threads: 2
+gpus:
+    - name: Onboard - Video
+      device: 0xdevice0
+      vendor: "0x8086"
+      driver: i915
+    - name: NVIDIA GeForce GTX 1050 Ti
+      device: 0xdevice1
+      vendor: "0x10de"
+      driver: nvidia
+mem:
+    total: 31941
+blks:
+    - size: 953856
+      type: disk
+      children:
+        - size: 1024
+          type: part
+          children: []
+        - size: 2048
+          type: part
+          children: []
+        - size: 950784
+          type: part
+          children:
+            - size: 950681
+              type: crypt
+              children:
+                - size: 950681
+                  type: lvm
+                  children: []
+        - size: 358400
+          type: part
+          children:
+            - size: 179200
+              type: part
+              children: []
+            - size: 102400
+              type: part
+              children: []
+            - size: 76800
+              type: part
+              children: []
+screens:
+    - physicalresolution: ""
+      size: 598mm x 336mm
+      resolution: 1920x1080
+      refreshrate: "60.00"
+    - physicalresolution: ""
+      size: 344mm x 193mm
+      resolution: 1920x1080
+      refreshrate: "60.03"
diff --git a/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/wayland_singledisplay_is_sane b/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/wayland_singledisplay_is_sane
new file mode 100644
index 00000000..bcb27c91
--- /dev/null
+++ b/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/wayland_singledisplay_is_sane
@@ -0,0 +1,59 @@
+product:
+    family: Framework DIY
+    name: DIY Framework Laptop 16
+    vendor: Framework
+cpu:
+    name: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+    vendor: GenuineIntel
+    arch: x86_64
+    cpus: 12
+    sockets: 1
+    cores: 6
+    threads: 2
+gpus:
+    - name: Onboard - Video
+      device: 0xdevice0
+      vendor: "0x8086"
+      driver: i915
+    - name: NVIDIA GeForce GTX 1050 Ti
+      device: 0xdevice1
+      vendor: "0x10de"
+      driver: nvidia
+mem:
+    total: 31941
+blks:
+    - size: 953856
+      type: disk
+      children:
+        - size: 1024
+          type: part
+          children: []
+        - size: 2048
+          type: part
+          children: []
+        - size: 950784
+          type: part
+          children:
+            - size: 950681
+              type: crypt
+              children:
+                - size: 950681
+                  type: lvm
+                  children: []
+        - size: 358400
+          type: part
+          children:
+            - size: 179200
+              type: part
+              children: []
+            - size: 102400
+              type: part
+              children: []
+            - size: 76800
+              type: part
+              children: []
+screens:
+    - physicalresolution: 1x1
+      size: 1mm x 1mm
+      resolution: ""
+      refreshrate: "10.00"
diff --git a/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/wsl_hardware_information_with_xrandr b/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/wsl_hardware_information_ignores_xrandr
similarity index 80%
rename from internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/wsl_hardware_information_with_xrandr
rename to internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/wsl_hardware_information_ignores_xrandr
index 245543c2..40408bc4 100644
--- a/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/wsl_hardware_information_with_xrandr
+++ b/internal/collector/sysinfo/hardware/testdata/TestCollectLinux/golden/wsl_hardware_information_ignores_xrandr
@@ -44,12 +44,4 @@ blks:
             - size: 76800
               type: part
               children: []
-screens:
-    - physicalresolution: ""
-      size: 598mm x 336mm
-      resolution: 1920x1080
-      refreshrate: "60.00"
-    - physicalresolution: ""
-      size: 344mm x 193mm
-      resolution: 1920x1080
-      refreshrate: "60.03"
+screens: []
diff --git a/internal/collector/sysinfo/hardware/wayland_displays_linux.c b/internal/collector/sysinfo/hardware/wayland_displays_linux.c
new file mode 100644
index 00000000..1631918b
--- /dev/null
+++ b/internal/collector/sysinfo/hardware/wayland_displays_linux.c
@@ -0,0 +1,171 @@
+#include "wayland_displays_linux.h"
+#include "wayland_displays_linux_test.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <wayland-client.h>
+
+static bool memory_error = false;
+
+static struct wayland_display **displays = NULL;
+static struct wl_output **outputs = NULL;
+
+static size_t count = 0;
+static size_t capacity = 4;
+
+static struct wl_display *display;
+static struct wl_registry *registry;
+
+// Function to handle geometry events from the Wayland output
+void handle_geometry(void *data, struct wl_output *output, int32_t x, int32_t y,
+                     int32_t physical_width, int32_t physical_height,
+                     int32_t subpixel, const char *make, const char *model,
+                     int32_t transform) {
+  struct wayland_display *info = (struct wayland_display *)data;
+  info->phys_width = physical_width;
+  info->phys_height = physical_height;
+}
+
+// Function to handle mode events from the Wayland output
+void handle_mode(void *data, struct wl_output *output, uint32_t flags,
+                 int32_t width, int32_t height, int32_t refresh) {
+  if (flags & WL_OUTPUT_MODE_CURRENT) {
+    struct wayland_display *info = (struct wayland_display *)data;
+    info->width = width;
+    info->height = height;
+    info->refresh = refresh;
+  }
+}
+
+static const struct wl_output_listener output_listener = {
+    .geometry = handle_geometry,
+    .mode = handle_mode,
+    .done = NULL,
+    .scale = NULL,
+};
+
+// Function to handle global events from the Wayland registry
+void global_handler(void *data, struct wl_registry *registry, uint32_t name,
+                    const char *interface, uint32_t version) {
+  if (strcmp(interface, "wl_output") != 0) {
+    return;
+  }
+  // Check if we need to increase the capacity of the arrays
+  if (count >= capacity) {
+    capacity *= 2;
+    struct wl_output **new_outputs =
+        realloc(outputs, capacity * sizeof(struct wl_output *));
+
+    struct wayland_display **new_displays =
+        realloc(displays, capacity * sizeof(struct wayland_display *));
+    if (!new_outputs || !new_displays) {
+      free(new_outputs);
+      free(new_displays);
+      new_displays = NULL;
+      new_outputs = NULL;
+      memory_error = true;
+      return;
+    }
+    outputs = new_outputs;
+    displays = new_displays;
+  }
+
+  struct wayland_display *display = malloc(sizeof(struct wayland_display));
+  if (!display) {
+    memory_error = true;
+    return;
+  }
+  memset(display, 0, sizeof(struct wayland_display));
+
+  struct wl_output *output =
+      wl_registry_bind(registry, name, &wl_output_interface, 1);
+
+  displays[count] = display;
+  outputs[count] = output;
+  count++;
+
+  wl_output_add_listener(output, &output_listener, display);
+}
+
+void global_remove(void *data, struct wl_registry *registry, uint32_t name) {}
+
+static const struct wl_registry_listener registry_listener = {
+    .global = global_handler,
+    .global_remove = global_remove,
+};
+
+int init_wayland() {
+  outputs = malloc(capacity * sizeof(struct wl_output *));
+  displays = malloc(capacity * sizeof(struct wayland_display *));
+
+  if (!outputs || !displays) {
+    free(outputs);
+    free(displays);
+    outputs = NULL;
+    displays = NULL;
+    memory_error = true;
+    return -1;
+  }
+
+  display = wl_display_connect(NULL);
+  if (!display) {
+    cleanup();
+    return -1;
+  }
+
+  registry = wl_display_get_registry(display);
+  wl_registry_add_listener(registry, &registry_listener, NULL);
+  if (wl_display_roundtrip(display) < 0) {
+    cleanup();
+    return -1;
+  }
+  if (wl_display_dispatch(display) < 0) {
+    cleanup();
+    return -1;
+  }
+  return 0;
+}
+
+void cleanup() {
+  if (outputs) {
+    for (size_t i = 0; i < count; i++) {
+      if (outputs[i]) {
+        wl_output_destroy(outputs[i]);
+      }
+    }
+  }
+
+  free(outputs);
+  outputs = NULL;
+
+  if (registry) {
+    wl_registry_destroy(registry);
+    registry = NULL;
+  }
+  if (display) {
+    wl_display_disconnect(display);
+    display = NULL;
+  }
+
+  for (size_t i = 0; i < count; i++) {
+    free(displays[i]);
+  }
+  free(displays);
+  displays = NULL;
+  count = 0;
+  capacity = 4;
+  memory_error = false;
+}
+
+struct wayland_display **get_displays() { return displays; }
+
+int get_output_count() { return count; }
+bool had_memory_error() { return memory_error; }
+
+void set_displays(struct wayland_display **new_displays, int c) {
+  cleanup();
+  displays = new_displays;
+  count = capacity = c;
+}
+
+void set_memory_error(bool error) { memory_error = error; }
diff --git a/internal/collector/sysinfo/hardware/wayland_displays_linux.h b/internal/collector/sysinfo/hardware/wayland_displays_linux.h
new file mode 100644
index 00000000..827bc6c6
--- /dev/null
+++ b/internal/collector/sysinfo/hardware/wayland_displays_linux.h
@@ -0,0 +1,30 @@
+#ifndef WAYLAND_DISPLAYS_H
+#define WAYLAND_DISPLAYS_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+// Structure representing the wayland display information
+struct wayland_display {
+  int32_t width;
+  int32_t height;
+  int32_t refresh;
+  int32_t phys_width;
+  int32_t phys_height;
+};
+// Initalize Wayland display information.
+int init_wayland();
+
+// Cleanup Wayland display information.
+void cleanup();
+
+// Get the Wayland display information.
+struct wayland_display **get_displays();
+
+// Get the number of Wayland displays.
+int get_output_count();
+
+// Checks if there was a memory error.
+bool had_memory_error();
+
+#endif // WAYLAND_DISPLAYS_H
diff --git a/internal/collector/sysinfo/hardware/wayland_displays_linux_test.h b/internal/collector/sysinfo/hardware/wayland_displays_linux_test.h
new file mode 100644
index 00000000..5fe8f7be
--- /dev/null
+++ b/internal/collector/sysinfo/hardware/wayland_displays_linux_test.h
@@ -0,0 +1,11 @@
+#ifndef WAYLAND_DISPLAYS_TEST_H
+#define WAYLAND_DISPLAYS_TEST_H
+#include <stdbool.h>
+
+// Setter function to set displays for testing purposes
+void set_displays(struct wayland_display **displays, int count);
+
+// Setter function to set memory error for testing purposes
+void set_memory_error(bool error);
+
+#endif // WAYLAND_DISPLAYS_TEST_H
diff --git a/internal/collector/sysinfo/hardware/waylandtest_linux.go b/internal/collector/sysinfo/hardware/waylandtest_linux.go
new file mode 100644
index 00000000..147e757b
--- /dev/null
+++ b/internal/collector/sysinfo/hardware/waylandtest_linux.go
@@ -0,0 +1,63 @@
+package hardware
+
+/*
+#cgo LDFLAGS: -lwayland-client
+#include "wayland_displays_linux.h"
+#include "wayland_displays_linux_test.h"
+*/
+import "C"
+
+import (
+	"testing"
+	"unsafe"
+)
+
+type cWaylandDisplay struct {
+	Width      int32
+	Height     int32
+	Refresh    int32
+	PhysWidth  int32
+	PhysHeight int32
+}
+
+// makeCWaylandDisplay converts a Go cWaylandDisplay struct to a C struct.
+func makeCWaylandDisplay(d cWaylandDisplay) (wd *C.struct_wayland_display) {
+	// Cleanup is done in C.
+	wd = (*C.struct_wayland_display)(C.malloc(C.size_t(C.sizeof_struct_wayland_display)))
+	wd.width = C.int(d.Width)
+	wd.height = C.int(d.Height)
+	wd.refresh = C.int(d.Refresh)
+	wd.phys_width = C.int(d.PhysWidth)
+	wd.phys_height = C.int(d.PhysHeight)
+	return wd
+}
+
+func makeCwaylandDisplays(d []cWaylandDisplay) (wds **C.struct_wayland_display) {
+	if len(d) == 0 {
+		return nil
+	}
+
+	// Cleanup is done in C.
+	wds = (**C.struct_wayland_display)(C.malloc(C.size_t(len(d)) * C.size_t(C.sizeof_struct_wayland_display)))
+
+	// Cast the C pointer 'wds' to a Go slice of *C.struct_wayland_display.
+	// We cast to a pointer to a huge array (size 1<<30) so that we guarantee enough
+	// room to index any element up to len(d), valid for both 32 and 64 bit systems.
+	// The huge array size doesn't cause massive allocation because we're only reinterpreting
+	// the memory pointed to by 'wds'. Then, we slice the array to exactly len(d) elements.
+	slice := (*[1 << 30]*C.struct_wayland_display)(unsafe.Pointer(wds))[:len(d):len(d)]
+	for i, display := range d {
+		wd := makeCWaylandDisplay(display)
+		slice[i] = wd
+	}
+	return wds
+}
+
+// TestingInitWayland initializes the Wayland display for testing purposes.
+func TestingInitWayland(t *testing.T, cwd []cWaylandDisplay, memoryErr bool) {
+	t.Helper()
+
+	wds := makeCwaylandDisplays(cwd)
+	C.set_displays(wds, C.int(len(cwd)))
+	C.set_memory_error(C.bool(memoryErr))
+}


Reviews:
user: login: cnihelton
body: Great work. Sorry for taking that long to reply, asynchronous protocols like wayland may have tons of corner cases. I needed to re-read some parts of its documentation to make sure I could properly follow what you're doing here.
It looks to me that you got the pointer arithmetic correct, but I'd like to call out your attention the added runtime dependency not acknowledged in `debian/control` and a corner case that we might end up falling into regarding geometry and mode callback call times.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:26:58Z
user: login: hk21702
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T17:17:09Z
user: login: hk21702
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T18:19:10Z
user: login: hk21702
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T18:25:49Z
user: login: hk21702
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T18:30:47Z
user: login: hk21702
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T18:35:00Z
user: login: cnihelton
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T15:28:38Z
user: login: cnihelton
body: Great work!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T17:48:26Z

Review comments:
diff_hunk: @@ -7,6 +7,7 @@ Build-Depends: debhelper-compat (= 13),
                dh-golang,
                golang-go (>= 2:1.24~) | golang-1.24-go,
                dctrl-tools,
+               libwayland-dev,
path: debian/control
user: login: cnihelton
body: We need to depend on libwayland-client0 at runtime. If we don't acknowledge that we would fail at runtime on systems where wayland stuff is not installed. I think we're fine adding the runtime dependency, though, because all recent versions of Ubuntu desktop have that library anyway (unless a very savvy user removes it for some special reason,  but then having the dependency properly acknowledged by apt/dpkg allows insights to be removed together, avoiding the risk of failure to launch).

To be honest I had to check this to be sure. Here are the details if you want to verify on your machine as well (or just for information):

WIth CGO_ENABLED=1, which is the Go default (and dh_golang's as well), we get this:

```console
cn@a515 ~/Dev/1_Work/ubuntu-insights  wayland  
$ go build ./cmd/insights/
go: downloading github.com/spf13/viper v1.20.1
go: downloading github.com/BurntSushi/toml v1.5.0
go: downloading golang.org/x/sys v0.29.0
go: downloading github.com/go-viper/mapstructure/v2 v2.2.1
go: downloading github.com/sagikazarmark/locafero v0.7.0
go: downloading github.com/sourcegraph/conc v0.3.0

cn@a515 ~/Dev/1_Work/ubuntu-insights  wayland 
$ ldd ./insights   
	linux-vdso.so.1 (0x0000786053302000)
	libwayland-client.so.0 => /lib/x86_64-linux-gnu/libwayland-client.so.0 (0x00007860532cb000) # Here's the runtime dependency 🥲 
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0000786053000000)
	libffi.so.8 => /lib/x86_64-linux-gnu/libffi.so.8 (0x00007860532be000)
	/lib64/ld-linux-x86-64.so.2 (0x0000786053304000)
```
(The same thing with `dpkg-buildpackage -us -uc`)

```console
 dpkg-genbuildinfo -O../ubuntu-insights_0.0.1~ppa3_amd64.buildinfo
 dpkg-genchanges -O../ubuntu-insights_0.0.1~ppa3_amd64.changes
dpkg-genchanges: info: including full source code in upload
 dpkg-source --after-build .
dpkg-buildpackage: info: full upload; Debian-native package (full source is included)
cn@a515 ~/Dev/1_Work 
$ dpkg-deb -x ./ubuntu-insights_0.0.1\~ppa3_amd64.deb ./deb
cn@a515 ~/Dev/1_Work/deb/usr/bin
$ ldd ./ubuntu-insights 
	linux-vdso.so.1 (0x00007fb460f1b000)
	libwayland-client.so.0 => /lib/x86_64-linux-gnu/libwayland-client.so.0 (0x00007fb460ee4000) # 🥲 
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb460200000)
	libffi.so.8 => /lib/x86_64-linux-gnu/libffi.so.8 (0x00007fb460ed7000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fb460f1d000)
```
With `CGO_ENABLED=0` we fail to build:

```console
PKG_CONFIG='pkg-config'
	cd _build && go generate -v github.com/ubuntu/ubuntu-insights/cmd/insights
src/github.com/ubuntu/ubuntu-insights/cmd/insights/main.go
# github.com/ubuntu/ubuntu-insights/internal/collector/sysinfo/hardware
../../internal/collector/sysinfo/hardware/hardware.go:74:11: undefined: platformOptions
../../internal/collector/sysinfo/hardware/hardware.go:84:11: undefined: platformOptions
../../internal/collector/sysinfo/hardware/hardware.go:93:18: undefined: defaultPlatformOptions
../../internal/collector/sysinfo/hardware/hardware.go:111:24: h.collectProduct undefined (type Collector has no field or method collectProduct)
../../internal/collector/sysinfo/hardware/hardware.go:117:20: h.collectCPU undefined (type Collector has no field or method collectCPU)
../../internal/collector/sysinfo/hardware/hardware.go:125:21: h.collectGPUs undefined (type Collector has no field or method collectGPUs)
../../internal/collector/sysinfo/hardware/hardware.go:131:20: h.collectMemory undefined (type Collector has no field or method collectMemory)
../../internal/collector/sysinfo/hardware/hardware.go:137:21: h.collectDisks undefined (type Collector has no field or method collectDisks)
../../internal/collector/sysinfo/hardware/hardware.go:143:24: h.collectScreens undefined (type Collector has no field or method collectScreens)
src/github.com/ubuntu/ubuntu-insights/cmd/insights/main.go:12: running "go": exit status 1
dh_auto_build: error: cd _build && go generate -v github.com/ubuntu/ubuntu-insights/cmd/insights returned exit code 1
make: *** [debian/rules:19: binary] Error 1
dpkg-buildpackage: error: debian/rules binary subprocess returned exit status 2
```

If we try to force it to link libwayland-client statically, it won't find its static version at link time:

```console
cn@a515 ~/Dev/1_Work/ubuntu-insights  wayland ●   
$ go build --ldflags='-extldflags=-static' ./cmd/insights               
# github.com/ubuntu/ubuntu-insights/cmd/insights
/usr/lib/go-1.24/pkg/tool/linux_amd64/link: running gcc failed: exit status 1
/usr/bin/gcc -m64 -Wl,--build-id=0x591168ede97f4f2d81a9811cbf0b9ae31c19ba7d -o $WORK/b001/exe/a.out -Wl,--export-dynamic-symbol=_cgo_panic -Wl,--export-dynamic-symbol=_cgo_topofstack -Wl,--export-dynamic-symbol=crosscall2 -Wl,--compress-debug-sections=zlib /tmp/go-link-3051847298/go.o /tmp/go-link-3051847298/000000.o /tmp/go-link-3051847298/000001.o /tmp/go-link-3051847298/000002.o /tmp/go-link-3051847298/000003.o /tmp/go-link-3051847298/000004.o /tmp/go-link-3051847298/000005.o /tmp/go-link-3051847298/000006.o /tmp/go-link-3051847298/000007.o /tmp/go-link-3051847298/000008.o /tmp/go-link-3051847298/000009.o /tmp/go-link-3051847298/000010.o /tmp/go-link-3051847298/000011.o /tmp/go-link-3051847298/000012.o /tmp/go-link-3051847298/000013.o /tmp/go-link-3051847298/000014.o /tmp/go-link-3051847298/000015.o /tmp/go-link-3051847298/000016.o /tmp/go-link-3051847298/000017.o /tmp/go-link-3051847298/000018.o /tmp/go-link-3051847298/000019.o /tmp/go-link-3051847298/000020.o /tmp/go-link-3051847298/000021.o /tmp/go-link-3051847298/000022.o /tmp/go-link-3051847298/000023.o -O2 -g -lresolv -O2 -g -lwayland-client -lwayland-client -O2 -g -lpthread -no-pie -static
/usr/bin/ld: cannot find -lwayland-client: No such file or directory
/usr/bin/ld: have you installed the static version of the wayland-client library ?
/usr/bin/ld: cannot find -lwayland-client: No such file or directory
/usr/bin/ld: have you installed the static version of the wayland-client library ?
/usr/bin/ld: /tmp/go-link-3051847298/000004.o: in function `_cgo_77133bf98b3a_C2func_getaddrinfo':
/tmp/go-build/cgo-gcc-prolog:60:(.text+0x37): warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
collect2: error: ld returned 1 exit status
```

My system has both libraries installed, but no static version of libwayland-client0
```console
cn@a515 ~/Dev/1_Work/ubuntu-insights  wayland ●  
$ apt policy libwayland-dev
libwayland-dev:
  Installed: 1.23.1-3
  Candidate: 1.23.1-3
  Version table:
 *** 1.23.1-3 500
        500 http://ubuntu-archive.locaweb.com.br/ubuntu plucky/main amd64 Packages
        100 /var/lib/dpkg/status
cn@a515 ~/Dev/1_Work/ubuntu-insights  wayland ●
$ apt policy libwayland-client0 
libwayland-client0:
  Installed: 1.23.1-3
  Candidate: 1.23.1-3
  Version table:
 *** 1.23.1-3 500
        500 http://ubuntu-archive.locaweb.com.br/ubuntu plucky/main amd64 Packages
        100 /var/lib/dpkg/status
```

It doesn't look like we produce statically linked libraries from the wayland source package: https://launchpad.net/ubuntu/plucky/+source/wayland
![image](https://github.com/user-attachments/assets/e2aa5bc2-4823-4307-af5c-30848a054fbb)

created_at: 2025-04-11T19:04:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,106 @@
+#include "wayland_displays_linux.h"
+
+static struct wayland_display **displays = NULL;
+static size_t capacity = 4;
+static size_t output_count = 0;
+static bool memory_error = false;
+
+static struct wl_display *display;
+static struct wl_registry *registry;
+
+void handle_geometry(void *data, struct wl_output *output, int32_t x, int32_t y,
+                     int32_t physical_width, int32_t physical_height,
+                     int32_t subpixel, const char *make, const char *model,
+                     int32_t transform) {
+  if (output_count >= capacity) {
+    capacity *= 2;
+    displays = realloc(displays, capacity * sizeof(struct wayland_display *));
+    if (!displays) {
+      memory_error = true;
+      return;
+    }
+  }
+  struct wayland_display *info = malloc(sizeof(struct wayland_display));
+  if (!info) {
+    memory_error = true;
+    return;
+  }
+  info->phys_width = physical_width;
+  info->phys_height = physical_height;
+
+  displays[output_count++] = info;
+}
+
+void handle_mode(void *data, struct wl_output *output, uint32_t flags,
+                 int32_t width, int32_t height, int32_t refresh) {
+  if (flags & WL_OUTPUT_MODE_CURRENT) {
+    struct wayland_display *info = displays[output_count - 1];
+    info->width = width;
+    info->height = height;
+    info->refresh = refresh;
+  }
+}
+
+static const struct wl_output_listener output_listener = {
+    .geometry = handle_geometry,
+    .mode = handle_mode,
+    .done = NULL,
+    .scale = NULL,
+};
+
+void global_handler(void *data, struct wl_registry *registry, uint32_t name,
+                    const char *interface, uint32_t version) {
+  if (strcmp(interface, "wl_output") == 0) {
+    struct wl_output *output =
+        wl_registry_bind(registry, name, &wl_output_interface, 1);
+    wl_output_add_listener(output, &output_listener, NULL);
+  }
+}
+
+void global_remove(void *data, struct wl_registry *registry, uint32_t name) {}
+
+static const struct wl_registry_listener registry_listener = {
+    .global = global_handler,
+    .global_remove = global_remove,
+};
+
+int init_wayland() {
+  displays = malloc(capacity * sizeof(struct wayland_display *));
+  if (!displays) {
+    memory_error = true;
+    return -1;
+  }
+
+  display = wl_display_connect(NULL);
+  if (!display) {
+    cleanup();
+    return -1;
+  }
+
+  registry = wl_display_get_registry(display);
+  wl_registry_add_listener(registry, &registry_listener, NULL);
+  // Call twice to ensure we get all the events
+  wl_display_roundtrip(display);
+  wl_display_roundtrip(display);
+  return 0;
+}
+
+void cleanup() {
+  if (display) {
+    wl_display_disconnect(display);
+    display = NULL;
+  }
+  for (size_t i = 0; i < output_count; i++) {
+    free(displays[i]);
+  }
+  free(displays);
+  displays = NULL;
+  output_count = 0;
+  capacity = 4;
+  memory_error = false;
+}
+
+struct wayland_display **get_displays() { return displays; }
+
+int get_output_count() { return output_count; }
+bool had_memory_error() { return memory_error; }
path: internal/collector/sysinfo/hardware/wayland_displays_linux.c
user: login: cnihelton
body: nitpick: do you mind adding a terminal new line? This red circle is so annoying... :)
created_at: 2025-04-12T18:49:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,33 @@
+#ifndef WAYLAND_DISPLAYS_H
+#define WAYLAND_DISPLAYS_H
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <wayland-client.h>
path: internal/collector/sysinfo/hardware/wayland_displays_linux.h
user: login: cnihelton
body: It doesn't seem like you need any of those headers included here, as none of their declarations appear in this header file. OTOH, you'll need stdint.h for the fixed width integer types. If they are dependencies of your implementation only (the .c file), then we can #include those headers there.
created_at: 2025-04-12T18:55:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,33 @@
+#ifndef WAYLAND_DISPLAYS_H
+#define WAYLAND_DISPLAYS_H
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <wayland-client.h>
+
+// Structure representing the wayland display information
+struct wayland_display {
+  int32_t width;
+  int32_t height;
+  int32_t refresh;
+  int32_t phys_width;
+  int32_t phys_height;
+};
+// Initalize Wayland display information.
+int init_wayland();
+
+// Cleanup Wayland display information.
+void cleanup();
+
+// Get the Wayland display information.
+struct wayland_display **get_displays();
+
+// Get the number of Wayland displays.
+int get_output_count();
+
+// Checks if there was a memory error.
+bool had_memory_error();
+
+#endif // WAYLAND_DISPLAYS_H
path: internal/collector/sysinfo/hardware/wayland_displays_linux.h
user: login: cnihelton
body: nitpick: do you mind adding a terminal new line in the C files? This red circle is so annoying :)
created_at: 2025-04-14T14:54:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,58 @@
+package hardware
+
+/*
+#cgo LDFLAGS: -lwayland-client
+#include "wayland_displays_linux.h"
+#include "wayland_displays_linux_test.h"
+*/
+import "C"
+
+import (
+	"testing"
+	"unsafe"
+)
+
+type cWaylandDisplay struct {
+	Width      int32
+	Height     int32
+	Refresh    int32
+	PhysWidth  int32
+	PhysHeight int32
+}
+
+// makeCWaylandDisplay converts a Go cWaylandDisplay struct to a C struct.
+func makeCWaylandDisplay(d cWaylandDisplay) (wd *C.struct_wayland_display) {
+	// Cleanup is done in C.
+	wd = (*C.struct_wayland_display)(C.malloc(C.size_t(C.sizeof_struct_wayland_display)))
+	wd.width = C.int(d.Width)
+	wd.height = C.int(d.Height)
+	wd.refresh = C.int(d.Refresh)
+	wd.phys_width = C.int(d.PhysWidth)
+	wd.phys_height = C.int(d.PhysHeight)
+	return wd
+}
+
+func makeCwaylandDisplays(d []cWaylandDisplay) (wds **C.struct_wayland_display) {
+	if len(d) == 0 {
+		return nil
+	}
+
+	// Cleanup is done in C.
+	wds = (**C.struct_wayland_display)(C.malloc(C.size_t(len(d)) * C.size_t(C.sizeof_struct_wayland_display)))
+
+	slice := (*[1 << 30]*C.struct_wayland_display)(unsafe.Pointer(wds))[:len(d):len(d)]
path: internal/collector/sysinfo/hardware/waylandtest_linux.go
user: login: cnihelton
body: The slice-trimming trick is a bit cryptic. Do you mind adding a short comment explaining that while we start using a huge number for the slice size (2^30) we end up trimming it to the actual length of  `d`?
created_at: 2025-04-14T15:07:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,10 @@
+#ifndef WAYLAND_DISPLAYS_TEST_H
+#define WAYLAND_DISPLAYS_TEST_H
+
path: internal/collector/sysinfo/hardware/wayland_displays_linux_test.h
user: login: cnihelton
body: To be pedantic it's better to `#include <stdbool.h>` to avoid assuming the order in which this header would be included later on.
created_at: 2025-04-14T15:09:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,160 @@
+#include "wayland_displays_linux.h"
+#include "wayland_displays_linux_test.h"
+
+static bool memory_error = false;
+
+static struct wayland_display **displays = NULL;
+static size_t displays_count = 0;
+static size_t displays_capacity = 4;
+
+static struct wl_output **outputs = NULL;
+static size_t outputs_count = 0;
+static size_t outputs_capacity = 4;
+
+static struct wl_display *display;
+static struct wl_registry *registry;
+
+void handle_geometry(void *data, struct wl_output *output, int32_t x, int32_t y,
+                     int32_t physical_width, int32_t physical_height,
+                     int32_t subpixel, const char *make, const char *model,
+                     int32_t transform) {
+  if (displays_count >= displays_capacity) {
+    displays_capacity *= 2;
+    struct wayland_display **new_displays =
+        realloc(displays, displays_capacity * sizeof(struct wayland_display *));
+    if (!new_displays) {
+      memory_error = true;
+      return;
+    }
+    displays = new_displays;
+  }
+  struct wayland_display *info = malloc(sizeof(struct wayland_display));
+  if (!info) {
+    memory_error = true;
+    return;
+  }
+  info->phys_width = physical_width;
+  info->phys_height = physical_height;
+
+  displays[displays_count++] = info;
+}
+
+void handle_mode(void *data, struct wl_output *output, uint32_t flags,
+                 int32_t width, int32_t height, int32_t refresh) {
+  if (flags & WL_OUTPUT_MODE_CURRENT) {
+    struct wayland_display *info = displays[displays_count - 1];
+    info->width = width;
+    info->height = height;
+    info->refresh = refresh;
+  }
+}
+
+static const struct wl_output_listener output_listener = {
+    .geometry = handle_geometry,
+    .mode = handle_mode,
+    .done = NULL,
+    .scale = NULL,
+};
+
+void global_handler(void *data, struct wl_registry *registry, uint32_t name,
+                    const char *interface, uint32_t version) {
+  if (strcmp(interface, "wl_output") == 0) {
+    struct wl_output *output =
+        wl_registry_bind(registry, name, &wl_output_interface, 1);
+
+    if (outputs_count >= outputs_capacity) {
+      outputs_capacity *= 2;
+      struct wl_output **new_outputs =
+          realloc(outputs, outputs_capacity * sizeof(struct wl_output *));
+      if (!new_outputs) {
+        memory_error = true;
+        return;
+      }
+      outputs = new_outputs;
+    }
+    outputs[outputs_count++] = output;
+
+    wl_output_add_listener(output, &output_listener, NULL);
+  }
+}
+
+void global_remove(void *data, struct wl_registry *registry, uint32_t name) {}
+
+static const struct wl_registry_listener registry_listener = {
+    .global = global_handler,
+    .global_remove = global_remove,
+};
+
+int init_wayland() {
+  outputs = malloc(outputs_capacity * sizeof(struct wl_output *));
+  if (!outputs) {
+    memory_error = true;
+    return -1;
+  }
+
+  displays = malloc(displays_capacity * sizeof(struct wayland_display *));
+  if (!displays) {
+    free(outputs);
+    outputs = NULL;
+    memory_error = true;
+    return -1;
+  }
+
+  display = wl_display_connect(NULL);
+  if (!display) {
+    cleanup();
+    return -1;
+  }
+
+  registry = wl_display_get_registry(display);
+  wl_registry_add_listener(registry, &registry_listener, NULL);
+  // Call twice to ensure we get all the events
+  wl_display_roundtrip(display);
+  wl_display_roundtrip(display);
+  return 0;
path: internal/collector/sysinfo/hardware/wayland_displays_linux.c
user: login: cnihelton
body: Documentation advises that `wl_display_roundtrip` blocks until all pending requests are processed by the Wayland server. Did you observe an actual need to call it twice? this also complicates a bit error handling, we're not checking for `-1` being returned in any of those calls, but that could be the case. How should we treat an error being returned by one of them in that case? At least I see that the happy path with the second call completing with no event being dispatched by the server, it will return 0, instead of `-1`, that is no events is not an error.
created_at: 2025-04-14T15:19:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,160 @@
+#include "wayland_displays_linux.h"
path: internal/collector/sysinfo/hardware/wayland_displays_linux.c
user: login: cnihelton
body: Wayland is a quite involved protocol, we shouldn't assume we'll remember most of the callback sequencing herein presented, so I'd advise to comment thoroughly the callback functions, explaining why they exist and when they are supposed to be called. If changes happen later, it will be much easier to grasp what were the preconditions we assumed that may no longer hold true.
created_at: 2025-04-14T15:33:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,160 @@
+#include "wayland_displays_linux.h"
+#include "wayland_displays_linux_test.h"
+
+static bool memory_error = false;
+
+static struct wayland_display **displays = NULL;
+static size_t displays_count = 0;
+static size_t displays_capacity = 4;
+
+static struct wl_output **outputs = NULL;
+static size_t outputs_count = 0;
+static size_t outputs_capacity = 4;
+
+static struct wl_display *display;
+static struct wl_registry *registry;
+
+void handle_geometry(void *data, struct wl_output *output, int32_t x, int32_t y,
+                     int32_t physical_width, int32_t physical_height,
+                     int32_t subpixel, const char *make, const char *model,
+                     int32_t transform) {
+  if (displays_count >= displays_capacity) {
+    displays_capacity *= 2;
+    struct wayland_display **new_displays =
+        realloc(displays, displays_capacity * sizeof(struct wayland_display *));
+    if (!new_displays) {
+      memory_error = true;
+      return;
+    }
+    displays = new_displays;
+  }
+  struct wayland_display *info = malloc(sizeof(struct wayland_display));
+  if (!info) {
+    memory_error = true;
+    return;
+  }
+  info->phys_width = physical_width;
+  info->phys_height = physical_height;
+
+  displays[displays_count++] = info;
+}
path: internal/collector/sysinfo/hardware/wayland_displays_linux.c
user: login: cnihelton
body: This pattern is mostly fine because this binary is short-lived, so it won't significantly contribute to system memory fragmentation, thus I'll point it out to make sure you considered whether the current implementation is actually simpler and easier to get right than _making the `displays` pointer be a contiguous memory region where you dump all `wayland_display` structs one after the other instead of performing the micro allocations we're doing now for each invocation of this callback_.
created_at: 2025-04-14T16:06:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,160 @@
+#include "wayland_displays_linux.h"
+#include "wayland_displays_linux_test.h"
+
+static bool memory_error = false;
+
+static struct wayland_display **displays = NULL;
+static size_t displays_count = 0;
+static size_t displays_capacity = 4;
+
+static struct wl_output **outputs = NULL;
+static size_t outputs_count = 0;
+static size_t outputs_capacity = 4;
+
+static struct wl_display *display;
+static struct wl_registry *registry;
+
+void handle_geometry(void *data, struct wl_output *output, int32_t x, int32_t y,
+                     int32_t physical_width, int32_t physical_height,
+                     int32_t subpixel, const char *make, const char *model,
+                     int32_t transform) {
+  if (displays_count >= displays_capacity) {
+    displays_capacity *= 2;
+    struct wayland_display **new_displays =
+        realloc(displays, displays_capacity * sizeof(struct wayland_display *));
+    if (!new_displays) {
+      memory_error = true;
+      return;
+    }
+    displays = new_displays;
+  }
+  struct wayland_display *info = malloc(sizeof(struct wayland_display));
+  if (!info) {
+    memory_error = true;
+    return;
+  }
+  info->phys_width = physical_width;
+  info->phys_height = physical_height;
+
+  displays[displays_count++] = info;
+}
path: internal/collector/sysinfo/hardware/wayland_displays_linux.c
user: login: cnihelton
body: I just noticed that for the case of `outputs`, the we don't allocate, anything but the array of pointers, thus doing the way you did treats both consistently, which can be seen as a source of simplification indeed. Is that the main reason?
created_at: 2025-04-14T16:20:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,160 @@
+#include "wayland_displays_linux.h"
+#include "wayland_displays_linux_test.h"
+
+static bool memory_error = false;
+
+static struct wayland_display **displays = NULL;
+static size_t displays_count = 0;
+static size_t displays_capacity = 4;
+
+static struct wl_output **outputs = NULL;
+static size_t outputs_count = 0;
+static size_t outputs_capacity = 4;
+
+static struct wl_display *display;
+static struct wl_registry *registry;
+
+void handle_geometry(void *data, struct wl_output *output, int32_t x, int32_t y,
+                     int32_t physical_width, int32_t physical_height,
+                     int32_t subpixel, const char *make, const char *model,
+                     int32_t transform) {
+  if (displays_count >= displays_capacity) {
+    displays_capacity *= 2;
+    struct wayland_display **new_displays =
+        realloc(displays, displays_capacity * sizeof(struct wayland_display *));
+    if (!new_displays) {
+      memory_error = true;
+      return;
+    }
+    displays = new_displays;
+  }
+  struct wayland_display *info = malloc(sizeof(struct wayland_display));
+  if (!info) {
+    memory_error = true;
+    return;
+  }
+  info->phys_width = physical_width;
+  info->phys_height = physical_height;
+
+  displays[displays_count++] = info;
+}
+
+void handle_mode(void *data, struct wl_output *output, uint32_t flags,
+                 int32_t width, int32_t height, int32_t refresh) {
+  if (flags & WL_OUTPUT_MODE_CURRENT) {
+    struct wayland_display *info = displays[displays_count - 1];
path: internal/collector/sysinfo/hardware/wayland_displays_linux.c
user: login: cnihelton
body: While it most often works (because we have very few displays connected I suppose), I don't think we are guaranteed that the mode we're receiving corresponds to the last display in the array, as the protocol is asynchronous, the server is free to deliver events ordered by its heart content. This way we'd end up swapping physical geometry with the display mode.

Isn't to prevent that condition the reason why we have the `void *data` parameter? (which we should have by the way bound to the callback when calling `wl_output_add_listener` instead of passing NULL)
created_at: 2025-04-14T16:31:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,160 @@
+#include "wayland_displays_linux.h"
+#include "wayland_displays_linux_test.h"
+
+static bool memory_error = false;
+
+static struct wayland_display **displays = NULL;
+static size_t displays_count = 0;
+static size_t displays_capacity = 4;
+
+static struct wl_output **outputs = NULL;
+static size_t outputs_count = 0;
+static size_t outputs_capacity = 4;
+
+static struct wl_display *display;
+static struct wl_registry *registry;
+
+void handle_geometry(void *data, struct wl_output *output, int32_t x, int32_t y,
+                     int32_t physical_width, int32_t physical_height,
+                     int32_t subpixel, const char *make, const char *model,
+                     int32_t transform) {
+  if (displays_count >= displays_capacity) {
+    displays_capacity *= 2;
+    struct wayland_display **new_displays =
+        realloc(displays, displays_capacity * sizeof(struct wayland_display *));
+    if (!new_displays) {
+      memory_error = true;
+      return;
+    }
+    displays = new_displays;
+  }
+  struct wayland_display *info = malloc(sizeof(struct wayland_display));
+  if (!info) {
+    memory_error = true;
+    return;
+  }
+  info->phys_width = physical_width;
+  info->phys_height = physical_height;
+
+  displays[displays_count++] = info;
+}
+
+void handle_mode(void *data, struct wl_output *output, uint32_t flags,
+                 int32_t width, int32_t height, int32_t refresh) {
+  if (flags & WL_OUTPUT_MODE_CURRENT) {
+    struct wayland_display *info = displays[displays_count - 1];
path: internal/collector/sysinfo/hardware/wayland_displays_linux.c
user: login: cnihelton
body: It seems to me that the `info` variable should be allocated and passed there, and retrieved here via this `void *data` parameter, this way we guarantee that we don't swap physical geometry with mode data.
created_at: 2025-04-14T16:53:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,160 @@
+#include "wayland_displays_linux.h"
+#include "wayland_displays_linux_test.h"
+
+static bool memory_error = false;
+
+static struct wayland_display **displays = NULL;
+static size_t displays_count = 0;
+static size_t displays_capacity = 4;
+
+static struct wl_output **outputs = NULL;
+static size_t outputs_count = 0;
+static size_t outputs_capacity = 4;
+
+static struct wl_display *display;
+static struct wl_registry *registry;
+
+void handle_geometry(void *data, struct wl_output *output, int32_t x, int32_t y,
+                     int32_t physical_width, int32_t physical_height,
+                     int32_t subpixel, const char *make, const char *model,
+                     int32_t transform) {
+  if (displays_count >= displays_capacity) {
+    displays_capacity *= 2;
+    struct wayland_display **new_displays =
+        realloc(displays, displays_capacity * sizeof(struct wayland_display *));
+    if (!new_displays) {
+      memory_error = true;
+      return;
+    }
+    displays = new_displays;
+  }
+  struct wayland_display *info = malloc(sizeof(struct wayland_display));
+  if (!info) {
+    memory_error = true;
+    return;
+  }
+  info->phys_width = physical_width;
+  info->phys_height = physical_height;
+
+  displays[displays_count++] = info;
+}
+
+void handle_mode(void *data, struct wl_output *output, uint32_t flags,
+                 int32_t width, int32_t height, int32_t refresh) {
+  if (flags & WL_OUTPUT_MODE_CURRENT) {
+    struct wayland_display *info = displays[displays_count - 1];
+    info->width = width;
+    info->height = height;
+    info->refresh = refresh;
+  }
+}
+
+static const struct wl_output_listener output_listener = {
+    .geometry = handle_geometry,
+    .mode = handle_mode,
+    .done = NULL,
+    .scale = NULL,
+};
+
+void global_handler(void *data, struct wl_registry *registry, uint32_t name,
+                    const char *interface, uint32_t version) {
+  if (strcmp(interface, "wl_output") == 0) {
+    struct wl_output *output =
+        wl_registry_bind(registry, name, &wl_output_interface, 1);
+
+    if (outputs_count >= outputs_capacity) {
+      outputs_capacity *= 2;
+      struct wl_output **new_outputs =
+          realloc(outputs, outputs_capacity * sizeof(struct wl_output *));
+      if (!new_outputs) {
+        memory_error = true;
+        return;
+      }
+      outputs = new_outputs;
+    }
+    outputs[outputs_count++] = output;
+
+    wl_output_add_listener(output, &output_listener, NULL);
+  }
+}
+
path: internal/collector/sysinfo/hardware/wayland_displays_linux.c
user: login: cnihelton
body: My reasoning about how to prevent swapping geometry and mode of two different displays is that:
- When the global_handler callback is invoked for the "wl_output" interface we have a new output,
- For that output we register a listener to get geometry and mode
- So unless an error happen, at least once we'll receive both, thus we could allocate the display info object here and bound it to the event handler.
created_at: 2025-04-14T16:57:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,160 @@
+#include "wayland_displays_linux.h"
+#include "wayland_displays_linux_test.h"
+
+static bool memory_error = false;
+
+static struct wayland_display **displays = NULL;
+static size_t displays_count = 0;
+static size_t displays_capacity = 4;
+
+static struct wl_output **outputs = NULL;
+static size_t outputs_count = 0;
+static size_t outputs_capacity = 4;
+
+static struct wl_display *display;
+static struct wl_registry *registry;
+
+void handle_geometry(void *data, struct wl_output *output, int32_t x, int32_t y,
+                     int32_t physical_width, int32_t physical_height,
+                     int32_t subpixel, const char *make, const char *model,
+                     int32_t transform) {
+  if (displays_count >= displays_capacity) {
+    displays_capacity *= 2;
+    struct wayland_display **new_displays =
+        realloc(displays, displays_capacity * sizeof(struct wayland_display *));
+    if (!new_displays) {
+      memory_error = true;
+      return;
+    }
+    displays = new_displays;
+  }
+  struct wayland_display *info = malloc(sizeof(struct wayland_display));
+  if (!info) {
+    memory_error = true;
+    return;
+  }
+  info->phys_width = physical_width;
+  info->phys_height = physical_height;
+
+  displays[displays_count++] = info;
+}
+
+void handle_mode(void *data, struct wl_output *output, uint32_t flags,
+                 int32_t width, int32_t height, int32_t refresh) {
+  if (flags & WL_OUTPUT_MODE_CURRENT) {
+    struct wayland_display *info = displays[displays_count - 1];
+    info->width = width;
+    info->height = height;
+    info->refresh = refresh;
+  }
+}
+
+static const struct wl_output_listener output_listener = {
+    .geometry = handle_geometry,
+    .mode = handle_mode,
+    .done = NULL,
+    .scale = NULL,
+};
+
+void global_handler(void *data, struct wl_registry *registry, uint32_t name,
+                    const char *interface, uint32_t version) {
+  if (strcmp(interface, "wl_output") == 0) {
+    struct wl_output *output =
+        wl_registry_bind(registry, name, &wl_output_interface, 1);
+
+    if (outputs_count >= outputs_capacity) {
+      outputs_capacity *= 2;
+      struct wl_output **new_outputs =
+          realloc(outputs, outputs_capacity * sizeof(struct wl_output *));
+      if (!new_outputs) {
+        memory_error = true;
+        return;
+      }
+      outputs = new_outputs;
+    }
+    outputs[outputs_count++] = output;
+
+    wl_output_add_listener(output, &output_listener, NULL);
+  }
+}
+
path: internal/collector/sysinfo/hardware/wayland_displays_linux.c
user: login: cnihelton
body: There may exist some fancier setups where a single `wl_output` is mapped to multiple displays or a large display is broken down in more than one `wl_output` (digital signage equipment maybe??) so in theory this relationship can get quite complex. For now at least I think we can be fine with assuming that we'll have one output per display and vice versa, but we should capture that explicitly as comments at least.
created_at: 2025-04-14T17:01:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -424,6 +446,43 @@ func (h Collector) collectScreens(pi platform.Info) (info []screen, err error) {
 			RefreshRate: v[2],
 		})
 	}
-
 	return info, nil
 }
+
+type realWaylandProvider struct{}
+
+func (*realWaylandProvider) InitWayland() int {
+	return int(C.init_wayland())
path: internal/collector/sysinfo/hardware/hardware_linux.go
user: login: cnihelton
body: I think we should lock/unlock the waylandMutex here or maybe even make this a sync.Once function.
created_at: 2025-04-14T17:06:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -424,6 +446,43 @@ func (h Collector) collectScreens(pi platform.Info) (info []screen, err error) {
 			RefreshRate: v[2],
 		})
 	}
-
 	return info, nil
 }
+
+type realWaylandProvider struct{}
+
+func (*realWaylandProvider) InitWayland() int {
+	return int(C.init_wayland())
+}
+
+func (h Collector) cScreensWayland() (screens []screen, err error) {
+	waylandMutex.Lock()
+	defer waylandMutex.Unlock()
+	if h.platform.wayland.InitWayland() != 0 {
+		return nil, fmt.Errorf("failed to connect to Wayland display")
+	}
+	defer C.cleanup()
+
+	if bool(C.had_memory_error()) {
+		h.log.Warn("Memory error while trying to get displays")
+		return nil, fmt.Errorf("failed to get displays")
+	}
+
+	count := int(C.get_output_count())
+	screens = make([]screen, count)
+	displays := C.get_displays()
+	for i := range count {
+		display := *(**C.struct_wayland_display)(unsafe.Pointer(uintptr(unsafe.Pointer(displays)) + uintptr(i)*unsafe.Sizeof((*C.struct_wayland_display)(nil))))
path: internal/collector/sysinfo/hardware/hardware_linux.go
user: login: cnihelton
body: This written in the middle of some Go code looks quite ugly... but I couldn't find a nicer way that wouldn't be too verbose for little benefit.
created_at: 2025-04-14T17:21:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -424,6 +446,43 @@ func (h Collector) collectScreens(pi platform.Info) (info []screen, err error) {
 			RefreshRate: v[2],
 		})
 	}
-
 	return info, nil
 }
+
+type realWaylandProvider struct{}
+
+func (*realWaylandProvider) InitWayland() int {
+	return int(C.init_wayland())
path: internal/collector/sysinfo/hardware/hardware_linux.go
user: login: hk21702
body: I think it is probably fine to make it `sync.Once`, but the locking and unlocking from I think would need to at the very least encompass the reading of displays and the cleanup like it is right now. But, I'm also not entirely sure if that is correct since I haven't actually managed to break things by not including the mutex, even in parallel tests, or with go test --race. I'm not entirely sure how cgo handles globals between threads or even goroutines.
created_at: 2025-04-15T17:17:09Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,160 @@
+#include "wayland_displays_linux.h"
+#include "wayland_displays_linux_test.h"
+
+static bool memory_error = false;
+
+static struct wayland_display **displays = NULL;
+static size_t displays_count = 0;
+static size_t displays_capacity = 4;
+
+static struct wl_output **outputs = NULL;
+static size_t outputs_count = 0;
+static size_t outputs_capacity = 4;
+
+static struct wl_display *display;
+static struct wl_registry *registry;
+
+void handle_geometry(void *data, struct wl_output *output, int32_t x, int32_t y,
+                     int32_t physical_width, int32_t physical_height,
+                     int32_t subpixel, const char *make, const char *model,
+                     int32_t transform) {
+  if (displays_count >= displays_capacity) {
+    displays_capacity *= 2;
+    struct wayland_display **new_displays =
+        realloc(displays, displays_capacity * sizeof(struct wayland_display *));
+    if (!new_displays) {
+      memory_error = true;
+      return;
+    }
+    displays = new_displays;
+  }
+  struct wayland_display *info = malloc(sizeof(struct wayland_display));
+  if (!info) {
+    memory_error = true;
+    return;
+  }
+  info->phys_width = physical_width;
+  info->phys_height = physical_height;
+
+  displays[displays_count++] = info;
+}
+
+void handle_mode(void *data, struct wl_output *output, uint32_t flags,
+                 int32_t width, int32_t height, int32_t refresh) {
+  if (flags & WL_OUTPUT_MODE_CURRENT) {
+    struct wayland_display *info = displays[displays_count - 1];
+    info->width = width;
+    info->height = height;
+    info->refresh = refresh;
+  }
+}
+
+static const struct wl_output_listener output_listener = {
+    .geometry = handle_geometry,
+    .mode = handle_mode,
+    .done = NULL,
+    .scale = NULL,
+};
+
+void global_handler(void *data, struct wl_registry *registry, uint32_t name,
+                    const char *interface, uint32_t version) {
+  if (strcmp(interface, "wl_output") == 0) {
+    struct wl_output *output =
+        wl_registry_bind(registry, name, &wl_output_interface, 1);
+
+    if (outputs_count >= outputs_capacity) {
+      outputs_capacity *= 2;
+      struct wl_output **new_outputs =
+          realloc(outputs, outputs_capacity * sizeof(struct wl_output *));
+      if (!new_outputs) {
+        memory_error = true;
+        return;
+      }
+      outputs = new_outputs;
+    }
+    outputs[outputs_count++] = output;
+
+    wl_output_add_listener(output, &output_listener, NULL);
+  }
+}
+
path: internal/collector/sysinfo/hardware/wayland_displays_linux.c
user: login: hk21702
body: That is a good point, I think screen splitting is actually not as rare as one may think, even on consumer hardware. But, I might not be able to test these cases. I think it can be done using both the display firmware itself, or through screen virtualization software maybe?
created_at: 2025-04-15T18:19:10Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,160 @@
+#include "wayland_displays_linux.h"
+#include "wayland_displays_linux_test.h"
+
+static bool memory_error = false;
+
+static struct wayland_display **displays = NULL;
+static size_t displays_count = 0;
+static size_t displays_capacity = 4;
+
+static struct wl_output **outputs = NULL;
+static size_t outputs_count = 0;
+static size_t outputs_capacity = 4;
+
+static struct wl_display *display;
+static struct wl_registry *registry;
+
+void handle_geometry(void *data, struct wl_output *output, int32_t x, int32_t y,
+                     int32_t physical_width, int32_t physical_height,
+                     int32_t subpixel, const char *make, const char *model,
+                     int32_t transform) {
+  if (displays_count >= displays_capacity) {
+    displays_capacity *= 2;
+    struct wayland_display **new_displays =
+        realloc(displays, displays_capacity * sizeof(struct wayland_display *));
+    if (!new_displays) {
+      memory_error = true;
+      return;
+    }
+    displays = new_displays;
+  }
+  struct wayland_display *info = malloc(sizeof(struct wayland_display));
+  if (!info) {
+    memory_error = true;
+    return;
+  }
+  info->phys_width = physical_width;
+  info->phys_height = physical_height;
+
+  displays[displays_count++] = info;
+}
path: internal/collector/sysinfo/hardware/wayland_displays_linux.c
user: login: hk21702
body: `wl_registry_bind` I think is doing some allocation behind the scenes, and itself is returning a pointer. It's also why `wl_output_destroy` is needed in the cleanup to prevent memory leaks.
created_at: 2025-04-15T18:25:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,160 @@
+#include "wayland_displays_linux.h"
+#include "wayland_displays_linux_test.h"
+
+static bool memory_error = false;
+
+static struct wayland_display **displays = NULL;
+static size_t displays_count = 0;
+static size_t displays_capacity = 4;
+
+static struct wl_output **outputs = NULL;
+static size_t outputs_count = 0;
+static size_t outputs_capacity = 4;
+
+static struct wl_display *display;
+static struct wl_registry *registry;
+
+void handle_geometry(void *data, struct wl_output *output, int32_t x, int32_t y,
+                     int32_t physical_width, int32_t physical_height,
+                     int32_t subpixel, const char *make, const char *model,
+                     int32_t transform) {
+  if (displays_count >= displays_capacity) {
+    displays_capacity *= 2;
+    struct wayland_display **new_displays =
+        realloc(displays, displays_capacity * sizeof(struct wayland_display *));
+    if (!new_displays) {
+      memory_error = true;
+      return;
+    }
+    displays = new_displays;
+  }
+  struct wayland_display *info = malloc(sizeof(struct wayland_display));
+  if (!info) {
+    memory_error = true;
+    return;
+  }
+  info->phys_width = physical_width;
+  info->phys_height = physical_height;
+
+  displays[displays_count++] = info;
+}
+
+void handle_mode(void *data, struct wl_output *output, uint32_t flags,
+                 int32_t width, int32_t height, int32_t refresh) {
+  if (flags & WL_OUTPUT_MODE_CURRENT) {
+    struct wayland_display *info = displays[displays_count - 1];
+    info->width = width;
+    info->height = height;
+    info->refresh = refresh;
+  }
+}
+
+static const struct wl_output_listener output_listener = {
+    .geometry = handle_geometry,
+    .mode = handle_mode,
+    .done = NULL,
+    .scale = NULL,
+};
+
+void global_handler(void *data, struct wl_registry *registry, uint32_t name,
+                    const char *interface, uint32_t version) {
+  if (strcmp(interface, "wl_output") == 0) {
+    struct wl_output *output =
+        wl_registry_bind(registry, name, &wl_output_interface, 1);
+
+    if (outputs_count >= outputs_capacity) {
+      outputs_capacity *= 2;
+      struct wl_output **new_outputs =
+          realloc(outputs, outputs_capacity * sizeof(struct wl_output *));
+      if (!new_outputs) {
+        memory_error = true;
+        return;
+      }
+      outputs = new_outputs;
+    }
+    outputs[outputs_count++] = output;
+
+    wl_output_add_listener(output, &output_listener, NULL);
+  }
+}
+
+void global_remove(void *data, struct wl_registry *registry, uint32_t name) {}
+
+static const struct wl_registry_listener registry_listener = {
+    .global = global_handler,
+    .global_remove = global_remove,
+};
+
+int init_wayland() {
+  outputs = malloc(outputs_capacity * sizeof(struct wl_output *));
+  if (!outputs) {
+    memory_error = true;
+    return -1;
+  }
+
+  displays = malloc(displays_capacity * sizeof(struct wayland_display *));
+  if (!displays) {
+    free(outputs);
+    outputs = NULL;
+    memory_error = true;
+    return -1;
+  }
+
+  display = wl_display_connect(NULL);
+  if (!display) {
+    cleanup();
+    return -1;
+  }
+
+  registry = wl_display_get_registry(display);
+  wl_registry_add_listener(registry, &registry_listener, NULL);
+  // Call twice to ensure we get all the events
+  wl_display_roundtrip(display);
+  wl_display_roundtrip(display);
+  return 0;
path: internal/collector/sysinfo/hardware/wayland_displays_linux.c
user: login: hk21702
body: It might only need one call. I forget where I read that it was best to do it twice, but I can't find it again. On my machine, it appears to be fine with a single call, but that's just a sample size of 1.
created_at: 2025-04-15T18:30:47Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,160 @@
+#include "wayland_displays_linux.h"
+#include "wayland_displays_linux_test.h"
+
+static bool memory_error = false;
+
+static struct wayland_display **displays = NULL;
+static size_t displays_count = 0;
+static size_t displays_capacity = 4;
+
+static struct wl_output **outputs = NULL;
+static size_t outputs_count = 0;
+static size_t outputs_capacity = 4;
+
+static struct wl_display *display;
+static struct wl_registry *registry;
+
+void handle_geometry(void *data, struct wl_output *output, int32_t x, int32_t y,
+                     int32_t physical_width, int32_t physical_height,
+                     int32_t subpixel, const char *make, const char *model,
+                     int32_t transform) {
+  if (displays_count >= displays_capacity) {
+    displays_capacity *= 2;
+    struct wayland_display **new_displays =
+        realloc(displays, displays_capacity * sizeof(struct wayland_display *));
+    if (!new_displays) {
+      memory_error = true;
+      return;
+    }
+    displays = new_displays;
+  }
+  struct wayland_display *info = malloc(sizeof(struct wayland_display));
+  if (!info) {
+    memory_error = true;
+    return;
+  }
+  info->phys_width = physical_width;
+  info->phys_height = physical_height;
+
+  displays[displays_count++] = info;
+}
+
+void handle_mode(void *data, struct wl_output *output, uint32_t flags,
+                 int32_t width, int32_t height, int32_t refresh) {
+  if (flags & WL_OUTPUT_MODE_CURRENT) {
+    struct wayland_display *info = displays[displays_count - 1];
+    info->width = width;
+    info->height = height;
+    info->refresh = refresh;
+  }
+}
+
+static const struct wl_output_listener output_listener = {
+    .geometry = handle_geometry,
+    .mode = handle_mode,
+    .done = NULL,
+    .scale = NULL,
+};
+
+void global_handler(void *data, struct wl_registry *registry, uint32_t name,
+                    const char *interface, uint32_t version) {
+  if (strcmp(interface, "wl_output") == 0) {
+    struct wl_output *output =
+        wl_registry_bind(registry, name, &wl_output_interface, 1);
+
+    if (outputs_count >= outputs_capacity) {
+      outputs_capacity *= 2;
+      struct wl_output **new_outputs =
+          realloc(outputs, outputs_capacity * sizeof(struct wl_output *));
+      if (!new_outputs) {
+        memory_error = true;
+        return;
+      }
+      outputs = new_outputs;
+    }
+    outputs[outputs_count++] = output;
+
+    wl_output_add_listener(output, &output_listener, NULL);
+  }
+}
+
+void global_remove(void *data, struct wl_registry *registry, uint32_t name) {}
+
+static const struct wl_registry_listener registry_listener = {
+    .global = global_handler,
+    .global_remove = global_remove,
+};
+
+int init_wayland() {
+  outputs = malloc(outputs_capacity * sizeof(struct wl_output *));
+  if (!outputs) {
+    memory_error = true;
+    return -1;
+  }
+
+  displays = malloc(displays_capacity * sizeof(struct wayland_display *));
+  if (!displays) {
+    free(outputs);
+    outputs = NULL;
+    memory_error = true;
+    return -1;
+  }
+
+  display = wl_display_connect(NULL);
+  if (!display) {
+    cleanup();
+    return -1;
+  }
+
+  registry = wl_display_get_registry(display);
+  wl_registry_add_listener(registry, &registry_listener, NULL);
+  // Call twice to ensure we get all the events
+  wl_display_roundtrip(display);
+  wl_display_roundtrip(display);
+  return 0;
path: internal/collector/sysinfo/hardware/wayland_displays_linux.c
user: login: hk21702
body: As for error handling, I'm not entirely sure what could trigger an error when calling `wl_display_roundtrip`, but to be safe we should probably indicate that something went wrong in the output, and to discard anything collected.
created_at: 2025-04-15T18:35:00Z
author_association: COLLABORATOR
diff_hunk: @@ -424,6 +446,43 @@ func (h Collector) collectScreens(pi platform.Info) (info []screen, err error) {
 			RefreshRate: v[2],
 		})
 	}
-
 	return info, nil
 }
+
+type realWaylandProvider struct{}
+
+func (*realWaylandProvider) InitWayland() int {
+	return int(C.init_wayland())
path: internal/collector/sysinfo/hardware/hardware_linux.go
user: login: cnihelton
body: The test cases don't exercise the entire C code since we have a mocking function that builds the entire data structure that would otherwise be built as the Wayland events come in, isn't it the case? I don't think CGo is doing any magic behind the scenes, and globals remain globals.
As long as we don't call this method twice nor interleaved with other methods, which is the case since the entire lifetime of the Wayland-related data structures are within a single method we seem fine.
created_at: 2025-04-16T15:28:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,170 @@
+#include "wayland_displays_linux.h"
+#include "wayland_displays_linux_test.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <wayland-client.h>
+
+static bool memory_error = false;
+
+static struct wayland_display **displays = NULL;
+static struct wl_output **outputs = NULL;
+
+static size_t count = 0;
+static size_t capacity = 4;
+
+static struct wl_display *display;
+static struct wl_registry *registry;
+
+// Function to handle geometry events from the Wayland output
+void handle_geometry(void *data, struct wl_output *output, int32_t x, int32_t y,
+                     int32_t physical_width, int32_t physical_height,
+                     int32_t subpixel, const char *make, const char *model,
+                     int32_t transform) {
+  struct wayland_display *info = (struct wayland_display *)data;
+  info->phys_width = physical_width;
+  info->phys_height = physical_height;
+}
+
+// Function to handle mode events from the Wayland output
+void handle_mode(void *data, struct wl_output *output, uint32_t flags,
+                 int32_t width, int32_t height, int32_t refresh) {
+  struct data_info *data_i = data;
+
+  if (flags & WL_OUTPUT_MODE_CURRENT) {
+    struct wayland_display *info = (struct wayland_display *)data;
+    info->width = width;
+    info->height = height;
+    info->refresh = refresh;
+  }
+}
+
+static const struct wl_output_listener output_listener = {
+    .geometry = handle_geometry,
+    .mode = handle_mode,
+    .done = NULL,
+    .scale = NULL,
+};
+
+// Function to handle global events from the Wayland registry
+void global_handler(void *data, struct wl_registry *registry, uint32_t name,
+                    const char *interface, uint32_t version) {
+  if (strcmp(interface, "wl_output") != 0) {
+    return;
+  }
+  // Check if we need to increase the capacity of the arrays
+  if (count >= capacity) {
+    capacity *= 2;
+    struct wl_output **new_outputs =
+        realloc(outputs, capacity * sizeof(struct wl_output *));
+
+    struct wayland_display **new_displays =
+        realloc(displays, capacity * sizeof(struct wayland_display *));
+    if (!new_outputs || !new_displays) {
+      free(new_outputs);
+      free(new_displays);
+      new_displays = NULL;
+      new_outputs = NULL;
+      memory_error = true;
+      return;
+    }
+    outputs = new_outputs;
+    displays = new_displays;
+  }
+
+  struct wayland_display *display = malloc(sizeof(struct wayland_display));
+  if (!display) {
+    memory_error = true;
+    return;
+  }
+
+  struct wl_output *output =
+      wl_registry_bind(registry, name, &wl_output_interface, 1);
+
+  displays[count] = display;
+  outputs[count] = output;
+  count++;
+
+  wl_output_add_listener(output, &output_listener, display);
+}
+
+void global_remove(void *data, struct wl_registry *registry, uint32_t name) {
+  free(data);
path: internal/collector/sysinfo/hardware/wayland_displays_linux.c
user: login: cnihelton
body: Calling `free(data);` here is essentially calling `free(NULL);` because this data is the value passed to `wl_registry_add_listener` at line 120:

```
wl_registry_add_listener(registry, &registry_listener, NULL);
```

Instead we can make this function empty ;)
created_at: 2025-04-16T17:47:31Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov-commenter
created_at: 2025-04-09T20:17:40Z
body: ## [Codecov](https://app.codecov.io/gh/ubuntu/ubuntu-insights/pull/85?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ubuntu) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 87.18%. Comparing base [(`71031a4`)](https://app.codecov.io/gh/ubuntu/ubuntu-insights/commit/71031a4bf701a33b192dea8126ee37dab1c5b4eb?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ubuntu) to head [(`1c576dd`)](https://app.codecov.io/gh/ubuntu/ubuntu-insights/commit/1c576ddd77561d21f45f9a1e78631577c63f03fe?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ubuntu).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main      #85      +/-   ##
==========================================
+ Coverage   86.80%   87.18%   +0.38%     
==========================================
  Files          34       35       +1     
  Lines        3023     3090      +67     
==========================================
+ Hits         2624     2694      +70     
+ Misses        280      278       -2     
+ Partials      119      118       -1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/ubuntu/ubuntu-insights/pull/85?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ubuntu).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ubuntu).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: cnihelton
created_at: 2025-04-14T17:27:47Z
body: Do not hesitate to ping me if you think I wasn't clear enough. Or maybe even schedule a pairing session ;)
author_association: CONTRIBUTOR