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