| /* |
| * Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| |
| /* |
| * @test |
| * @summary Test JCMD with side car pattern. |
| * Sidecar is a common pattern used in the cloud environments for monitoring |
| * and other uses. In side car pattern the main application/service container |
| * is paired with a sidecar container by sharing certain aspects of container |
| * namespace such as PID namespace, specific sub-directories, IPC and more. |
| * @requires docker.support |
| * @requires vm.flagless |
| * @modules java.base/jdk.internal.misc |
| * java.management |
| * jdk.jartool/sun.tools.jar |
| * @library /test/lib |
| * @build EventGeneratorLoop |
| * @run driver TestJcmdWithSideCar |
| */ |
| import java.nio.file.Paths; |
| import java.util.Arrays; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.concurrent.TimeUnit; |
| import java.util.function.Consumer; |
| import java.util.stream.Collectors; |
| import jdk.test.lib.Container; |
| import jdk.test.lib.Utils; |
| import jdk.test.lib.containers.docker.Common; |
| import jdk.test.lib.containers.docker.DockerRunOptions; |
| import jdk.test.lib.containers.docker.DockerTestUtils; |
| import jdk.test.lib.process.OutputAnalyzer; |
| import jdk.test.lib.process.ProcessTools; |
| |
| |
| public class TestJcmdWithSideCar { |
| private static final String IMAGE_NAME = Common.imageName("jfr-jcmd"); |
| private static final int TIME_TO_RUN_MAIN_PROCESS = (int) (30 * Utils.TIMEOUT_FACTOR); // seconds |
| private static final long TIME_TO_WAIT_FOR_MAIN_METHOD_START = 50 * 1000; // milliseconds |
| private static final String MAIN_CONTAINER_NAME = "test-container-main"; |
| |
| public static void main(String[] args) throws Exception { |
| if (!DockerTestUtils.canTestDocker()) { |
| return; |
| } |
| |
| DockerTestUtils.buildJdkContainerImage(IMAGE_NAME); |
| |
| try { |
| // Start the loop process in the "main" container, then run test cases |
| // using a sidecar container. |
| MainContainer mainContainer = new MainContainer(); |
| mainContainer.start(); |
| mainContainer.waitForMainMethodStart(TIME_TO_WAIT_FOR_MAIN_METHOD_START); |
| |
| long mainProcPid = testCase01(); |
| |
| // Excluding the test case below until JDK-8228850 is fixed |
| // JDK-8228850: jhsdb jinfo fails with ClassCastException: |
| // s.j.h.oops.TypeArray cannot be cast to s.j.h.oops.Instance |
| // mainContainer.assertIsAlive(); |
| // testCase02(mainProcPid); |
| |
| mainContainer.assertIsAlive(); |
| testCase03(mainProcPid); |
| |
| mainContainer.waitForAndCheck(TIME_TO_RUN_MAIN_PROCESS * 1000); |
| } finally { |
| DockerTestUtils.removeDockerImage(IMAGE_NAME); |
| } |
| } |
| |
| |
| // Run "jcmd -l" in a sidecar container, find a target process. |
| private static long testCase01() throws Exception { |
| OutputAnalyzer out = runSideCar(MAIN_CONTAINER_NAME, "/jdk/bin/jcmd", "-l") |
| .shouldHaveExitValue(0) |
| .shouldContain("sun.tools.jcmd.JCmd"); |
| long pid = findProcess(out, "EventGeneratorLoop"); |
| if (pid == -1) { |
| throw new RuntimeException("Could not find specified process"); |
| } |
| |
| return pid; |
| } |
| |
| // run jhsdb jinfo <PID> (jhsdb uses PTRACE) |
| private static void testCase02(long pid) throws Exception { |
| runSideCar(MAIN_CONTAINER_NAME, "/jdk/bin/jhsdb", "jinfo", "--pid", "" + pid) |
| .shouldHaveExitValue(0) |
| .shouldContain("Java System Properties") |
| .shouldContain("VM Flags"); |
| } |
| |
| // test jcmd with some commands (help, start JFR recording) |
| // JCMD will use signal mechanism and Unix Socket |
| private static void testCase03(long pid) throws Exception { |
| runSideCar(MAIN_CONTAINER_NAME, "/jdk/bin/jcmd", "" + pid, "help") |
| .shouldHaveExitValue(0) |
| .shouldContain("VM.version"); |
| runSideCar(MAIN_CONTAINER_NAME, "/jdk/bin/jcmd", "" + pid, "JFR.start") |
| .shouldHaveExitValue(0) |
| .shouldContain("Started recording"); |
| } |
| |
| |
| // JCMD relies on the attach mechanism (com.sun.tools.attach), |
| // which in turn relies on JVMSTAT mechanism, which puts its mapped |
| // buffers in /tmp directory (hsperfdata_<user>). Thus, in sidecar |
| // we mount /tmp via --volumes-from from the main container. |
| private static OutputAnalyzer runSideCar(String mainContainerName, String whatToRun, |
| String... args) throws Exception { |
| List<String> cmd = new ArrayList<>(); |
| String[] command = new String[] { |
| Container.ENGINE_COMMAND, "run", |
| "--tty=true", "--rm", |
| "--cap-add=SYS_PTRACE", "--sig-proxy=true", |
| "--pid=container:" + mainContainerName, |
| "--volumes-from", mainContainerName, |
| IMAGE_NAME, whatToRun |
| }; |
| |
| cmd.addAll(Arrays.asList(command)); |
| cmd.addAll(Arrays.asList(args)); |
| return DockerTestUtils.execute(cmd); |
| } |
| |
| // Returns PID of a matching process, or -1 if not found. |
| private static long findProcess(OutputAnalyzer out, String name) throws Exception { |
| List<String> l = out.asLines() |
| .stream() |
| .filter(s -> s.contains(name)) |
| .collect(Collectors.toList()); |
| if (l.isEmpty()) { |
| return -1; |
| } |
| String psInfo = l.get(0); |
| System.out.println("findProcess(): psInfo: " + psInfo); |
| String pid = psInfo.substring(0, psInfo.indexOf(' ')); |
| System.out.println("findProcess(): pid: " + pid); |
| return Long.parseLong(pid); |
| } |
| |
| private static DockerRunOptions commonDockerOpts(String className) { |
| return new DockerRunOptions(IMAGE_NAME, "/jdk/bin/java", className) |
| .addDockerOpts("--volume", Utils.TEST_CLASSES + ":/test-classes/") |
| .addJavaOpts("-cp", "/test-classes/"); |
| } |
| |
| private static void sleep(long delay) { |
| try { |
| Thread.sleep(delay); |
| } catch (InterruptedException e) { |
| System.out.println("InterruptedException" + e.getMessage()); |
| } |
| } |
| |
| |
| static class MainContainer { |
| boolean mainMethodStarted; |
| Process p; |
| |
| private Consumer<String> outputConsumer = s -> { |
| if (!mainMethodStarted && s.contains(EventGeneratorLoop.MAIN_METHOD_STARTED)) { |
| System.out.println("MainContainer: setting mainMethodStarted"); |
| mainMethodStarted = true; |
| } |
| }; |
| |
| public Process start() throws Exception { |
| // start "main" container (the observee) |
| DockerRunOptions opts = commonDockerOpts("EventGeneratorLoop"); |
| opts.addDockerOpts("--cap-add=SYS_PTRACE") |
| .addDockerOpts("--name", MAIN_CONTAINER_NAME) |
| .addDockerOpts("--volume", "/tmp") |
| .addDockerOpts("--volume", Paths.get(".").toAbsolutePath() + ":/workdir/") |
| .addJavaOpts("-XX:+UsePerfData") |
| .addClassOptions("" + TIME_TO_RUN_MAIN_PROCESS); |
| // avoid large Xmx |
| opts.appendTestJavaOptions = false; |
| |
| List<String> cmd = DockerTestUtils.buildJavaCommand(opts); |
| ProcessBuilder pb = new ProcessBuilder(cmd); |
| p = ProcessTools.startProcess("main-container-process", |
| pb, |
| outputConsumer); |
| return p; |
| } |
| |
| public void waitForMainMethodStart(long howLong) { |
| long expiration = System.currentTimeMillis() + howLong; |
| |
| do { |
| if (mainMethodStarted) { |
| return; |
| } |
| sleep(200); |
| } while (System.currentTimeMillis() < expiration); |
| |
| throw new RuntimeException("Timed out while waiting for main() to start"); |
| } |
| |
| public void assertIsAlive() throws Exception { |
| if (!p.isAlive()) { |
| throw new RuntimeException("Main container process stopped unexpectedly, exit value: " |
| + p.exitValue()); |
| } |
| } |
| |
| public void waitFor(long timeout) throws Exception { |
| p.waitFor(timeout, TimeUnit.MILLISECONDS); |
| } |
| |
| public void waitForAndCheck(long timeout) throws Exception { |
| int exitValue = -1; |
| int retryCount = 3; |
| |
| do { |
| waitFor(timeout); |
| try { |
| exitValue = p.exitValue(); |
| } catch(IllegalThreadStateException ex) { |
| System.out.println("IllegalThreadStateException occured when calling exitValue()"); |
| retryCount--; |
| } |
| } while (exitValue == -1 && retryCount > 0); |
| |
| if (exitValue != 0) { |
| throw new RuntimeException("DockerThread stopped unexpectedly, non-zero exit value is " + exitValue); |
| } |
| } |
| |
| } |
| |
| } |