1#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: MIT
5#
6import os
7import socketserver
8import subprocess
9import time
10import urllib
11import pathlib
12
13from oeqa.core.decorator import OETestTag
14from oeqa.selftest.case import OESelftestTestCase
15from oeqa.utils.commands import bitbake, get_bb_var, runqemu
16
17
18class Debuginfod(OESelftestTestCase):
19
20    def wait_for_debuginfod(self, port):
21        """
22        debuginfod takes time to scan the packages and requesting too early may
23        result in a test failure if the right packages haven't been scanned yet.
24
25        Request the metrics endpoint periodically and wait for there to be no
26        busy scanning threads.
27
28        Returns if debuginfod is ready, raises an exception if not within the
29        timeout.
30        """
31
32        # Wait two minutes
33        countdown = 24
34        delay = 5
35        latest = None
36
37        while countdown:
38            self.logger.info("waiting...")
39            time.sleep(delay)
40
41            self.logger.info("polling server")
42            if self.debuginfod.poll():
43                self.logger.info("server dead")
44                self.debuginfod.communicate()
45                self.fail("debuginfod terminated unexpectedly")
46            self.logger.info("server alive")
47
48            try:
49                with urllib.request.urlopen("http://localhost:%d/metrics" % port, timeout=10) as f:
50                    for line in f.read().decode("ascii").splitlines():
51                        key, value = line.rsplit(" ", 1)
52                        if key == "thread_busy{role=\"scan\"}":
53                            latest = int(value)
54                            self.logger.info("Waiting for %d scan jobs to finish" % latest)
55                            if latest == 0:
56                                return
57            except urllib.error.URLError as e:
58                # TODO: how to catch just timeouts?
59                self.logger.error(e)
60
61            countdown -= 1
62
63        raise TimeoutError("Cannot connect debuginfod, still %d scan jobs running" % latest)
64
65    def start_debuginfod(self):
66        # We assume that the caller has already bitbake'd elfutils-native:do_addto_recipe_sysroot
67
68        # Save some useful paths for later
69        native_sysroot = pathlib.Path(get_bb_var("RECIPE_SYSROOT_NATIVE", "elfutils-native"))
70        native_bindir = native_sysroot / "usr" / "bin"
71        self.debuginfod = native_bindir / "debuginfod"
72        self.debuginfod_find = native_bindir / "debuginfod-find"
73
74        cmd = [
75            self.debuginfod,
76            "--verbose",
77            # In-memory database, this is a one-shot test
78            "--database=:memory:",
79            # Don't use all the host cores
80            "--concurrency=8",
81            "--connection-pool=8",
82            # Disable rescanning, this is a one-shot test
83            "--rescan-time=0",
84            "--groom-time=0",
85            get_bb_var("DEPLOY_DIR"),
86        ]
87
88        format = get_bb_var("PACKAGE_CLASSES").split()[0]
89        if format == "package_deb":
90            cmd.append("--scan-deb-dir")
91        elif format == "package_ipk":
92            cmd.append("--scan-deb-dir")
93        elif format == "package_rpm":
94            cmd.append("--scan-rpm-dir")
95        else:
96            self.fail("Unknown package class %s" % format)
97
98        # Find a free port. Racey but the window is small.
99        with socketserver.TCPServer(("localhost", 0), None) as s:
100            self.port = s.server_address[1]
101            cmd.append("--port=%d" % self.port)
102
103        self.logger.info(f"Starting server {cmd}")
104        self.debuginfod = subprocess.Popen(cmd, env={})
105        self.wait_for_debuginfod(self.port)
106
107
108    def test_debuginfod_native(self):
109        """
110        Test debuginfod outside of qemu, by building a package and looking up a
111        binary's debuginfo using elfutils-native.
112        """
113
114        self.write_config("""
115TMPDIR = "${TOPDIR}/tmp-debuginfod"
116DISTRO_FEATURES:append = " debuginfod"
117""")
118        bitbake("elfutils-native:do_addto_recipe_sysroot xz xz:do_package")
119
120        try:
121            self.start_debuginfod()
122
123            env = os.environ.copy()
124            env["DEBUGINFOD_URLS"] = "http://localhost:%d/" % self.port
125
126            pkgs = pathlib.Path(get_bb_var("PKGDEST", "xz"))
127            cmd = (self.debuginfod_find, "debuginfo", pkgs / "xz" / "usr" / "bin" / "xz.xz")
128            self.logger.info(f"Starting client {cmd}")
129            output = subprocess.check_output(cmd, env=env, text=True)
130            # This should be more comprehensive
131            self.assertIn("/.cache/debuginfod_client/", output)
132        finally:
133            self.debuginfod.kill()
134
135    @OETestTag("runqemu")
136    def test_debuginfod_qemu(self):
137        """
138        Test debuginfod-find inside a qemu, talking to a debuginfod on the host.
139        """
140
141        self.write_config("""
142TMPDIR = "${TOPDIR}/tmp-debuginfod"
143DISTRO_FEATURES:append = " debuginfod"
144CORE_IMAGE_EXTRA_INSTALL += "elfutils xz"
145        """)
146        bitbake("core-image-minimal elfutils-native:do_addto_recipe_sysroot")
147
148        try:
149            self.start_debuginfod()
150
151            with runqemu("core-image-minimal", runqemuparams="nographic") as qemu:
152                cmd = "DEBUGINFOD_URLS=http://%s:%d/ debuginfod-find debuginfo /usr/bin/xz" % (qemu.server_ip, self.port)
153                self.logger.info(f"Starting client {cmd}")
154                status, output = qemu.run_serial(cmd)
155                # This should be more comprehensive
156                self.assertIn("/.cache/debuginfod_client/", output)
157        finally:
158            self.debuginfod.kill()
159