1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this
3# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5from __future__ import division, absolute_import, print_function, unicode_literals
6
7"""
8This file contains functions used for telemetry.
9"""
10
11import distro
12import os
13import math
14import platform
15import sys
16
17import mozpack.path as mozpath
18from .base import BuildEnvironmentNotFoundException
19
20
21def cpu_brand_linux():
22    """
23    Read the CPU brand string out of /proc/cpuinfo on Linux.
24    """
25    with open("/proc/cpuinfo", "r") as f:
26        for line in f:
27            if line.startswith("model name"):
28                _, brand = line.split(": ", 1)
29                return brand.rstrip()
30    # not found?
31    return None
32
33
34def cpu_brand_windows():
35    """
36    Read the CPU brand string from the registry on Windows.
37    """
38    try:
39        import _winreg
40    except ImportError:
41        import winreg as _winreg
42
43    try:
44        h = _winreg.OpenKey(
45            _winreg.HKEY_LOCAL_MACHINE,
46            r"HARDWARE\DESCRIPTION\System\CentralProcessor\0",
47        )
48        (brand, ty) = _winreg.QueryValueEx(h, "ProcessorNameString")
49        if ty == _winreg.REG_SZ:
50            return brand
51    except WindowsError:
52        pass
53    return None
54
55
56def cpu_brand_mac():
57    """
58    Get the CPU brand string via sysctl on macos.
59    """
60    import ctypes
61    import ctypes.util
62
63    libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("c"))
64    # First, find the required buffer size.
65    bufsize = ctypes.c_size_t(0)
66    result = libc.sysctlbyname(
67        b"machdep.cpu.brand_string", None, ctypes.byref(bufsize), None, 0
68    )
69    if result != 0:
70        return None
71    bufsize.value += 1
72    buf = ctypes.create_string_buffer(bufsize.value)
73    # Now actually get the value.
74    result = libc.sysctlbyname(
75        b"machdep.cpu.brand_string", buf, ctypes.byref(bufsize), None, 0
76    )
77    if result != 0:
78        return None
79
80    return buf.value.decode()
81
82
83def get_cpu_brand():
84    """
85    Get the CPU brand string as returned by CPUID.
86    """
87    return {
88        "Linux": cpu_brand_linux,
89        "Windows": cpu_brand_windows,
90        "Darwin": cpu_brand_mac,
91    }.get(platform.system(), lambda: None)()
92
93
94def get_os_name():
95    return {"Linux": "linux", "Windows": "windows", "Darwin": "macos"}.get(
96        platform.system(), "other"
97    )
98
99
100def get_psutil_stats():
101    """Return whether psutil exists and its associated stats.
102
103    @returns (bool, int, int, int) whether psutil exists, the logical CPU count,
104        physical CPU count, and total number of bytes of memory.
105    """
106    try:
107        import psutil
108
109        return (
110            True,
111            psutil.cpu_count(),
112            psutil.cpu_count(logical=False),
113            psutil.virtual_memory().total,
114        )
115    except ImportError:
116        return False, None, None, None
117
118
119def get_system_info():
120    """
121    Gather info to fill the `system` keys in the schema.
122    """
123    # Normalize OS names a bit, and bucket non-tier-1 platforms into "other".
124    has_psutil, logical_cores, physical_cores, memory_total = get_psutil_stats()
125    info = {"os": get_os_name()}
126    if has_psutil:
127        # `total` on Linux is gathered from /proc/meminfo's `MemTotal`, which is the
128        # total amount of physical memory minus some kernel usage, so round up to the
129        # nearest GB to get a sensible answer.
130        info["memory_gb"] = int(math.ceil(float(memory_total) / (1024 * 1024 * 1024)))
131        info["logical_cores"] = logical_cores
132        if physical_cores is not None:
133            info["physical_cores"] = physical_cores
134    cpu_brand = get_cpu_brand()
135    if cpu_brand is not None:
136        info["cpu_brand"] = cpu_brand
137    # TODO: drive_is_ssd, virtual_machine: https://bugzilla.mozilla.org/show_bug.cgi?id=1481613
138    return info
139
140
141def get_build_opts(substs):
142    """
143    Translate selected items from `substs` into `build_opts` keys in the schema.
144    """
145    try:
146        opts = {
147            k: ty(substs.get(s, None))
148            for (k, s, ty) in (
149                # Selected substitutions.
150                ("artifact", "MOZ_ARTIFACT_BUILDS", bool),
151                ("debug", "MOZ_DEBUG", bool),
152                ("opt", "MOZ_OPTIMIZE", bool),
153                ("ccache", "CCACHE", bool),
154                ("sccache", "MOZ_USING_SCCACHE", bool),
155            )
156        }
157        compiler = substs.get("CC_TYPE", None)
158        if compiler:
159            opts["compiler"] = str(compiler)
160        if substs.get("CXX_IS_ICECREAM", None):
161            opts["icecream"] = True
162        return opts
163    except BuildEnvironmentNotFoundException:
164        return {}
165
166
167def get_build_attrs(attrs):
168    """
169    Extracts clobber and cpu usage info from command attributes.
170    """
171    res = {}
172    clobber = attrs.get("clobber")
173    if clobber:
174        res["clobber"] = clobber
175    usage = attrs.get("usage")
176    if usage:
177        cpu_percent = usage.get("cpu_percent")
178        if cpu_percent:
179            res["cpu_percent"] = int(round(cpu_percent))
180    return res
181
182
183def filter_args(command, argv, topsrcdir, topobjdir, cwd=None):
184    """
185    Given the full list of command-line arguments, remove anything up to and including `command`,
186    and attempt to filter absolute pathnames out of any arguments after that.
187    """
188    if cwd is None:
189        cwd = os.getcwd()
190
191    # Each key is a pathname and the values are replacement sigils
192    paths = {
193        topsrcdir: "$topsrcdir/",
194        topobjdir: "$topobjdir/",
195        mozpath.normpath(os.path.expanduser("~")): "$HOME/",
196        # This might override one of the existing entries, that's OK.
197        # We don't use a sigil here because we treat all arguments as potentially relative
198        # paths, so we'd like to get them back as they were specified.
199        mozpath.normpath(cwd): "",
200    }
201
202    args = list(argv)
203    while args:
204        a = args.pop(0)
205        if a == command:
206            break
207
208    def filter_path(p):
209        p = mozpath.abspath(p)
210        base = mozpath.basedir(p, paths.keys())
211        if base:
212            return paths[base] + mozpath.relpath(p, base)
213        # Best-effort.
214        return "<path omitted>"
215
216    return [filter_path(arg) for arg in args]
217
218
219def get_distro_and_version():
220    if sys.platform.startswith("linux"):
221        dist, version, _ = distro.linux_distribution(full_distribution_name=False)
222        return dist, version
223    elif sys.platform.startswith("darwin"):
224        return "macos", platform.mac_ver()[0]
225    elif sys.platform.startswith("win32") or sys.platform.startswith("msys"):
226        ver = sys.getwindowsversion()
227        return "windows", "%s.%s.%s" % (ver.major, ver.minor, ver.build)
228    else:
229        return sys.platform, ""
230
231
232def get_shell_info():
233    """Returns if the current shell was opened by vscode and if it's a SSH connection"""
234
235    return (
236        True if "vscode" in os.getenv("TERM_PROGRAM", "") else False,
237        bool(os.getenv("SSH_CLIENT", False)),
238    )
239