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"""Functions to help with shelling out to other commands."""
17
18from __future__ import absolute_import
19from __future__ import division
20from __future__ import unicode_literals
21
22
23import contextlib
24import errno
25import os
26import re
27import signal
28import subprocess
29import sys
30import threading
31import time
32
33
34from googlecloudsdk.core import argv_utils
35from googlecloudsdk.core import config
36from googlecloudsdk.core import exceptions
37from googlecloudsdk.core import log
38from googlecloudsdk.core import properties
39from googlecloudsdk.core.configurations import named_configs
40from googlecloudsdk.core.util import encoding
41from googlecloudsdk.core.util import parallel
42from googlecloudsdk.core.util import platforms
43
44import six
45from six.moves import map
46
47
48class OutputStreamProcessingException(exceptions.Error):
49  """Error class for errors raised during output stream processing."""
50
51
52class PermissionError(exceptions.Error):
53  """User does not have execute permissions."""
54
55  def __init__(self, error):
56    super(PermissionError, self).__init__(
57        '{err}\nPlease verify that you have execute permission for all '
58        'files in your CLOUD SDK bin folder'.format(err=error))
59
60
61class InvalidCommandError(exceptions.Error):
62  """Command entered cannot be found."""
63
64  def __init__(self, cmd):
65    super(InvalidCommandError, self).__init__(
66        '{cmd}: command not found'.format(cmd=cmd))
67
68
69# Doesn't work in par or stub files.
70def GetPythonExecutable():
71  """Gets the path to the Python interpreter that should be used."""
72  cloudsdk_python = encoding.GetEncodedValue(os.environ, 'CLOUDSDK_PYTHON')
73  if cloudsdk_python:
74    return cloudsdk_python
75  python_bin = sys.executable
76  if not python_bin:
77    raise ValueError('Could not find Python executable.')
78  return python_bin
79
80
81# From https://en.wikipedia.org/wiki/Unix_shell#Bourne_shell_compatible
82# Many scripts that we execute via execution_utils are bash scripts, and we need
83# a compatible shell to run them.
84# zsh, was initially on this list, but it doesn't work 100% without running it
85# in `emulate sh` mode.
86_BORNE_COMPATIBLE_SHELLS = [
87    'ash',
88    'bash',
89    'busybox'
90    'dash',
91    'ksh',
92    'mksh',
93    'pdksh',
94    'sh',
95]
96
97
98def _GetShellExecutable():
99  """Gets the path to the Shell that should be used.
100
101  First tries the current environment $SHELL, if set, then `bash` and `sh`. The
102  first of these that is found is used.
103
104  The shell must be Borne-compatible, as the commands that we execute with it
105  are often bash/sh scripts.
106
107  Returns:
108    str, the path to the shell
109
110  Raises:
111    ValueError: if no Borne compatible shell is found
112  """
113  shells = ['/bin/bash', '/bin/sh']
114
115  user_shell = encoding.GetEncodedValue(os.environ, 'SHELL')
116  if user_shell and os.path.basename(user_shell) in _BORNE_COMPATIBLE_SHELLS:
117    shells.insert(0, user_shell)
118
119  for shell in shells:
120    if os.path.isfile(shell):
121      return shell
122
123  raise ValueError("You must set your 'SHELL' environment variable to a "
124                   "valid Borne-compatible shell executable to use this tool.")
125
126
127def _GetToolArgs(interpreter, interpreter_args, executable_path, *args):
128  tool_args = []
129  if interpreter:
130    tool_args.append(interpreter)
131  if interpreter_args:
132    tool_args.extend(interpreter_args)
133  tool_args.append(executable_path)
134  tool_args.extend(list(args))
135  return tool_args
136
137
138def _GetToolEnv(env=None):
139  """Generate the environment that should be used for the subprocess.
140
141  Args:
142    env: {str, str}, An existing environment to augment.  If None, the current
143      environment will be cloned and used as the base for the subprocess.
144
145  Returns:
146    The modified env.
147  """
148  if env is None:
149    env = dict(os.environ)
150  env = encoding.EncodeEnv(env)
151  encoding.SetEncodedValue(env, 'CLOUDSDK_WRAPPER', '1')
152
153  # Flags can set properties which override the properties file and the existing
154  # env vars.  We need to propagate them to children processes through the
155  # environment so that those commands will use the same settings.
156  for s in properties.VALUES:
157    for p in s:
158      encoding.SetEncodedValue(
159          env, p.EnvironmentName(), p.Get(required=False, validate=False))
160
161  # Configuration needs to be handled separately because it's not a real
162  # property (although it behaves like one).
163  encoding.SetEncodedValue(
164      env, config.CLOUDSDK_ACTIVE_CONFIG_NAME,
165      named_configs.ConfigurationStore.ActiveConfig().name)
166
167  return env
168
169
170# Doesn't work in par or stub files.
171def ArgsForPythonTool(executable_path, *args, **kwargs):
172  """Constructs an argument list for calling the Python interpreter.
173
174  Args:
175    executable_path: str, The full path to the Python main file.
176    *args: args for the command
177    **kwargs: python: str, path to Python executable to use (defaults to
178      automatically detected)
179
180  Returns:
181    An argument list to execute the Python interpreter
182
183  Raises:
184    TypeError: if an unexpected keyword argument is passed
185  """
186  unexpected_arguments = set(kwargs) - set(['python'])
187  if unexpected_arguments:
188    raise TypeError(("ArgsForPythonTool() got unexpected keyword arguments "
189                     "'[{0}]'").format(', '.join(unexpected_arguments)))
190  python_executable = kwargs.get('python') or GetPythonExecutable()
191  python_args_str = encoding.GetEncodedValue(
192      os.environ, 'CLOUDSDK_PYTHON_ARGS', '')
193  python_args = python_args_str.split()
194  return _GetToolArgs(
195      python_executable, python_args, executable_path, *args)
196
197
198def ArgsForCMDTool(executable_path, *args):
199  """Constructs an argument list for calling the cmd interpreter.
200
201  Args:
202    executable_path: str, The full path to the cmd script.
203    *args: args for the command
204
205  Returns:
206    An argument list to execute the cmd interpreter
207  """
208  return _GetToolArgs('cmd', ['/c'], executable_path, *args)
209
210
211def ArgsForExecutableTool(executable_path, *args):
212  """Constructs an argument list for an executable.
213
214   Can be used for calling a native binary or shell executable.
215
216  Args:
217    executable_path: str, The full path to the binary.
218    *args: args for the command
219
220  Returns:
221    An argument list to execute the native binary
222  """
223  return _GetToolArgs(None, None, executable_path, *args)
224
225
226# Works in regular installs as well as hermetic par and stub files. Doesn't work
227# in classic par and stub files.
228def ArgsForGcloud():
229  """Constructs an argument list to run gcloud."""
230  if not sys.executable:
231    # In hermetic par/stub files sys.executable is None. In regular installs,
232    # and in classic par/stub files it is a non-empty string.
233    return _GetToolArgs(None, None, argv_utils.GetDecodedArgv()[0])
234  return ArgsForPythonTool(config.GcloudPath())
235
236
237class _ProcessHolder(object):
238  """Process holder that can handle signals raised during processing."""
239
240  def __init__(self):
241    self.process = None
242    self.signum = None
243
244  def Handler(self, signum, unused_frame):
245    """Handle the intercepted signal."""
246    self.signum = signum
247    if self.process:
248      log.debug('Subprocess [{pid}] got [{signum}]'.format(
249          signum=signum,
250          pid=self.process.pid
251      ))
252      # We could have jumped to the signal handler between cleaning up our
253      # finished child process in communicate() and removing the signal handler.
254      # Check to see if our process is still running before we attempt to send
255      # it a signal. If poll() returns None, even if the process dies right
256      # between that and the terminate() call, the terminate() call will still
257      # complete without an error (it just might send a signal to a zombie
258      # process).
259      if self.process.poll() is None:
260        self.process.terminate()
261      # The return code will be checked later in the normal processing flow.
262
263
264@contextlib.contextmanager
265def _ReplaceSignal(signo, handler):
266  old_handler = signal.signal(signo, handler)
267  try:
268    yield
269  finally:
270    signal.signal(signo, old_handler)
271
272
273def _Exec(args,
274          process_holder,
275          env=None,
276          out_func=None,
277          err_func=None,
278          in_str=None,
279          **extra_popen_kwargs):
280  """See Exec docstring."""
281  if out_func:
282    extra_popen_kwargs['stdout'] = subprocess.PIPE
283  if err_func:
284    extra_popen_kwargs['stderr'] = subprocess.PIPE
285  if in_str:
286    extra_popen_kwargs['stdin'] = subprocess.PIPE
287  try:
288    if args and isinstance(args, list):
289      # On Python 2.x on Windows, the first arg can't be unicode. We encode
290      # encode it anyway because there is really nothing else we can do if
291      # that happens.
292      # https://bugs.python.org/issue19264
293      args = [encoding.Encode(a) for a in args]
294    p = subprocess.Popen(args, env=_GetToolEnv(env=env), **extra_popen_kwargs)
295  except OSError as err:
296    if err.errno == errno.EACCES:
297      raise PermissionError(err.strerror)
298    elif err.errno == errno.ENOENT:
299      raise InvalidCommandError(args[0])
300    raise
301  process_holder.process = p
302
303  if process_holder.signum is not None:
304    # This covers the small possibility that process_holder handled a
305    # signal when the process was starting but not yet set to
306    # process_holder.process.
307    if p.poll() is None:
308      p.terminate()
309
310  if isinstance(in_str, six.text_type):
311    in_str = in_str.encode('utf-8')
312  stdout, stderr = list(map(encoding.Decode, p.communicate(input=in_str)))
313
314  if out_func:
315    out_func(stdout)
316  if err_func:
317    err_func(stderr)
318  return p.returncode
319
320
321def Exec(args,
322         env=None,
323         no_exit=False,
324         out_func=None,
325         err_func=None,
326         in_str=None,
327         **extra_popen_kwargs):
328  """Emulates the os.exec* set of commands, but uses subprocess.
329
330  This executes the given command, waits for it to finish, and then exits this
331  process with the exit code of the child process.
332
333  Args:
334    args: [str], The arguments to execute.  The first argument is the command.
335    env: {str: str}, An optional environment for the child process.
336    no_exit: bool, True to just return the exit code of the child instead of
337      exiting.
338    out_func: str->None, a function to call with the stdout of the executed
339      process. This can be e.g. log.file_only_logger.debug or log.out.write.
340    err_func: str->None, a function to call with the stderr of the executed
341      process. This can be e.g. log.file_only_logger.debug or log.err.write.
342    in_str: bytes or str, input to send to the subprocess' stdin.
343    **extra_popen_kwargs: Any additional kwargs will be passed through directly
344      to subprocess.Popen
345
346  Returns:
347    int, The exit code of the child if no_exit is True, else this method does
348    not return.
349
350  Raises:
351    PermissionError: if user does not have execute permission for cloud sdk bin
352    files.
353    InvalidCommandError: if the command entered cannot be found.
354  """
355  log.debug('Executing command: %s', args)
356  # We use subprocess instead of execv because windows does not support process
357  # replacement.  The result of execv on windows is that a new processes is
358  # started and the original is killed.  When running in a shell, the prompt
359  # returns as soon as the parent is killed even though the child is still
360  # running.  subprocess waits for the new process to finish before returning.
361  process_holder = _ProcessHolder()
362
363  # pylint:disable=protected-access
364  # Python 3 has a cleaner way to check if on main thread, but must support PY2.
365  if isinstance(threading.current_thread(), threading._MainThread):
366    # pylint:enable=protected-access
367    # Signal replacement is not allowed by Python on non-main threads.
368    # https://bugs.python.org/issue38904
369    with _ReplaceSignal(signal.SIGTERM, process_holder.Handler):
370      with _ReplaceSignal(signal.SIGINT, process_holder.Handler):
371        ret_val = _Exec(args, process_holder, env, out_func, err_func, in_str,
372                        **extra_popen_kwargs)
373  else:
374    ret_val = _Exec(args, process_holder, env, out_func, err_func, in_str,
375                    **extra_popen_kwargs)
376
377  if no_exit and process_holder.signum is None:
378    return ret_val
379  sys.exit(ret_val)
380
381
382def _ProcessStreamHandler(proc, err=False, handler=log.Print):
383  """Process output stream from a running subprocess in realtime."""
384  stream = proc.stderr if err else proc.stdout
385  stream_reader = stream.readline
386  while True:
387    line = stream_reader() or b''
388    if not line and proc.poll() is not None:
389      try:
390        stream.close()
391      except OSError:
392        pass  # This is thread cleanup so we should just
393        # exit so runner can Join()
394      break
395    line_str = line.decode('utf-8')
396    line_str = line_str.rstrip('\r\n')
397    if line_str:
398      handler(line_str)
399
400
401def _KillProcIfRunning(proc):
402  """Kill process and close open streams."""
403  if proc:
404    if proc.poll() is None:
405      proc.terminate()
406    try:
407      if not proc.stdin.closed:
408        proc.stdin.close()
409      if not proc.stdout.closed:
410        proc.stdout.close()
411      if not proc.stderr.closed:
412        proc.stderr.close()
413    except OSError:
414      pass  # Clean Up
415
416
417def ExecWithStreamingOutput(args,
418                            env=None,
419                            no_exit=False,
420                            out_func=None,
421                            err_func=None,
422                            in_str=None,
423                            **extra_popen_kwargs):
424  """Emulates the os.exec* set of commands, but uses subprocess.
425
426  This executes the given command, waits for it to finish, and then exits this
427  process with the exit code of the child process. Allows realtime processing of
428  stderr and stdout from subprocess using threads.
429
430  Args:
431    args: [str], The arguments to execute.  The first argument is the command.
432    env: {str: str}, An optional environment for the child process.
433    no_exit: bool, True to just return the exit code of the child instead of
434      exiting.
435    out_func: str->None, a function to call with each line of the stdout of the
436      executed process. This can be e.g. log.file_only_logger.debug or
437      log.out.write.
438    err_func: str->None, a function to call with each line of the stderr of
439      the executed process. This can be e.g. log.file_only_logger.debug or
440      log.err.write.
441    in_str: bytes or str, input to send to the subprocess' stdin.
442    **extra_popen_kwargs: Any additional kwargs will be passed through directly
443      to subprocess.Popen
444
445  Returns:
446    int, The exit code of the child if no_exit is True, else this method does
447    not return.
448
449  Raises:
450    PermissionError: if user does not have execute permission for cloud sdk bin
451    files.
452    InvalidCommandError: if the command entered cannot be found.
453  """
454  log.debug('Executing command: %s', args)
455  # We use subprocess instead of execv because windows does not support process
456  # replacement.  The result of execv on windows is that a new processes is
457  # started and the original is killed.  When running in a shell, the prompt
458  # returns as soon as the parent is killed even though the child is still
459  # running.  subprocess waits for the new process to finish before returning.
460  env = _GetToolEnv(env=env)
461  process_holder = _ProcessHolder()
462  with _ReplaceSignal(signal.SIGTERM, process_holder.Handler):
463    with _ReplaceSignal(signal.SIGINT, process_holder.Handler):
464      out_handler_func = out_func or log.Print
465      err_handler_func = err_func or log.status.Print
466      if in_str:
467        extra_popen_kwargs['stdin'] = subprocess.PIPE
468      try:
469        if args and isinstance(args, list):
470          # On Python 2.x on Windows, the first arg can't be unicode. We encode
471          # encode it anyway because there is really nothing else we can do if
472          # that happens.
473          # https://bugs.python.org/issue19264
474          args = [encoding.Encode(a) for a in args]
475        p = subprocess.Popen(args, env=env, stderr=subprocess.PIPE,
476                             stdout=subprocess.PIPE, **extra_popen_kwargs)
477
478        if in_str:
479          in_str = six.text_type(in_str).encode('utf-8')
480          try:
481            p.stdin.write(in_str)
482            p.stdin.close()
483          except OSError as exc:
484            if (exc.errno == errno.EPIPE or
485                exc.errno == errno.EINVAL):
486              pass  # Obey same conventions as subprocess.communicate()
487            else:
488              _KillProcIfRunning(p)
489              raise OutputStreamProcessingException(exc)
490
491        try:
492          with parallel.GetPool(2) as pool:
493            std_out_future = pool.ApplyAsync(_ProcessStreamHandler,
494                                             (p, False, out_handler_func))
495            std_err_future = pool.ApplyAsync(_ProcessStreamHandler,
496                                             (p, True, err_handler_func))
497            std_out_future.Get()
498            std_err_future.Get()
499        except Exception as e:
500          _KillProcIfRunning(p)
501          raise  OutputStreamProcessingException(e)
502
503      except OSError as err:
504        if err.errno == errno.EACCES:
505          raise PermissionError(err.strerror)
506        elif err.errno == errno.ENOENT:
507          raise InvalidCommandError(args[0])
508        raise
509      process_holder.process = p
510
511      if process_holder.signum is not None:
512        # This covers the small possibility that process_holder handled a
513        # signal when the process was starting but not yet set to
514        # process_holder.process.
515        _KillProcIfRunning(p)
516
517      ret_val = p.returncode
518
519  if no_exit and process_holder.signum is None:
520    return ret_val
521  sys.exit(ret_val)
522
523
524def UninterruptibleSection(stream, message=None):
525  """Run a section of code with CTRL-C disabled.
526
527  When in this context manager, the ctrl-c signal is caught and a message is
528  printed saying that the action cannot be cancelled.
529
530  Args:
531    stream: the stream to write to if SIGINT is received
532    message: str, optional: the message to write
533
534  Returns:
535    Context manager that is uninterruptible during its lifetime.
536  """
537  message = '\n\n{message}\n\n'.format(
538      message=(message or 'This operation cannot be cancelled.'))
539  def _Handler(unused_signal, unused_frame):
540    stream.write(message)
541  return CtrlCSection(_Handler)
542
543
544def RaisesKeyboardInterrupt():
545  """Run a section of code where CTRL-C raises KeyboardInterrupt."""
546  def _Handler(signal, frame):  # pylint: disable=redefined-outer-name
547    del signal, frame  # Unused in _Handler
548    raise KeyboardInterrupt
549  return CtrlCSection(_Handler)
550
551
552def CtrlCSection(handler):
553  """Run a section of code with CTRL-C redirected handler.
554
555  Args:
556    handler: func(), handler to call if SIGINT is received. In every case
557      original Ctrl-C handler is not invoked.
558
559  Returns:
560    Context manager that redirects ctrl-c handler during its lifetime.
561  """
562  return _ReplaceSignal(signal.SIGINT, handler)
563
564
565def KillSubprocess(p):
566  """Kills a subprocess using an OS specific method when python can't do it.
567
568  This also kills all processes rooted in this process.
569
570  Args:
571    p: the Popen or multiprocessing.Process object to kill
572
573  Raises:
574    RuntimeError: if it fails to kill the process
575  """
576
577  # This allows us to kill a Popen object or a multiprocessing.Process object
578  code = None
579  if hasattr(p, 'returncode'):
580    code = p.returncode
581  elif hasattr(p, 'exitcode'):
582    code = p.exitcode
583
584  if code is not None:
585    # already dead
586    return
587
588  if platforms.OperatingSystem.Current() == platforms.OperatingSystem.WINDOWS:
589    # Consume stdout so it doesn't show in the shell
590    taskkill_process = subprocess.Popen(
591        ['taskkill', '/F', '/T', '/PID', six.text_type(p.pid)],
592        stdout=subprocess.PIPE,
593        stderr=subprocess.PIPE)
594    (stdout, stderr) = taskkill_process.communicate()
595    if taskkill_process.returncode != 0 and _IsTaskKillError(stderr):
596      # Sometimes taskkill does things in the wrong order and the processes
597      # disappear before it gets a chance to kill it.  This is exposed as an
598      # error even though it's the outcome we want.
599      raise RuntimeError(
600          'Failed to call taskkill on pid {0}\nstdout: {1}\nstderr: {2}'
601          .format(p.pid, stdout, stderr))
602
603  else:
604    # Create a mapping of ppid to pid for all processes, then kill all
605    # subprocesses from the main process down
606
607    # set env LANG for subprocess.Popen to be 'en_US.UTF-8'
608    new_env = encoding.EncodeEnv(dict(os.environ))
609    new_env['LANG'] = 'en_US.UTF-8'
610    get_pids_process = subprocess.Popen(['ps', '-e',
611                                         '-o', 'ppid=', '-o', 'pid='],
612                                        stdout=subprocess.PIPE,
613                                        stderr=subprocess.PIPE,
614                                        env=new_env)
615    (stdout, stderr) = get_pids_process.communicate()
616    stdout = stdout.decode('utf-8')
617    if get_pids_process.returncode != 0:
618      raise RuntimeError('Failed to get subprocesses of process: {0}'
619                         .format(p.pid))
620
621    # Create the process map
622    pid_map = {}
623    for line in stdout.strip().split('\n'):
624      (ppid, pid) = re.match(r'\s*(\d+)\s+(\d+)', line).groups()
625      ppid = int(ppid)
626      pid = int(pid)
627      children = pid_map.get(ppid)
628      if not children:
629        pid_map[ppid] = [pid]
630      else:
631        children.append(pid)
632
633    # Expand all descendants of the main process
634    all_pids = [p.pid]
635    to_process = [p.pid]
636    while to_process:
637      current = to_process.pop()
638      children = pid_map.get(current)
639      if children:
640        to_process.extend(children)
641        all_pids.extend(children)
642
643    # Kill all the subprocesses we found
644    for pid in all_pids:
645      _KillPID(pid)
646
647    # put this in if you need extra info from the process itself
648    # print p.communicate()
649
650
651def _IsTaskKillError(stderr):
652  """Returns whether the stderr output of taskkill indicates it failed.
653
654  Args:
655    stderr: the string error output of the taskkill command
656
657  Returns:
658    True iff the stderr is considered to represent an actual error.
659  """
660  # The taskkill "reason" string indicates why it fails. We consider the
661  # following reasons to be acceptable. Reason strings differ among different
662  # versions of taskkill. If you know a string is specific to a version, feel
663  # free to document that here.
664  non_error_reasons = (
665      # The process might be in the midst of exiting.
666      'Access is denied.',
667      'The operation attempted is not supported.',
668      'There is no running instance of the task.',
669      'There is no running instance of the task to terminate.')
670  non_error_patterns = (
671      re.compile(r'The process "\d+" not found\.'),)
672  for reason in non_error_reasons:
673    if reason in stderr:
674      return False
675  for pattern in non_error_patterns:
676    if pattern.search(stderr):
677      return False
678
679  return True
680
681
682def _KillPID(pid):
683  """Kills the given process with SIGTERM, then with SIGKILL if it doesn't stop.
684
685  Args:
686    pid: The process id of the process to check.
687  """
688  try:
689    # Try sigterm first.
690    os.kill(pid, signal.SIGTERM)
691
692    # If still running, wait a few seconds to see if it dies.
693    deadline = time.time() + 3
694    while time.time() < deadline:
695      if not _IsStillRunning(pid):
696        return
697      time.sleep(0.1)
698
699    # No luck, just force kill it.
700    os.kill(pid, signal.SIGKILL)
701  except OSError as error:
702    if 'No such process' not in error.strerror:
703      exceptions.reraise(sys.exc_info()[1])
704
705
706def _IsStillRunning(pid):
707  """Determines if the given pid is still running.
708
709  Args:
710    pid: The process id of the process to check.
711
712  Returns:
713    bool, True if it is still running.
714  """
715  try:
716    (actual_pid, code) = os.waitpid(pid, os.WNOHANG)
717    if (actual_pid, code) == (0, 0):
718      return True
719  except OSError as error:
720    if 'No child processes' not in error.strerror:
721      exceptions.reraise(sys.exc_info()[1])
722  return False
723