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 absolute_import
6
7import os
8import subprocess
9import sys
10
11import psutil
12
13from distutils.util import strtobool
14from distutils.version import LooseVersion
15import mozpack.path as mozpath
16
17# Minimum recommended logical processors in system.
18PROCESSORS_THRESHOLD = 4
19
20# Minimum recommended total system memory, in gigabytes.
21MEMORY_THRESHOLD = 7.4
22
23# Minimum recommended free space on each disk, in gigabytes.
24FREESPACE_THRESHOLD = 10
25
26# Latest MozillaBuild version
27LATEST_MOZILLABUILD_VERSION = '1.11.0'
28
29DISABLE_LASTACCESS_WIN = '''
30Disable the last access time feature?
31This improves the speed of file and
32directory access by deferring Last Access Time modification on disk by up to an
33hour. Backup programs that rely on this feature may be affected.
34https://technet.microsoft.com/en-us/library/cc785435.aspx
35'''
36
37class Doctor(object):
38    def __init__(self, srcdir, objdir, fix):
39        self.srcdir = mozpath.normpath(srcdir)
40        self.objdir = mozpath.normpath(objdir)
41        self.srcdir_mount = self.getmount(self.srcdir)
42        self.objdir_mount = self.getmount(self.objdir)
43        self.path_mounts = [
44            ('srcdir', self.srcdir, self.srcdir_mount),
45            ('objdir', self.objdir, self.objdir_mount)
46        ]
47        self.fix = fix
48        self.results = []
49
50    def check_all(self):
51        checks = [
52            'cpu',
53            'memory',
54            'storage_freespace',
55            'fs_lastaccess',
56            'mozillabuild'
57        ]
58        for check in checks:
59            self.report(getattr(self, check))
60        good = True
61        fixable = False
62        denied = False
63        for result in self.results:
64            if result.get('status') != 'GOOD':
65                good = False
66            if result.get('fixable', False):
67                fixable = True
68            if result.get('denied', False):
69                denied = True
70        if denied:
71            print('run "mach doctor --fix" AS ADMIN to re-attempt fixing your system')
72        elif False: # elif fixable:
73            print('run "mach doctor --fix" as admin to attempt fixing your system')
74        return int(not good)
75
76    def getmount(self, path):
77        while path != '/' and not os.path.ismount(path):
78            path = mozpath.abspath(mozpath.join(path, os.pardir))
79        return path
80
81    def prompt_bool(self, prompt, limit=5):
82        ''' Prompts the user with prompt and requires a boolean value. '''
83        valid = False
84        while not valid and limit > 0:
85            try:
86                choice = strtobool(raw_input(prompt + '[Y/N]\n'))
87                valid = True
88            except ValueError:
89                print("ERROR! Please enter a valid option!")
90                limit -= 1
91
92        if limit > 0:
93            return choice
94        else:
95            raise Exception("Error! Reached max attempts of entering option.")
96
97    def report(self, results):
98        # Handle single dict result or list of results.
99        if isinstance(results, dict):
100            results = [results]
101        for result in results:
102            status = result.get('status', 'UNSURE')
103            if status == 'SKIPPED':
104                continue
105            self.results.append(result)
106            print('%s...\t%s\n' % (
107                   result.get('desc', ''),
108                   status
109                )
110            ).expandtabs(40)
111
112    @property
113    def platform(self):
114        platform = getattr(self, '_platform', None)
115        if not platform:
116            platform = sys.platform
117            while platform[-1].isdigit():
118                platform = platform[:-1]
119            setattr(self, '_platform', platform)
120        return platform
121
122    @property
123    def cpu(self):
124        cpu_count = psutil.cpu_count()
125        if cpu_count < PROCESSORS_THRESHOLD:
126            status = 'BAD'
127            desc = '%d logical processors detected, <%d' % (
128                cpu_count, PROCESSORS_THRESHOLD
129            )
130        else:
131            status = 'GOOD'
132            desc = '%d logical processors detected, >=%d' % (
133                cpu_count, PROCESSORS_THRESHOLD
134            )
135        return {'status': status, 'desc': desc}
136
137    @property
138    def memory(self):
139        memory = psutil.virtual_memory().total
140        # Convert to gigabytes.
141        memory_GB = memory / 1024**3.0
142        if memory_GB < MEMORY_THRESHOLD:
143            status = 'BAD'
144            desc = '%.1fGB of physical memory, <%.1fGB' % (
145                memory_GB, MEMORY_THRESHOLD
146            )
147        else:
148            status = 'GOOD'
149            desc = '%.1fGB of physical memory, >%.1fGB' % (
150                memory_GB, MEMORY_THRESHOLD
151            )
152        return {'status': status, 'desc': desc}
153
154    @property
155    def storage_freespace(self):
156        results = []
157        desc = ''
158        mountpoint_line = self.srcdir_mount != self.objdir_mount
159        for (purpose, path, mount) in self.path_mounts:
160            desc += '%s = %s\n' % (purpose, path)
161            if not mountpoint_line:
162                mountpoint_line = True
163                continue
164            try:
165                usage = psutil.disk_usage(mount)
166                freespace, size = usage.free, usage.total
167                freespace_GB = freespace / 1024**3
168                size_GB = size / 1024**3
169                if freespace_GB < FREESPACE_THRESHOLD:
170                    status = 'BAD'
171                    desc += 'mountpoint = %s\n%dGB of %dGB free, <%dGB' % (
172                        mount, freespace_GB, size_GB, FREESPACE_THRESHOLD
173                    )
174                else:
175                    status = 'GOOD'
176                    desc += 'mountpoint = %s\n%dGB of %dGB free, >=%dGB' % (
177                        mount, freespace_GB, size_GB, FREESPACE_THRESHOLD
178                    )
179            except OSError:
180                status = 'UNSURE'
181                desc += 'path invalid'
182            results.append({'status': status, 'desc': desc})
183        return results
184
185    @property
186    def fs_lastaccess(self):
187        results = []
188        if self.platform == 'win':
189            fixable = False
190            denied = False
191            # See 'fsutil behavior':
192            # https://technet.microsoft.com/en-us/library/cc785435.aspx
193            try:
194                command = 'fsutil behavior query disablelastaccess'.split(' ')
195                fsutil_output = subprocess.check_output(command)
196                disablelastaccess = int(fsutil_output.partition('=')[2][1])
197            except subprocess.CalledProcessError:
198                disablelastaccess = -1
199                status = 'UNSURE'
200                desc = 'unable to check lastaccess behavior'
201            if disablelastaccess == 1:
202                status = 'GOOD'
203                desc = 'lastaccess disabled systemwide'
204            elif disablelastaccess == 0:
205                if False: # if self.fix:
206                    choice = self.prompt_bool(DISABLE_LASTACCESS_WIN)
207                    if not choice:
208                        return {'status': 'BAD, NOT FIXED',
209                                'desc': 'lastaccess enabled systemwide'}
210                    try:
211                        command = 'fsutil behavior set disablelastaccess 1'.split(' ')
212                        fsutil_output = subprocess.check_output(command)
213                        status = 'GOOD, FIXED'
214                        desc = 'lastaccess disabled systemwide'
215                    except subprocess.CalledProcessError as e:
216                        desc = 'lastaccess enabled systemwide'
217                        if e.output.find('denied') != -1:
218                            status = 'BAD, FIX DENIED'
219                            denied = True
220                        else:
221                            status = 'BAD, NOT FIXED'
222                else:
223                    status = 'BAD, FIXABLE'
224                    desc = 'lastaccess enabled'
225                    fixable = True
226            results.append({'status': status, 'desc': desc, 'fixable': fixable,
227                            'denied': denied})
228        elif self.platform in ['darwin', 'freebsd', 'linux', 'openbsd']:
229            common_mountpoint = self.srcdir_mount == self.objdir_mount
230            for (purpose, path, mount) in self.path_mounts:
231                results.append(self.check_mount_lastaccess(mount))
232                if common_mountpoint:
233                    break
234        else:
235            results.append({'status': 'SKIPPED'})
236        return results
237
238    def check_mount_lastaccess(self, mount):
239        partitions = psutil.disk_partitions()
240        atime_opts = {'atime', 'noatime', 'relatime', 'norelatime'}
241        option = ''
242        for partition in partitions:
243            if partition.mountpoint == mount:
244                mount_opts = set(partition.opts.split(','))
245                intersection = list(atime_opts & mount_opts)
246                if len(intersection) == 1:
247                    option = intersection[0]
248                break
249        if not option:
250            status = 'BAD'
251            if self.platform == 'linux':
252                option = 'noatime/relatime'
253            else:
254                option = 'noatime'
255            desc = '%s has no explicit %s mount option' % (
256                mount, option
257            )
258        elif option == 'atime' or option == 'norelatime':
259            status = 'BAD'
260            desc = '%s has %s mount option' % (
261                mount, option
262            )
263        elif option == 'noatime' or option == 'relatime':
264            status = 'GOOD'
265            desc = '%s has %s mount option' % (
266                mount, option
267            )
268        return {'status': status, 'desc': desc}
269
270    @property
271    def mozillabuild(self):
272        if self.platform != 'win':
273            return {'status': 'SKIPPED'}
274        MOZILLABUILD = mozpath.normpath(os.environ.get('MOZILLABUILD', ''))
275        if not MOZILLABUILD or not os.path.exists(MOZILLABUILD):
276            return {'desc': 'not running under MozillaBuild'}
277        try:
278            with open(mozpath.join(MOZILLABUILD, 'VERSION'), 'r') as fh:
279                version = fh.readline()
280            if not version:
281                raise ValueError()
282            if LooseVersion(version) < LooseVersion(LATEST_MOZILLABUILD_VERSION):
283                status = 'BAD'
284                desc = 'MozillaBuild %s in use, <%s' % (
285                    version, LATEST_MOZILLABUILD_VERSION
286                )
287            else:
288                status = 'GOOD'
289                desc = 'MozillaBuild %s in use' % version
290        except (IOError, ValueError):
291            status = 'UNSURE'
292            desc = 'MozillaBuild version not found'
293        return {'status': status, 'desc': desc}
294