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