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