1# -*- coding: utf-8 -*- # 2# Copyright 2013 Google LLC. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16"""Utilities for determining the current platform and architecture.""" 17 18from __future__ import absolute_import 19from __future__ import division 20from __future__ import unicode_literals 21 22import os 23import platform 24import subprocess 25import sys 26 27 28class Error(Exception): 29 """Base class for exceptions in the platforms moudle.""" 30 pass 31 32 33class InvalidEnumValue(Error): 34 """Exception for when a string could not be parsed to a valid enum value.""" 35 36 def __init__(self, given, enum_type, options): 37 """Constructs a new exception. 38 39 Args: 40 given: str, The given string that could not be parsed. 41 enum_type: str, The human readable name of the enum you were trying to 42 parse. 43 options: list(str), The valid values for this enum. 44 """ 45 super(InvalidEnumValue, self).__init__( 46 'Could not parse [{0}] into a valid {1}. Valid values are [{2}]' 47 .format(given, enum_type, ', '.join(options))) 48 49 50class OperatingSystem(object): 51 """An enum representing the operating system you are running on.""" 52 53 class _OS(object): 54 """A single operating system.""" 55 56 # pylint: disable=redefined-builtin 57 def __init__(self, id, name, file_name): 58 self.id = id 59 self.name = name 60 self.file_name = file_name 61 62 def __str__(self): 63 return self.id 64 65 def __eq__(self, other): 66 return (isinstance(other, type(self)) and 67 self.id == other.id and 68 self.name == other.name and 69 self.file_name == other.file_name) 70 71 def __hash__(self): 72 return hash(self.id) + hash(self.name) + hash(self.file_name) 73 74 def __ne__(self, other): 75 return not self == other 76 77 @classmethod 78 def _CmpHelper(cls, x, y): 79 """Just a helper equivalent to the cmp() function in Python 2.""" 80 return (x > y) - (x < y) 81 82 def __lt__(self, other): 83 return self._CmpHelper( 84 (self.id, self.name, self.file_name), 85 (other.id, other.name, other.file_name)) < 0 86 87 def __gt__(self, other): 88 return self._CmpHelper( 89 (self.id, self.name, self.file_name), 90 (other.id, other.name, other.file_name)) > 0 91 92 def __le__(self, other): 93 return not self.__gt__(other) 94 95 def __ge__(self, other): 96 return not self.__lt__(other) 97 98 WINDOWS = _OS('WINDOWS', 'Windows', 'windows') 99 MACOSX = _OS('MACOSX', 'Mac OS X', 'darwin') 100 LINUX = _OS('LINUX', 'Linux', 'linux') 101 CYGWIN = _OS('CYGWIN', 'Cygwin', 'cygwin') 102 MSYS = _OS('MSYS', 'Msys', 'msys') 103 _ALL = [WINDOWS, MACOSX, LINUX, CYGWIN, MSYS] 104 105 @staticmethod 106 def AllValues(): 107 """Gets all possible enum values. 108 109 Returns: 110 list, All the enum values. 111 """ 112 return list(OperatingSystem._ALL) 113 114 @staticmethod 115 def FromId(os_id, error_on_unknown=True): 116 """Gets the enum corresponding to the given operating system id. 117 118 Args: 119 os_id: str, The operating system id to parse 120 error_on_unknown: bool, True to raise an exception if the id is unknown, 121 False to just return None. 122 123 Raises: 124 InvalidEnumValue: If the given value cannot be parsed. 125 126 Returns: 127 OperatingSystemTuple, One of the OperatingSystem constants or None if the 128 input is None. 129 """ 130 if not os_id: 131 return None 132 for operating_system in OperatingSystem._ALL: 133 if operating_system.id == os_id: 134 return operating_system 135 if error_on_unknown: 136 raise InvalidEnumValue(os_id, 'Operating System', 137 [value.id for value in OperatingSystem._ALL]) 138 return None 139 140 @staticmethod 141 def Current(): 142 """Determines the current operating system. 143 144 Returns: 145 OperatingSystemTuple, One of the OperatingSystem constants or None if it 146 cannot be determined. 147 """ 148 if os.name == 'nt': 149 return OperatingSystem.WINDOWS 150 elif 'linux' in sys.platform: 151 return OperatingSystem.LINUX 152 elif 'darwin' in sys.platform: 153 return OperatingSystem.MACOSX 154 elif 'cygwin' in sys.platform: 155 return OperatingSystem.CYGWIN 156 return None 157 158 @staticmethod 159 def IsWindows(): 160 """Returns True if the current operating system is Windows.""" 161 return OperatingSystem.Current() is OperatingSystem.WINDOWS 162 163 164class Architecture(object): 165 """An enum representing the system architecture you are running on.""" 166 167 class _ARCH(object): 168 """A single architecture.""" 169 170 # pylint: disable=redefined-builtin 171 def __init__(self, id, name, file_name): 172 self.id = id 173 self.name = name 174 self.file_name = file_name 175 176 def __str__(self): 177 return self.id 178 179 def __eq__(self, other): 180 return (isinstance(other, type(self)) and 181 self.id == other.id and 182 self.name == other.name and 183 self.file_name == other.file_name) 184 185 def __hash__(self): 186 return hash(self.id) + hash(self.name) + hash(self.file_name) 187 188 def __ne__(self, other): 189 return not self == other 190 191 @classmethod 192 def _CmpHelper(cls, x, y): 193 """Just a helper equivalent to the cmp() function in Python 2.""" 194 return (x > y) - (x < y) 195 196 def __lt__(self, other): 197 return self._CmpHelper( 198 (self.id, self.name, self.file_name), 199 (other.id, other.name, other.file_name)) < 0 200 201 def __gt__(self, other): 202 return self._CmpHelper( 203 (self.id, self.name, self.file_name), 204 (other.id, other.name, other.file_name)) > 0 205 206 def __le__(self, other): 207 return not self.__gt__(other) 208 209 def __ge__(self, other): 210 return not self.__lt__(other) 211 212 x86 = _ARCH('x86', 'x86', 'x86') 213 x86_64 = _ARCH('x86_64', 'x86_64', 'x86_64') 214 ppc = _ARCH('PPC', 'PPC', 'ppc') 215 arm = _ARCH('arm', 'arm', 'arm') 216 _ALL = [x86, x86_64, ppc, arm] 217 218 # Possible values for `uname -m` and what arch they map to. 219 # Examples of possible values: https://en.wikipedia.org/wiki/Uname 220 _MACHINE_TO_ARCHITECTURE = { 221 'amd64': x86_64, 'x86_64': x86_64, 'i686-64': x86_64, 222 'i386': x86, 'i686': x86, 'x86': x86, 223 'ia64': x86, # Itanium is different x64 arch, treat it as the common x86. 224 'powerpc': ppc, 'power macintosh': ppc, 'ppc64': ppc, 225 'armv6': arm, 'armv6l': arm, 'arm64': arm, 'armv7': arm, 'armv7l': arm, 226 'aarch64': arm} 227 228 @staticmethod 229 def AllValues(): 230 """Gets all possible enum values. 231 232 Returns: 233 list, All the enum values. 234 """ 235 return list(Architecture._ALL) 236 237 @staticmethod 238 def FromId(architecture_id, error_on_unknown=True): 239 """Gets the enum corresponding to the given architecture id. 240 241 Args: 242 architecture_id: str, The architecture id to parse 243 error_on_unknown: bool, True to raise an exception if the id is unknown, 244 False to just return None. 245 246 Raises: 247 InvalidEnumValue: If the given value cannot be parsed. 248 249 Returns: 250 ArchitectureTuple, One of the Architecture constants or None if the input 251 is None. 252 """ 253 if not architecture_id: 254 return None 255 for arch in Architecture._ALL: 256 if arch.id == architecture_id: 257 return arch 258 if error_on_unknown: 259 raise InvalidEnumValue(architecture_id, 'Architecture', 260 [value.id for value in Architecture._ALL]) 261 return None 262 263 @staticmethod 264 def Current(): 265 """Determines the current system architecture. 266 267 Returns: 268 ArchitectureTuple, One of the Architecture constants or None if it cannot 269 be determined. 270 """ 271 return Architecture._MACHINE_TO_ARCHITECTURE.get(platform.machine().lower()) 272 273 274class Platform(object): 275 """Holds an operating system and architecture.""" 276 277 def __init__(self, operating_system, architecture): 278 """Constructs a new platform. 279 280 Args: 281 operating_system: OperatingSystem, The OS 282 architecture: Architecture, The machine architecture. 283 """ 284 self.operating_system = operating_system 285 self.architecture = architecture 286 287 def __str__(self): 288 return '{}-{}'.format(self.operating_system, self.architecture) 289 290 @staticmethod 291 def Current(os_override=None, arch_override=None): 292 """Determines the current platform you are running on. 293 294 Args: 295 os_override: OperatingSystem, A value to use instead of the current. 296 arch_override: Architecture, A value to use instead of the current. 297 298 Returns: 299 Platform, The platform tuple of operating system and architecture. Either 300 can be None if it could not be determined. 301 """ 302 return Platform( 303 os_override if os_override else OperatingSystem.Current(), 304 arch_override if arch_override else Architecture.Current()) 305 306 def UserAgentFragment(self): 307 """Generates the fragment of the User-Agent that represents the OS. 308 309 Examples: 310 (Linux 3.2.5-gg1236) 311 (Windows NT 6.1.7601) 312 (Macintosh; PPC Mac OS X 12.4.0) 313 (Macintosh; Intel Mac OS X 12.4.0) 314 315 Returns: 316 str, The fragment of the User-Agent string. 317 """ 318 # Below, there are examples of the value of platform.uname() per platform. 319 # platform.release() is uname[2], platform.version() is uname[3]. 320 if self.operating_system == OperatingSystem.LINUX: 321 # ('Linux', '<hostname goes here>', '3.2.5-gg1236', 322 # '#1 SMP Tue May 21 02:35:06 PDT 2013', 'x86_64', 'x86_64') 323 return '({name} {version})'.format( 324 name=self.operating_system.name, version=platform.release()) 325 elif self.operating_system == OperatingSystem.WINDOWS: 326 # ('Windows', '<hostname goes here>', '7', '6.1.7601', 'AMD64', 327 # 'Intel64 Family 6 Model 45 Stepping 7, GenuineIntel') 328 return '({name} NT {version})'.format( 329 name=self.operating_system.name, version=platform.version()) 330 elif self.operating_system == OperatingSystem.MACOSX: 331 # ('Darwin', '<hostname goes here>', '12.4.0', 332 # 'Darwin Kernel Version 12.4.0: Wed May 1 17:57:12 PDT 2013; 333 # root:xnu-2050.24.15~1/RELEASE_X86_64', 'x86_64', 'i386') 334 format_string = '(Macintosh; {name} Mac OS X {version})' 335 arch_string = (self.architecture.name 336 if self.architecture == Architecture.ppc else 'Intel') 337 return format_string.format( 338 name=arch_string, version=platform.release()) 339 else: 340 return '()' 341 342 def AsyncPopenArgs(self): 343 """Returns the args for spawning an async process using Popen on this OS. 344 345 Make sure the main process does not wait for the new process. On windows 346 this means setting the 0x8 creation flag to detach the process. 347 348 Killing a group leader kills the whole group. Setting creation flag 0x200 on 349 Windows or running setsid on *nix makes sure the new process is in a new 350 session with the new process the group leader. This means it can't be killed 351 if the parent is killed. 352 353 Finally, all file descriptors (FD) need to be closed so that waiting for the 354 output of the main process does not inadvertently wait for the output of the 355 new process, which means waiting for the termination of the new process. 356 If the new process wants to write to a file, it can open new FDs. 357 358 Returns: 359 {str:}, The args for spawning an async process using Popen on this OS. 360 """ 361 args = {} 362 if self.operating_system == OperatingSystem.WINDOWS: 363 args['close_fds'] = True # This is enough to close _all_ FDs on windows. 364 detached_process = 0x00000008 365 create_new_process_group = 0x00000200 366 # 0x008 | 0x200 == 0x208 367 args['creationflags'] = detached_process | create_new_process_group 368 else: 369 # Killing a group leader kills the whole group. 370 # Create a new session with the new process the group leader. 371 if sys.version_info[0] == 3 and sys.version_info[1] == 9: 372 args['start_new_session'] = True 373 else: 374 args['preexec_fn'] = os.setsid 375 args['close_fds'] = True # This closes all FDs _except_ 0, 1, 2 on *nix. 376 args['stdin'] = subprocess.PIPE 377 args['stdout'] = subprocess.PIPE 378 args['stderr'] = subprocess.PIPE 379 return args 380 381 382class PythonVersion(object): 383 """Class to validate the Python version we are using. 384 385 The Cloud SDK officially supports Python 2.7. 386 387 However, many commands do work with Python 2.6, so we don't error out when 388 users are using this (we consider it sometimes "compatible" but not 389 "supported"). 390 """ 391 392 # See class docstring for descriptions of what these mean 393 MIN_REQUIRED_PY2_VERSION = (2, 6) 394 MIN_SUPPORTED_PY2_VERSION = (2, 7) 395 MIN_REQUIRED_PY3_VERSION = (3, 4) 396 MIN_SUPPORTED_PY3_VERSION = (3, 5) 397 ENV_VAR_MESSAGE = """\ 398 399If you have a compatible Python interpreter installed, you can use it by setting 400the CLOUDSDK_PYTHON environment variable to point to it. 401 402""" 403 404 def __init__(self, version=None): 405 if version: 406 self.version = version 407 elif hasattr(sys, 'version_info'): 408 self.version = sys.version_info[:2] 409 else: 410 self.version = None 411 412 def SupportedVersionMessage(self): 413 return 'Please use Python version {0}.{1}.x or {2}.{3} and up.'.format( 414 PythonVersion.MIN_SUPPORTED_PY2_VERSION[0], 415 PythonVersion.MIN_SUPPORTED_PY2_VERSION[1], 416 PythonVersion.MIN_SUPPORTED_PY3_VERSION[0], 417 PythonVersion.MIN_SUPPORTED_PY3_VERSION[1]) 418 419 def IsCompatible(self, raise_exception=False): 420 """Ensure that the Python version we are using is compatible. 421 422 This will print an error message if not compatible. 423 424 Compatible versions are 2.6 and 2.7 and > 3.4. 425 We don't guarantee support for 2.6 so we want to warn about it. 426 We don't guarantee support for 3.4 so we want to warn about it. 427 428 Args: 429 raise_exception: bool, True to raise an exception rather than printing 430 the error and exiting. 431 432 Raises: 433 Error: If not compatible and raise_exception is True. 434 435 Returns: 436 bool, True if the version is valid, False otherwise. 437 """ 438 error = None 439 if not self.version: 440 # We don't know the version, not a good sign. 441 error = ('ERROR: Your current version of Python is not compatible with ' 442 'the Google Cloud SDK. {0}\n' 443 .format(self.SupportedVersionMessage())) 444 else: 445 if self.version[0] < 3: 446 # Python 2 Mode 447 if self.version < PythonVersion.MIN_REQUIRED_PY2_VERSION: 448 error = ('ERROR: Python {0}.{1} is not compatible with the Google ' 449 'Cloud SDK. {2}\n' 450 .format(self.version[0], self.version[1], 451 self.SupportedVersionMessage())) 452 else: 453 # Python 3 Mode 454 if self.version < PythonVersion.MIN_REQUIRED_PY3_VERSION: 455 error = ('ERROR: Python {0}.{1} is not compatible with the Google ' 456 'Cloud SDK. {2}\n' 457 .format(self.version[0], self.version[1], 458 self.SupportedVersionMessage())) 459 460 if error: 461 if raise_exception: 462 raise Error(error) 463 sys.stderr.write(error) 464 sys.stderr.write(PythonVersion.ENV_VAR_MESSAGE) 465 return False 466 467 # Warn that 2.6 might not work. 468 if (self.version >= self.MIN_REQUIRED_PY2_VERSION and 469 self.version < self.MIN_SUPPORTED_PY2_VERSION): 470 sys.stderr.write("""\ 471WARNING: Python 2.6.x is no longer officially supported by the Google Cloud SDK 472and may not function correctly. {0} 473{1}""".format(self.SupportedVersionMessage(), 474 PythonVersion.ENV_VAR_MESSAGE)) 475 476 # Warn that 3.4 might not work. XXX this logic needs some work 477 if (self.version >= self.MIN_REQUIRED_PY3_VERSION and 478 self.version < self.MIN_SUPPORTED_PY3_VERSION): 479 sys.stderr.write("""\ 480WARNING: Python 3.4.x is no longer officially supported by the Google Cloud SDK 481and may not function correctly. {0} 482{1}""".format(self.SupportedVersionMessage(), 483 PythonVersion.ENV_VAR_MESSAGE)) 484 485 return True 486