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