PageRenderTime 25ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/amps-maven-plugin/src/main/java/com/atlassian/maven/plugins/amps/product/BitbucketProductHandler.java

https://bitbucket.org/atlassian/amps
Java | 660 lines | 577 code | 57 blank | 26 comment | 24 complexity | d49a62ab43e5fb49757055ad4788b51f MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause
  1. package com.atlassian.maven.plugins.amps.product;
  2. import com.atlassian.maven.plugins.amps.MavenContext;
  3. import com.atlassian.maven.plugins.amps.MavenGoals;
  4. import com.atlassian.maven.plugins.amps.Node;
  5. import com.atlassian.maven.plugins.amps.Product;
  6. import com.atlassian.maven.plugins.amps.ProductArtifact;
  7. import com.atlassian.maven.plugins.amps.product.manager.WebAppManager;
  8. import com.atlassian.maven.plugins.amps.util.JvmArgsFix;
  9. import com.atlassian.maven.plugins.amps.util.MavenProjectLoader;
  10. import com.atlassian.maven.plugins.amps.util.PropertyUtils;
  11. import com.atlassian.maven.plugins.amps.util.ant.AntJavaExecutorThread;
  12. import com.atlassian.maven.plugins.amps.util.ant.JavaTaskFactory;
  13. import com.google.common.annotations.VisibleForTesting;
  14. import com.google.common.collect.ImmutableMap;
  15. import org.apache.maven.artifact.Artifact;
  16. import org.apache.maven.artifact.resolver.ArtifactResolver;
  17. import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
  18. import org.apache.maven.model.Dependency;
  19. import org.apache.maven.plugin.MojoExecutionException;
  20. import org.apache.maven.project.ProjectBuilder;
  21. import org.apache.maven.repository.RepositorySystem;
  22. import org.apache.tools.ant.taskdefs.Java;
  23. import javax.annotation.Nonnull;
  24. import javax.management.AttributeNotFoundException;
  25. import javax.management.InstanceNotFoundException;
  26. import javax.management.MBeanServerConnection;
  27. import javax.management.ObjectName;
  28. import javax.management.ReflectionException;
  29. import javax.management.remote.JMXConnector;
  30. import javax.management.remote.JMXConnectorFactory;
  31. import javax.management.remote.JMXServiceURL;
  32. import java.io.BufferedReader;
  33. import java.io.File;
  34. import java.io.FileNotFoundException;
  35. import java.io.IOException;
  36. import java.net.ConnectException;
  37. import java.net.InetAddress;
  38. import java.net.UnknownHostException;
  39. import java.nio.charset.StandardCharsets;
  40. import java.nio.file.Files;
  41. import java.nio.file.NoSuchFileException;
  42. import java.nio.file.Path;
  43. import java.util.ArrayList;
  44. import java.util.Collection;
  45. import java.util.HashMap;
  46. import java.util.List;
  47. import java.util.Map;
  48. import java.util.Optional;
  49. import java.util.Properties;
  50. import java.util.UUID;
  51. import static com.atlassian.maven.plugins.amps.util.ProductHandlerUtil.pickFreePort;
  52. import static java.lang.Boolean.TRUE;
  53. import static java.lang.String.format;
  54. import static java.util.Collections.emptyList;
  55. import static java.util.Objects.requireNonNull;
  56. import static java.util.Optional.empty;
  57. import static java.util.Optional.of;
  58. import static java.util.stream.Collectors.joining;
  59. import static org.apache.commons.io.FileUtils.deleteQuietly;
  60. import static org.apache.commons.lang3.StringUtils.defaultString;
  61. import static org.apache.commons.lang3.StringUtils.isBlank;
  62. import static org.apache.commons.lang3.StringUtils.isNotBlank;
  63. import static org.apache.maven.artifact.Artifact.LATEST_VERSION;
  64. import static org.apache.maven.artifact.Artifact.RELEASE_VERSION;
  65. /**
  66. * @since 6.1.0
  67. */
  68. public class BitbucketProductHandler extends AbstractProductHandler {
  69. @VisibleForTesting
  70. static final String EMBEDDED_ELASTICSEARCH_HTTP_PORT = "plugin.embedded-elasticsearch.http.port";
  71. @VisibleForTesting
  72. static final String EMBEDDED_ELASTICSEARCH_TCP_PORT = "plugin.embedded-elasticsearch.transport.tcp.port";
  73. @VisibleForTesting
  74. static final String SSH_PORT = "plugin.ssh.port";
  75. @VisibleForTesting
  76. static final String HAZELCAST_GROUP = "hazelcast.group.name";
  77. @VisibleForTesting
  78. static final String HAZELCAST_PORT = "hazelcast.port";
  79. private static final String ADMIN_OBJECT_NAME = "org.springframework.boot:type=Admin,name=SpringApplication";
  80. private static final int DEFAULT_JMX_PORT = 7995;
  81. // Note: Versions require an "-a0" qualifier so that milestone, rc and snapshot releases are evaluated as
  82. // being 'later' (see org.apache.maven.artifact.versioning.ComparableVersion.StringItem)
  83. private static final DefaultArtifactVersion FIRST_SEARCH_VERSION = new DefaultArtifactVersion("4.6.0-a0");
  84. private static final DefaultArtifactVersion FIRST_SPRING_BOOT_VERSION = new DefaultArtifactVersion("5.0.0-a0");
  85. private static final String JMX_PORT_FILE = "jmx-port";
  86. private static final String JMX_URL_FORMAT = "service:jmx:rmi:///jndi/rmi://127.0.0.1:%1$d/jmxrmi";
  87. private static final String SEARCH_GROUP_ID = "com.atlassian.bitbucket.search";
  88. private static final String SERVER_GROUP_ID = "com.atlassian.bitbucket.server";
  89. private static final String ELASTICSEARCH_BASEURL = "plugin.search.elasticsearch.baseurl";
  90. private static final String HAZELCAST_NETWORK_TCPIP = "hazelcast.network.tcpip";
  91. private static final String HAZELCAST_NETWORK_TCPIP_MEMBERS = "hazelcast.network.tcpip.members";
  92. private static final String HAZELCAST_PASSWORD = "hazelcast.group.password";
  93. private final MavenProjectLoader projectLoader;
  94. private final ProjectBuilder projectBuilder;
  95. private final JavaTaskFactory taskFactory;
  96. private final WebAppManager webAppManager;
  97. public BitbucketProductHandler(final MavenContext context,
  98. final MavenGoals goals,
  99. final RepositorySystem repositorySystem,
  100. final MavenProjectLoader projectLoader,
  101. final ProjectBuilder projectBuilder,
  102. final ArtifactResolver artifactResolver,
  103. final WebAppManager webAppManager) {
  104. super(context, goals, new BitbucketPluginProvider(), repositorySystem, artifactResolver);
  105. this.projectLoader = requireNonNull(projectLoader);
  106. this.projectBuilder = requireNonNull(projectBuilder);
  107. this.webAppManager = requireNonNull(webAppManager);
  108. this.taskFactory = new JavaTaskFactory(log);
  109. }
  110. @Override
  111. protected void cleanupProductHomeForZip(@Nonnull final Product product, @Nonnull final File snapshotDir)
  112. throws MojoExecutionException, IOException {
  113. super.cleanupProductHomeForZip(product, snapshotDir);
  114. deleteQuietly(new File(snapshotDir, "log/atlassian-bitbucket.log"));
  115. deleteQuietly(new File(snapshotDir, ".osgi-cache"));
  116. }
  117. @Override
  118. @Nonnull
  119. public String getId() {
  120. return ProductHandlerFactory.BITBUCKET;
  121. }
  122. @Override
  123. @Nonnull
  124. public List<ProductArtifact> getAdditionalPlugins(final Product bitbucket) throws MojoExecutionException {
  125. final List<ProductArtifact> additionalPlugins = new ArrayList<>();
  126. if (usesElasticSearch(bitbucket)) {
  127. // Add the embedded ES plugin, at the same version as the production search plugin
  128. getSearchPluginDependency(bitbucket)
  129. .ifPresent(dependency -> additionalPlugins.add(new ProductArtifact(SEARCH_GROUP_ID,
  130. "embedded-elasticsearch-plugin", dependency.getVersion())));
  131. }
  132. return additionalPlugins;
  133. }
  134. private boolean usesElasticSearch(final Product bitbucket) {
  135. return new DefaultArtifactVersion(bitbucket.getVersion()).compareTo(FIRST_SEARCH_VERSION) >= 0;
  136. }
  137. private Optional<Dependency> getSearchPluginDependency(final Product bitbucket) throws MojoExecutionException {
  138. final Artifact bitbucketParentPom = repositorySystem.createProjectArtifact(
  139. SERVER_GROUP_ID, "bitbucket-parent", bitbucket.getVersion());
  140. return projectLoader.loadMavenProject(context.getProject(), bitbucketParentPom, projectBuilder)
  141. .flatMap(mavenProject -> Optional.ofNullable(mavenProject.getDependencyManagement())
  142. .flatMap(dependencyManagement -> dependencyManagement.getDependencies().stream()
  143. .filter(dep -> SEARCH_GROUP_ID.equals(dep.getGroupId()))
  144. .findFirst()));
  145. }
  146. @Nonnull
  147. @Override
  148. public ProductArtifact getArtifact() {
  149. return new ProductArtifact(SERVER_GROUP_ID, "bitbucket-webapp");
  150. }
  151. @Override
  152. @Nonnull
  153. public File getBundledPluginPath(final Product product, final File productDir) {
  154. // Starting from 4.8, bundled plugins are no longer a zip file. Instead, they're unpacked in the
  155. // webapp itself. This way, first run doesn't need to pay the I/O cost to unpack them
  156. final File bundledPluginsDir = new File(productDir, "WEB-INF/atlassian-bundled-plugins");
  157. if (bundledPluginsDir.isDirectory()) {
  158. return bundledPluginsDir;
  159. }
  160. // If the atlassian-bundled-plugins directory doesn't exist, assume we're using an older version
  161. // of Bitbucket Server where bundled plugins are still zipped up
  162. return new File(productDir, "WEB-INF/classes/bitbucket-bundled-plugins.zip");
  163. }
  164. @Override
  165. @Nonnull
  166. public String getDefaultContainerId() {
  167. return "tomcat8x";
  168. }
  169. @Override
  170. public int getDefaultHttpPort() {
  171. return 7990;
  172. }
  173. @Override
  174. public int getDefaultHttpsPort() {
  175. return 8447;
  176. }
  177. @Nonnull
  178. @Override
  179. public Map<String, String> getSystemProperties(final Product product, final int nodeIndex) {
  180. final ImmutableMap.Builder<String, String> builder = ImmutableMap.<String, String>builder()
  181. .put("johnson.spring.lifecycle.synchronousStartup", TRUE.toString());
  182. defaultSyspropsFromFile(product);
  183. final File homeDirectory = getHomeDirectories(product).get(nodeIndex);
  184. final String baseUrl = product.getBaseUrlForNode(nodeIndex);
  185. builder.put("baseurl", baseUrl);
  186. builder.put("baseurl.display", baseUrl);
  187. builder.put("bitbucket.home", fixSlashes(homeDirectory.getPath()));
  188. if (product.isMultiNode()) {
  189. configureNodeForElasticsearch(product, nodeIndex);
  190. setUpSsh(product, nodeIndex);
  191. setUpHazelcast(product, nodeIndex);
  192. builder.put("cluster.node.name", product.getInstanceId() + "-" + nodeIndex);
  193. }
  194. return builder.build();
  195. }
  196. private static void configureNodeForElasticsearch(final Product product,
  197. final int nodeIndex) {
  198. final Node node = product.getNodes().get(nodeIndex);
  199. // We intentionally set ES ports on each node for now to prevent them
  200. // from conflicting. This won't be necessary when the embedded ES is
  201. // cluster-aware.
  202. // TODO: BSP-3059 Make this a product-level setting when embedded ES is updated.
  203. node.defaultSystemProperty(EMBEDDED_ELASTICSEARCH_HTTP_PORT, () -> String.valueOf(pickFreePort(0)));
  204. node.defaultSystemProperty(EMBEDDED_ELASTICSEARCH_TCP_PORT, () -> String.valueOf(pickFreePort(0)));
  205. final String nodeZeroElasticsearchHttpPort = product.getNodes().get(0)
  206. .getSystemProperties()
  207. .get(EMBEDDED_ELASTICSEARCH_HTTP_PORT);
  208. final String nodeZeroElasticsearchTcpPort = product.getNodes().get(0)
  209. .getSystemProperties()
  210. .get(EMBEDDED_ELASTICSEARCH_TCP_PORT);
  211. if (isBlank(nodeZeroElasticsearchHttpPort) || isBlank(nodeZeroElasticsearchTcpPort)) {
  212. throw new IllegalStateException(
  213. format("First node's Elasticsearch HTTP or TCP port is blank: '%s', '%s'",
  214. nodeZeroElasticsearchHttpPort,
  215. nodeZeroElasticsearchTcpPort));
  216. }
  217. product.defaultSystemProperty(ELASTICSEARCH_BASEURL, () ->
  218. format("http://localhost:%s", nodeZeroElasticsearchHttpPort));
  219. }
  220. private static void setUpSsh(final Product product,
  221. final int nodeIndex) {
  222. final Node node = product.getNodes().get(nodeIndex);
  223. node.defaultSystemProperty(SSH_PORT, () -> String.valueOf(pickFreePort(0)));
  224. }
  225. private static void setUpHazelcast(final Product product,
  226. final int nodeIndex) {
  227. if (nodeIndex == 0) {
  228. // Set up all hazelcast ports ahead of time so config can be appropriately managed
  229. product.getNodes().forEach(nodeToDefault ->
  230. nodeToDefault.defaultSystemProperty(HAZELCAST_PORT, () -> String.valueOf(pickFreePort(0)))
  231. );
  232. product.defaultSystemProperty(HAZELCAST_NETWORK_TCPIP, () -> String.valueOf(true));
  233. product.defaultSystemProperty(HAZELCAST_PASSWORD, () -> "admin");
  234. product.defaultSystemProperty(HAZELCAST_NETWORK_TCPIP_MEMBERS, () -> getHazelcastNetworkTcpMembers(product));
  235. product.defaultSystemProperty(HAZELCAST_GROUP, () -> String.valueOf(UUID.randomUUID()));
  236. }
  237. }
  238. private static String getHazelcastNetworkTcpMembers(final Product product) {
  239. return product.getNodes().stream()
  240. .map(Node::getSystemProperties)
  241. .map(p -> "127.0.0.1:" + p.get(HAZELCAST_PORT))
  242. .collect(joining(","));
  243. }
  244. @Nonnull
  245. @Override
  246. public Optional<ProductArtifact> getTestResourcesArtifact() {
  247. return Optional.of(new ProductArtifact(SERVER_GROUP_ID, "bitbucket-it-resources"));
  248. }
  249. @Override
  250. @Nonnull
  251. public Optional<File> getUserInstalledPluginsDirectory(Product product, File webappDir, File homeDir) {
  252. File baseDir = homeDir;
  253. File sharedHomeDir = new File(homeDir, "shared");
  254. if (sharedHomeDir.exists()) {
  255. baseDir = sharedHomeDir;
  256. }
  257. return Optional.of(new File(new File(baseDir, "plugins"), "installed-plugins"));
  258. }
  259. @Override
  260. protected void customiseInstance(final Product product, final File homeDir, final File explodedWarDir) {
  261. if (product.isMultiNode()) {
  262. configureCluster(homeDir, product);
  263. }
  264. }
  265. private void configureCluster(final File homeDir, final Product product) {
  266. product.defaultSystemProperty("bitbucket.shared.home",
  267. () -> getSharedHome(product).getPath());
  268. }
  269. private void defaultSyspropsFromFile(final Product product) {
  270. getBitbucketProperties(product).ifPresent(props -> {
  271. for (Map.Entry<Object, Object> property : props.entrySet()) {
  272. product.defaultSystemProperty(String.valueOf(property.getKey()),
  273. () -> String.valueOf(property.getValue()));
  274. }
  275. });
  276. }
  277. private Optional<Properties> getBitbucketProperties(final Product product) {
  278. File propertiesFile = new File(getSharedHome(product), "bitbucket.properties");
  279. try {
  280. return Optional.of(PropertyUtils.load(propertiesFile));
  281. } catch (MojoExecutionException e) {
  282. return Optional.empty();
  283. }
  284. }
  285. private File getSharedHome(final Product product) {
  286. if (isNotBlank(product.getSharedHome())) {
  287. return new File(product.getSharedHome());
  288. }
  289. // Otherwise, use the shared-home found in the default Bitbucket home ZIP
  290. return new File(getHomeDirectories(product).get(0), "shared");
  291. }
  292. @Override
  293. public void stop(@Nonnull final Product product) throws MojoExecutionException {
  294. if (isSpringBoot(product)) {
  295. int jmxPort = readJmxPort(product);
  296. boolean connected = false;
  297. try (JMXConnector connector = createConnector(jmxPort)) {
  298. MBeanServerConnection connection = connector.getMBeanServerConnection();
  299. connected = true; // Connected successfully, so an exception most likely means success
  300. connection.invoke(new ObjectName(ADMIN_OBJECT_NAME), "shutdown", null, null);
  301. } catch (InstanceNotFoundException e) {
  302. throw new MojoExecutionException("Spring Boot administration is not available; " +
  303. "Bitbucket Server will need to be stopped manually", e);
  304. } catch (Exception e) {
  305. // Invoking shutdown will never receive a response because the application stops as part
  306. // of processing the call. The "connected" flag is used to differentiate errors
  307. if (connected) {
  308. log.debug("Bitbucket Server has stopped");
  309. } else {
  310. log.warn("There was an error attempting to stop Bitbucket Server", e);
  311. }
  312. }
  313. } else {
  314. webAppManager.stopWebapp(product, context);
  315. }
  316. }
  317. @Override
  318. @Nonnull
  319. protected File extractApplication(final Product product) throws MojoExecutionException {
  320. final ProductArtifact artifact = getArtifact(product);
  321. // check for a stable version if needed
  322. if (RELEASE_VERSION.equals(artifact.getVersion()) || LATEST_VERSION.equals(artifact.getVersion())) {
  323. setLatestStableVersion(product, artifact);
  324. }
  325. final File baseDir = getBaseDirectory(product);
  326. if (isSpringBoot(product)) {
  327. // Use the maven-dependency-plugin to unpack the WAR file, rather than copying it over
  328. final File appDir = new File(baseDir, "app");
  329. goals.unpackWebappWar(appDir, artifact);
  330. return appDir;
  331. }
  332. return goals.copyWebappWar(artifact, baseDir, product.getId());
  333. }
  334. @Override
  335. protected void fixJvmArgs(Product product) {
  336. // Don't use JvmArgsFix.defaults(); it applies -XX:MaxPermSize which just triggers warnings
  337. // on Java 8 (which is the only Java version Bitbucket Server ever allowed)
  338. final String jvmArgs = JvmArgsFix.empty()
  339. .with("-Xmx", "1g") // Use a 1g max heap by default instead of 512m
  340. .withAddOpens(ADD_OPENS_FOR_TOMCAT)
  341. .withAddOpens(ADD_OPENS_FOR_FELIX)
  342. .apply(product.getJvmArgs());
  343. product.setJvmArgs(jvmArgs);
  344. }
  345. @Override
  346. @Nonnull
  347. protected List<Node> startProduct(
  348. final Product product, final File productFile, final List<Map<String, String>> systemProperties)
  349. throws MojoExecutionException {
  350. if (isSpringBoot(product)) {
  351. startNodes(product, productFile, systemProperties);
  352. return product.getNodes();
  353. }
  354. // For Bitbucket Server 4.x, deploy the webapp to Tomcat using Cargo
  355. return webAppManager.startWebapp(productFile, systemProperties, emptyList(), emptyList(), product, context);
  356. }
  357. @Override
  358. protected boolean supportsStaticPlugins() {
  359. return true;
  360. }
  361. @Nonnull
  362. private void startNodes(final Product product,
  363. final File app,
  364. final List<Map<String, String>> systemProperties) throws MojoExecutionException {
  365. final List<Node> nodes = product.getNodes();
  366. for (int nodeIndex = 0; nodeIndex < nodes.size(); nodeIndex++) {
  367. final Node node = nodes.get(nodeIndex);
  368. int connectorPort = node.getWebPort();
  369. int jmxPort = pickJmxPort(product, connectorPort);
  370. final Map<String, String> finalSystemProperties = addJmxProperties(systemProperties.get(nodeIndex), jmxPort);
  371. startNode(product, node, app, finalSystemProperties, jmxPort);
  372. }
  373. }
  374. @Nonnull
  375. private void startNode(final Product product,
  376. final Node node,
  377. final File app,
  378. final Map<String, String> systemProperties,
  379. final int jmxPort) throws MojoExecutionException {
  380. AntJavaExecutorThread thread = startJavaThread(product, node, app, systemProperties);
  381. waitUntilReady(thread, jmxPort, product.getStartupTimeout());
  382. }
  383. private static Map<String, String> addJmxProperties(Map<String, String> properties, int jmxPort) {
  384. Map<String, String> updatedProperties = new HashMap<>(properties);
  385. updatedProperties.put("com.sun.management.jmxremote.authenticate", "false");
  386. updatedProperties.put("com.sun.management.jmxremote.port", String.valueOf(jmxPort));
  387. updatedProperties.put("com.sun.management.jmxremote.ssl", "false");
  388. // On Java 8u102 and newer, setting jmxremote.host will cause JMX to only listen on localhost. In
  389. // addition, rmi.server.hostname is explicitly set to localhost to match. Without both connecting
  390. // to JMX via TCP/IP fails in some environments.
  391. updatedProperties.put("com.sun.management.jmxremote.host", "127.0.0.1");
  392. updatedProperties.put("java.rmi.server.hostname", "127.0.0.1");
  393. return updatedProperties;
  394. }
  395. private static String fixSlashes(String path) {
  396. return path.replaceAll("\\\\", "/");
  397. }
  398. /**
  399. * Gets the local loopback address.
  400. * <p>
  401. * This method exists because {@link InetAddress#getLoopbackAddress()} may return an IPv6-style loopback address
  402. * on systems which support IPv6. Since {@link #addJmxProperties} explicitly configures RMI to run on 127.0.0.1,
  403. * and {@link #JMX_URL_FORMAT} is hard-coded for the same, we always want an IPv4-style address. If that address
  404. * fails for any reason, we fall back on {@link InetAddress#getLoopbackAddress()}.
  405. *
  406. * @return the loopback address
  407. * @since 6.3.4
  408. */
  409. private static InetAddress getLoopbackAddress() {
  410. try {
  411. return InetAddress.getByAddress("localhost", new byte[]{0x7f, 0x00, 0x00, 0x01});
  412. } catch (UnknownHostException e) {
  413. return InetAddress.getLoopbackAddress();
  414. }
  415. }
  416. private static boolean isSpringBoot(Product product) {
  417. return new DefaultArtifactVersion(product.getVersion()).compareTo(FIRST_SPRING_BOOT_VERSION) >= 0;
  418. }
  419. /**
  420. * Normalizes Tomcat's range of supported {@code CertificateVerification} settings to their Spring Boot
  421. * {@code Ssl} equivalents.
  422. * <p>
  423. * Note: Spring Boot's {@code ClientAuth} enumeration does not support Tomcat's {@code OPTIONAL_NO_CA}
  424. * setting. If that is the requested value, client auth will not be enabled.
  425. *
  426. * @param value the Tomcat value to map
  427. * @return the mapped Spring Boot value, which may be {@code empty()} if client auth should not be configured
  428. */
  429. private static Optional<String> normalizeClientAuth(String value) {
  430. switch (defaultString(value)) {
  431. case "need": // Tomcat doesn't support this. It's allowed because Spring Boot does
  432. case "require":
  433. case "required":
  434. case "true":
  435. case "yes":
  436. return of("need");
  437. case "optional":
  438. case "want":
  439. return of("want");
  440. default:
  441. return empty();
  442. }
  443. }
  444. private JMXConnector createConnector(int jmxPort) throws IOException {
  445. JMXServiceURL serviceURL = new JMXServiceURL(format(JMX_URL_FORMAT, jmxPort));
  446. return JMXConnectorFactory.connect(serviceURL);
  447. }
  448. private int pickJmxPort(Product product, int connectorPort) throws MojoExecutionException {
  449. // If the configured HTTP port is the default JMX port, skip the default and select a random port.
  450. // Checking if the port is available will likely succeed, but startup would still fail because JMX
  451. // would take the port before the HTTP connector was opened
  452. int jmxPort = pickFreePort(DEFAULT_JMX_PORT == connectorPort ? 0 : DEFAULT_JMX_PORT, getLoopbackAddress());
  453. if (jmxPort != DEFAULT_JMX_PORT) {
  454. // If the default JMX port wasn't available, write the randomly-selected port to a file in the
  455. // product's base directory. This makes it available later when the product is stopped
  456. Path jmxFile = getBaseDirectory(product).toPath().resolve(JMX_PORT_FILE);
  457. try {
  458. Files.write(jmxFile, String.valueOf(jmxPort).getBytes(StandardCharsets.UTF_8));
  459. } catch (IOException e) {
  460. // If the port file cannot be written, it won't be possible to shut the product down later
  461. // if it's started, so fail instead
  462. throw new MojoExecutionException("JMX port " + DEFAULT_JMX_PORT + " is not available, and the " +
  463. "automatically-selected replacement could not be written to " + jmxFile.toAbsolutePath(), e);
  464. }
  465. }
  466. return jmxPort;
  467. }
  468. private int readJmxPort(Product product) throws MojoExecutionException {
  469. Path jmxFile = getBaseDirectory(product).toPath().resolve(JMX_PORT_FILE);
  470. try (BufferedReader reader = Files.newBufferedReader(jmxFile, StandardCharsets.UTF_8)) {
  471. return Integer.parseInt(reader.readLine());
  472. } catch (FileNotFoundException | NoSuchFileException e) {
  473. // If the JMX port was not written to a file, assume the default is in use
  474. return DEFAULT_JMX_PORT;
  475. } catch (IOException e) {
  476. throw new MojoExecutionException("The JMX port could not be read from " + jmxFile.toAbsolutePath(), e);
  477. } catch (NumberFormatException e) {
  478. throw new MojoExecutionException("The JMX port in " + jmxFile.toAbsolutePath() + " is not valid", e);
  479. }
  480. }
  481. private AntJavaExecutorThread startJavaThread(
  482. final Product product,
  483. final Node node,
  484. final File app,
  485. final Map<String, String> properties) {
  486. Java java = taskFactory.newJavaTask(JavaTaskFactory.output(product.getOutput()).systemProperties(properties));
  487. // Set the unpacked application directory as the classpath. This will allow Java to find the
  488. // WarLauncher, which in turn will use the manifest to assemble the real classpath
  489. java.createClasspath().createPathElement().setLocation(app);
  490. java.createJvmarg().setLine(product.getJvmArgs());
  491. java.createJvmarg().setLine(node.getDebugArgs()); // If debug args aren't set, nothing happens
  492. java.createArg().setValue("--server.port=" + node.getWebPort());
  493. if (product.isHttps()) {
  494. // Configure SSL
  495. java.createArg().setValue("--server.ssl.enabled=true");
  496. java.createArg().setValue("--server.ssl.key-alias=" + product.getHttpsKeyAlias());
  497. java.createArg().setValue("--server.ssl.key-password=" + product.getHttpsKeystorePass());
  498. java.createArg().setValue("--server.ssl.key-store=" + product.getHttpsKeystoreFile());
  499. java.createArg().setValue("--server.ssl.key-store-password=" + product.getHttpsKeystorePass());
  500. java.createArg().setValue("--server.ssl.protocol=" + product.getHttpsSSLProtocol());
  501. // Client auth requires special handling, to map from the various values Tomcat accepts to
  502. // their equivalent Spring Boot value. Tomcat's native values aren't supported
  503. normalizeClientAuth(product.getHttpsClientAuth())
  504. .ifPresent(clientAuth -> java.createArg().setValue("--server.ssl.client-auth=" + clientAuth));
  505. }
  506. // Set the context path for the application
  507. java.createArg().setValue("--server.contextPath=" + product.getContextPath());
  508. // Enable Spring Boot's admin JMX endpoints, which can be used to wait for the application
  509. // to start and to shut it down gracefully later
  510. java.createArg().setValue("--spring.application.admin.enabled=true");
  511. java.createArg().setValue("--spring.application.admin.jmx-name=" + ADMIN_OBJECT_NAME);
  512. java.setClassname("org.springframework.boot.loader.WarLauncher");
  513. AntJavaExecutorThread javaThread = new AntJavaExecutorThread(java);
  514. javaThread.start();
  515. return javaThread;
  516. }
  517. private void waitUntilReady(AntJavaExecutorThread javaThread, int jmxPort, int wait) throws MojoExecutionException {
  518. long timeout = System.currentTimeMillis() + wait;
  519. while (System.currentTimeMillis() < timeout) {
  520. if (javaThread.isFinished()) {
  521. throw new MojoExecutionException("Bitbucket Server failed to start", javaThread.getBuildException());
  522. }
  523. try (JMXConnector connector = createConnector(jmxPort)) {
  524. MBeanServerConnection connection = connector.getMBeanServerConnection();
  525. Boolean ready = (Boolean) connection.getAttribute(new ObjectName(ADMIN_OBJECT_NAME), "Ready");
  526. if (TRUE.equals(ready)) {
  527. return;
  528. }
  529. } catch (AttributeNotFoundException e) {
  530. // Unexpected change to Spring Boot's "Admin" MXBean?
  531. throw new MojoExecutionException(ADMIN_OBJECT_NAME + " has no \"Ready\" attribute", e);
  532. } catch (InstanceNotFoundException e) {
  533. log.debug("Spring Boot administration for Bitbucket Server is not available yet");
  534. } catch (ReflectionException e) {
  535. throw new MojoExecutionException("Failed to retrieve \"Ready\" attribute", e);
  536. } catch (IOException e) {
  537. boolean rethrow = true;
  538. Throwable t = e;
  539. while (t != null) {
  540. if (t instanceof ConnectException) {
  541. log.debug("Bitbucket Server's MBeanServer is not available yet");
  542. rethrow = false;
  543. t = null;
  544. } else {
  545. t = t.getCause();
  546. }
  547. }
  548. if (rethrow) {
  549. throw new MojoExecutionException("Could not be connect to Bitbucket Server via JMX", e);
  550. }
  551. } catch (Exception e) {
  552. throw new MojoExecutionException(e.getMessage(), e);
  553. }
  554. try {
  555. log.debug("Waiting to retry");
  556. Thread.sleep(500L);
  557. } catch (InterruptedException e) {
  558. Thread.currentThread().interrupt();
  559. throw new IllegalStateException("Interrupted while waiting for Bitbucket Server to start");
  560. }
  561. }
  562. // If we make it here, startup timed out. Try to interrupt the process's thread, to trigger the
  563. // application to be shutdown, and then throw
  564. javaThread.interrupt();
  565. throw new MojoExecutionException("Timed out waiting for Bitbucket Server to start");
  566. }
  567. private static class BitbucketPluginProvider extends AbstractPluginProvider {
  568. @Override
  569. protected Collection<ProductArtifact> getSalArtifacts(String salVersion) {
  570. return emptyList();
  571. }
  572. }
  573. }