Repository URL to install this package:
|
Version:
1.26.0.dev0+gite506aa5f ▾
|
# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).
import logging
import os
import subprocess
import sys
from abc import ABC, abstractmethod
from contextlib import contextmanager
from twitter.common.collections import maybe_list
from pants.util.contextutil import environment_as
logger = logging.getLogger(__name__)
class Executor(ABC):
"""Executes java programs.
:API: public
"""
@staticmethod
def _scrub_args(classpath, main, jvm_options, args):
classpath = maybe_list(classpath)
if not isinstance(main, str) or not main:
raise ValueError("A non-empty main classname is required, given: {}".format(main))
jvm_options = maybe_list(jvm_options or ())
args = maybe_list(args or ())
return classpath, main, jvm_options, args
class Error(Exception):
"""Indicates an error launching a java program.
:API: public
"""
class InvalidDistribution(ValueError):
"""Indicates an invalid Distribution was used to construct this runner."""
class Runner:
"""A re-usable executor that can run a configured java command line."""
@property
@abstractmethod
def executor(self):
"""Returns the executor this runner uses to run itself."""
raise NotImplementedError
@property
def cmd(self):
"""Returns a string representation of the command that will be run."""
return " ".join(self.command)
@property
@abstractmethod
def command(self):
"""Returns a copy of the command line that will be run as a list of command line
tokens."""
raise NotImplementedError
@abstractmethod
def run(self, stdout=None, stderr=None, stdin=None, cwd=None):
"""Runs the configured java command.
If there is a problem executing tha java program subclasses should raise Executor.Error.
Its guaranteed that all arguments are valid as documented in `execute`
:param stdout: An optional stream to pump stdout to; defaults to `sys.stdout`.
:param stderr: An optional stream to pump stderr to; defaults to `sys.stderr`.
:param stdin: An optional stream to receive stdin from; stdin is not propagated
by default.
:param string cwd: optionally set the working directory
"""
raise NotImplementedError
@abstractmethod
def spawn(self, stdout=None, stderr=None, stdin=None, cwd=None):
"""Spawns the configured java command.
:param stdout: An optional stream to pump stdout to; defaults to `sys.stdout`.
:param stderr: An optional stream to pump stderr to; defaults to `sys.stderr`.
:param stdin: An optional stream to receive stdin from; stdin is not propagated
by default.
:param string cwd: optionally set the working directory
"""
raise NotImplementedError
def __init__(self, distribution):
"""Constructs an Executor that can be used to launch java programs.
:param distribution: a validated java distribution to use when launching java programs.
"""
if not hasattr(distribution, "java") or not hasattr(distribution, "validate"):
raise self.InvalidDistribution(
"A valid distribution is required, given: {}".format(distribution)
)
distribution.validate()
self._distribution = distribution
@property
def distribution(self):
"""Returns the `Distribution` this executor runs via."""
return self._distribution
def runner(self, classpath, main, jvm_options=None, args=None):
"""Returns an `Executor.Runner` for the given java command."""
classpath, main, jvm_options, args = self._scrub_args(classpath, main, jvm_options, args)
return self._runner(classpath, main, jvm_options, args)
def execute(
self, classpath, main, jvm_options=None, args=None, stdout=None, stderr=None, cwd=None
):
"""Launches the java program defined by the classpath and main.
:param list classpath: the classpath for the java program
:param string main: the fully qualified class name of the java program's entry point
:param list jvm_options: an optional sequence of options for the underlying jvm
:param list args: an optional sequence of args to pass to the java program
:param string cwd: optionally set the working directory
Returns the exit code of the java program.
Raises Executor.Error if there was a problem launching java itself.
"""
runner = self.runner(classpath=classpath, main=main, jvm_options=jvm_options, args=args)
return runner.run(stdout=stdout, stderr=stderr, cwd=cwd)
@abstractmethod
def _runner(self, classpath, main, jvm_options, args):
"""Subclasses should return a `Runner` that can execute the given java main."""
def _create_command(self, classpath, main, jvm_options, args):
cmd = [self._distribution.java]
cmd.extend(jvm_options)
cmd.extend(["-cp", os.pathsep.join(classpath), main])
cmd.extend(args)
return cmd
class CommandLineGrabber(Executor):
"""Doesn't actually execute anything, just captures the cmd line."""
def __init__(self, distribution):
super().__init__(distribution=distribution)
self._command = None # Initialized when we run something.
def _runner(self, classpath, main, jvm_options, args):
self._command = self._create_command(classpath, main, jvm_options, args)
class Runner(self.Runner):
@property
def executor(_):
return self
@property
def command(_):
return list(self._command)
def run(_, stdout=None, stderr=None, stdin=None, cwd=None):
return 0
def spawn(_, stdout=None, stderr=None, stdin=None, cwd=None):
return None
return Runner()
@property
def cmd(self):
return self._command
class SubprocessExecutor(Executor):
"""Executes java programs by launching a jvm in a subprocess.
:API: public
"""
_SCRUBBED_ENV = {
# We attempt to control the classpath for correctness, caching and invalidation reasons and
# allowing CLASSPATH to influence would be a hermeticity leak
"CLASSPATH": None,
# We attempt to control jvm options and give user's explicit control in some cases as well.
# In all cases we want predictable behavior - pants defaults, repo defaults, or user tweaks
# specified on the command line. In addition cli options can affect outputs; ie: class debug
# info, target classfile version, etc - all breaking hermeticity.
"_JAVA_OPTIONS": None,
"JAVA_TOOL_OPTIONS": None,
}
@classmethod
@contextmanager
def _maybe_scrubbed_env(cls):
for env_var in cls._SCRUBBED_ENV:
value = os.getenv(env_var)
if value:
logger.warning("Scrubbing {env_var}={value}".format(env_var=env_var, value=value))
with environment_as(**cls._SCRUBBED_ENV):
yield
def _runner(self, classpath, main, jvm_options, args):
command = self._create_command(classpath, main, jvm_options, args)
class Runner(self.Runner):
@property
def executor(_):
return self
@property
def command(_):
return list(command)
def spawn(_, stdout=None, stderr=None, stdin=None, cwd=None):
return self._spawn(command, cwd=cwd, stdout=stdout, stderr=stderr, stdin=stdin)
def run(_, stdout=None, stderr=None, stdin=None, cwd=None):
return self._spawn(
command, cwd=cwd, stdout=stdout, stderr=stderr, stdin=stdin
).wait()
return Runner()
def spawn(self, classpath, main, jvm_options=None, args=None, cwd=None, **subprocess_args):
"""Spawns the java program passing any extra subprocess kwargs on to subprocess.Popen.
Returns the Popen process object handle to the spawned java program subprocess.
:API: public
:raises: :class:`Executor.Error` if there is a problem spawning the subprocess.
"""
classpath, main, jvm_options, args = self._scrub_args(classpath, main, jvm_options, args)
cmd = self._create_command(classpath, main, jvm_options, args)
return self._spawn(cmd, cwd=cwd, **subprocess_args)
def _spawn(self, cmd, cwd=None, stdout=None, stderr=None, stdin=None, **subprocess_args):
cwd = cwd or os.getcwd()
# NB: Only stdout and stderr have non-None defaults: callers that want to capture
# stdin should pass it explicitly.
stdout = stdout or sys.stdout
stderr = stderr or sys.stderr
with self._maybe_scrubbed_env():
logger.debug(
"Executing: {cmd} args={args} at cwd={cwd}".format(
cmd=" ".join(cmd), args=subprocess_args, cwd=cwd
)
)
try:
return subprocess.Popen(
cmd, cwd=cwd, stdin=stdin, stdout=stdout, stderr=stderr, **subprocess_args
)
except OSError as e:
raise self.Error(
"Problem executing {0} at cwd={1}: {2}".format(self._distribution.java, cwd, e)
)