PageRenderTime 68ms CodeModel.GetById 30ms RepoModel.GetById 0ms app.codeStats 1ms

/src/python/pants/java/nailgun_executor.py

https://gitlab.com/Ivy001/pants
Python | 286 lines | 231 code | 26 blank | 29 comment | 15 complexity | 168e01093fc8582b89b73436ebe16597 MD5 | raw file
  1. # coding=utf-8
  2. # Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
  3. # Licensed under the Apache License, Version 2.0 (see LICENSE).
  4. from __future__ import (absolute_import, division, generators, nested_scopes, print_function,
  5. unicode_literals, with_statement)
  6. import hashlib
  7. import logging
  8. import os
  9. import re
  10. import select
  11. import threading
  12. import time
  13. from contextlib import closing
  14. from six import string_types
  15. from twitter.common.collections import maybe_list
  16. from pants.base.build_environment import get_buildroot
  17. from pants.java.executor import Executor, SubprocessExecutor
  18. from pants.java.nailgun_client import NailgunClient
  19. from pants.pantsd.process_manager import ProcessGroup, ProcessManager
  20. from pants.util.dirutil import safe_file_dump, safe_open
  21. logger = logging.getLogger(__name__)
  22. class NailgunProcessGroup(ProcessGroup):
  23. _NAILGUN_KILL_LOCK = threading.Lock()
  24. def __init__(self, metadata_base_dir=None):
  25. super(NailgunProcessGroup, self).__init__(name='nailgun', metadata_base_dir=metadata_base_dir)
  26. # TODO: this should enumerate the .pids dir first, then fallback to ps enumeration (& warn).
  27. def _iter_nailgun_instances(self, everywhere=False):
  28. def predicate(proc):
  29. if proc.name() == NailgunExecutor._PROCESS_NAME:
  30. if not everywhere:
  31. return NailgunExecutor._PANTS_NG_BUILDROOT_ARG in proc.cmdline()
  32. else:
  33. return any(arg.startswith(NailgunExecutor._PANTS_NG_ARG_PREFIX) for arg in proc.cmdline())
  34. return self.iter_instances(predicate)
  35. def killall(self, everywhere=False):
  36. """Kills all nailgun servers started by pants.
  37. :param bool everywhere: If ``True``, kills all pants-started nailguns on this machine;
  38. otherwise restricts the nailguns killed to those started for the
  39. current build root.
  40. """
  41. with self._NAILGUN_KILL_LOCK:
  42. for proc in self._iter_nailgun_instances(everywhere):
  43. logger.info('killing nailgun server pid={pid}'.format(pid=proc.pid))
  44. proc.terminate()
  45. # TODO: Once we integrate standard logging into our reporting framework, we can consider making
  46. # some of the log.debug() below into log.info(). Right now it just looks wrong on the console.
  47. class NailgunExecutor(Executor, ProcessManager):
  48. """Executes java programs by launching them in nailgun server.
  49. If a nailgun is not available for a given set of jvm args and classpath, one is launched and
  50. re-used for the given jvm args and classpath on subsequent runs.
  51. """
  52. # 'NGServer 0.9.1 started on 127.0.0.1, port 53785.'
  53. _NG_PORT_REGEX = re.compile(r'.*\s+port\s+(\d+)\.$')
  54. # Used to identify if we own a given nailgun server.
  55. _PANTS_NG_ARG_PREFIX = b'-Dpants.buildroot'
  56. _PANTS_FINGERPRINT_ARG_PREFIX = b'-Dpants.nailgun.fingerprint'
  57. _PANTS_OWNER_ARG_PREFIX = b'-Dpants.nailgun.owner'
  58. _PANTS_NG_BUILDROOT_ARG = '='.join((_PANTS_NG_ARG_PREFIX, get_buildroot()))
  59. _NAILGUN_SPAWN_LOCK = threading.Lock()
  60. _SELECT_WAIT = 1
  61. _PROCESS_NAME = b'java'
  62. def __init__(self, identity, workdir, nailgun_classpath, distribution, ins=None,
  63. connect_timeout=10, connect_attempts=5, metadata_base_dir=None):
  64. Executor.__init__(self, distribution=distribution)
  65. ProcessManager.__init__(self,
  66. name=identity,
  67. process_name=self._PROCESS_NAME,
  68. metadata_base_dir=metadata_base_dir)
  69. if not isinstance(workdir, string_types):
  70. raise ValueError('Workdir must be a path string, not: {workdir}'.format(workdir=workdir))
  71. self._identity = identity
  72. self._workdir = workdir
  73. self._ng_stdout = os.path.join(workdir, 'stdout')
  74. self._ng_stderr = os.path.join(workdir, 'stderr')
  75. self._nailgun_classpath = maybe_list(nailgun_classpath)
  76. self._ins = ins
  77. self._connect_timeout = connect_timeout
  78. self._connect_attempts = connect_attempts
  79. def __str__(self):
  80. return 'NailgunExecutor({identity}, dist={dist}, pid={pid} socket={socket})'.format(
  81. identity=self._identity, dist=self._distribution, pid=self.pid, socket=self.socket)
  82. def _parse_fingerprint(self, cmdline):
  83. fingerprints = [cmd.split('=')[1] for cmd in cmdline if cmd.startswith(
  84. self._PANTS_FINGERPRINT_ARG_PREFIX + '=')]
  85. return fingerprints[0] if fingerprints else None
  86. @property
  87. def fingerprint(self):
  88. """This provides the nailgun fingerprint of the running process otherwise None."""
  89. if self.cmdline:
  90. return self._parse_fingerprint(self.cmdline)
  91. def _create_owner_arg(self, workdir):
  92. # Currently the owner is identified via the full path to the workdir.
  93. return '='.join((self._PANTS_OWNER_ARG_PREFIX, workdir))
  94. def _create_fingerprint_arg(self, fingerprint):
  95. return '='.join((self._PANTS_FINGERPRINT_ARG_PREFIX, fingerprint))
  96. @staticmethod
  97. def _fingerprint(jvm_options, classpath, java_version):
  98. """Compute a fingerprint for this invocation of a Java task.
  99. :param list jvm_options: JVM options passed to the java invocation
  100. :param list classpath: The -cp arguments passed to the java invocation
  101. :param Revision java_version: return value from Distribution.version()
  102. :return: a hexstring representing a fingerprint of the java invocation
  103. """
  104. digest = hashlib.sha1()
  105. # TODO(John Sirois): hash classpath contents?
  106. [digest.update(item) for item in (''.join(sorted(jvm_options)),
  107. ''.join(sorted(classpath)),
  108. repr(java_version))]
  109. return digest.hexdigest()
  110. def _runner(self, classpath, main, jvm_options, args, cwd=None):
  111. """Runner factory. Called via Executor.execute()."""
  112. command = self._create_command(classpath, main, jvm_options, args)
  113. class Runner(self.Runner):
  114. @property
  115. def executor(this):
  116. return self
  117. @property
  118. def command(self):
  119. return list(command)
  120. def run(this, stdout=None, stderr=None, cwd=None):
  121. nailgun = self._get_nailgun_client(jvm_options, classpath, stdout, stderr)
  122. try:
  123. logger.debug('Executing via {ng_desc}: {cmd}'.format(ng_desc=nailgun, cmd=this.cmd))
  124. return nailgun.execute(main, cwd, *args)
  125. except nailgun.NailgunError as e:
  126. self.terminate()
  127. raise self.Error('Problem launching via {ng_desc} command {main} {args}: {msg}'
  128. .format(ng_desc=nailgun, main=main, args=' '.join(args), msg=e))
  129. return Runner()
  130. def _check_nailgun_state(self, new_fingerprint):
  131. running = self.is_alive()
  132. updated = running and (self.fingerprint != new_fingerprint or
  133. self.cmd != self._distribution.java)
  134. logging.debug('Nailgun {nailgun} state: updated={up!s} running={run!s} fingerprint={old_fp} '
  135. 'new_fingerprint={new_fp} distribution={old_dist} new_distribution={new_dist}'
  136. .format(nailgun=self._identity, up=updated, run=running,
  137. old_fp=self.fingerprint, new_fp=new_fingerprint,
  138. old_dist=self.cmd, new_dist=self._distribution.java))
  139. return running, updated
  140. def _get_nailgun_client(self, jvm_options, classpath, stdout, stderr):
  141. """This (somewhat unfortunately) is the main entrypoint to this class via the Runner. It handles
  142. creation of the running nailgun server as well as creation of the client."""
  143. classpath = self._nailgun_classpath + classpath
  144. new_fingerprint = self._fingerprint(jvm_options, classpath, self._distribution.version)
  145. with self._NAILGUN_SPAWN_LOCK:
  146. running, updated = self._check_nailgun_state(new_fingerprint)
  147. if running and updated:
  148. logger.debug('Found running nailgun server that needs updating, killing {server}'
  149. .format(server=self._identity))
  150. self.terminate()
  151. if (not running) or (running and updated):
  152. return self._spawn_nailgun_server(new_fingerprint, jvm_options, classpath, stdout, stderr)
  153. return self._create_ngclient(self.socket, stdout, stderr)
  154. def _await_socket(self, timeout):
  155. """Blocks for the nailgun subprocess to bind and emit a listening port in the nailgun stdout."""
  156. with safe_open(self._ng_stdout, 'r') as ng_stdout:
  157. start_time = time.time()
  158. while 1:
  159. readable, _, _ = select.select([ng_stdout], [], [], self._SELECT_WAIT)
  160. if readable:
  161. line = ng_stdout.readline() # TODO: address deadlock risk here.
  162. try:
  163. return self._NG_PORT_REGEX.match(line).group(1)
  164. except AttributeError:
  165. pass
  166. if (time.time() - start_time) > timeout:
  167. raise NailgunClient.NailgunError(
  168. 'Failed to read nailgun output after {sec} seconds!'.format(sec=timeout))
  169. def _create_ngclient(self, port, stdout, stderr):
  170. return NailgunClient(port=port, ins=self._ins, out=stdout, err=stderr, workdir=get_buildroot())
  171. def ensure_connectable(self, nailgun):
  172. """Ensures that a nailgun client is connectable or raises NailgunError."""
  173. attempt_count = 1
  174. while 1:
  175. try:
  176. with closing(nailgun.try_connect()) as sock:
  177. logger.debug('Verified new ng server is connectable at {}'.format(sock.getpeername()))
  178. return
  179. except nailgun.NailgunConnectionError:
  180. if attempt_count >= self._connect_attempts:
  181. logger.debug('Failed to connect to ng after {} attempts'.format(self._connect_attempts))
  182. raise # Re-raise the NailgunConnectionError which provides more context to the user.
  183. attempt_count += 1
  184. time.sleep(self.WAIT_INTERVAL_SEC)
  185. def _spawn_nailgun_server(self, fingerprint, jvm_options, classpath, stdout, stderr):
  186. """Synchronously spawn a new nailgun server."""
  187. # Truncate the nailguns stdout & stderr.
  188. safe_file_dump(self._ng_stdout, '')
  189. safe_file_dump(self._ng_stderr, '')
  190. jvm_options = jvm_options + [self._PANTS_NG_BUILDROOT_ARG,
  191. self._create_owner_arg(self._workdir),
  192. self._create_fingerprint_arg(fingerprint)]
  193. post_fork_child_opts = dict(fingerprint=fingerprint,
  194. jvm_options=jvm_options,
  195. classpath=classpath,
  196. stdout=stdout,
  197. stderr=stderr)
  198. logger.debug('Spawning nailgun server {i} with fingerprint={f}, jvm_options={j}, classpath={cp}'
  199. .format(i=self._identity, f=fingerprint, j=jvm_options, cp=classpath))
  200. self.daemon_spawn(post_fork_child_opts=post_fork_child_opts)
  201. # Wait for and write the port information in the parent so we can bail on exception/timeout.
  202. self.await_pid(self._connect_timeout)
  203. self.write_socket(self._await_socket(self._connect_timeout))
  204. logger.debug('Spawned nailgun server {i} with fingerprint={f}, pid={pid} port={port}'
  205. .format(i=self._identity, f=fingerprint, pid=self.pid, port=self.socket))
  206. client = self._create_ngclient(self.socket, stdout, stderr)
  207. self.ensure_connectable(client)
  208. return client
  209. def _check_process_buildroot(self, process):
  210. """Matches only processes started from the current buildroot."""
  211. return self._PANTS_NG_BUILDROOT_ARG in process.cmdline()
  212. def is_alive(self):
  213. """A ProcessManager.is_alive() override that ensures buildroot flags are present in the process
  214. command line arguments."""
  215. return super(NailgunExecutor, self).is_alive(self._check_process_buildroot)
  216. def post_fork_child(self, fingerprint, jvm_options, classpath, stdout, stderr):
  217. """Post-fork() child callback for ProcessManager.daemon_spawn()."""
  218. java = SubprocessExecutor(self._distribution)
  219. subproc = java.spawn(classpath=classpath,
  220. main='com.martiansoftware.nailgun.NGServer',
  221. jvm_options=jvm_options,
  222. args=[':0'],
  223. stdin=safe_open('/dev/null', 'r'),
  224. stdout=safe_open(self._ng_stdout, 'w'),
  225. stderr=safe_open(self._ng_stderr, 'w'),
  226. close_fds=True)
  227. self.write_pid(subproc.pid)