--- old/test/lib/jdk/test/lib/containers/docker/DockerTestUtils.java 2019-07-15 20:33:48.000000000 -0700 +++ new/test/lib/jdk/test/lib/containers/docker/DockerTestUtils.java 2019-07-15 20:33:48.000000000 -0700 @@ -56,7 +56,7 @@ // Use this property to specify docker location on your system. // E.g.: "/usr/local/bin/docker". - private static final String DOCKER_COMMAND = + public static final String DOCKER_COMMAND = System.getProperty("jdk.test.docker.command", "docker"); // Set this property to true to retain image after test. By default @@ -194,7 +194,7 @@ /** * Build the docker command to run java inside a container * - * @param DockerRunOptions optins for running docker + * @param DockerRunOptions options for running docker * * @return command * @throws Exception @@ -225,9 +225,9 @@ } /** - * Run Java inside the docker image with specified parameters and options. + * Run Java inside a docker container with specified parameters and options. * - * @param DockerRunOptions optins for running docker + * @param DockerRunOptions options for running docker * * @return output of the run command * @throws Exception @@ -240,7 +240,7 @@ /** * Remove docker image * - * @param DockerRunOptions optins for running docker + * @param DockerRunOptions options for running docker * @throws Exception */ public static void removeDockerImage(String imageNameAndTag) throws Exception { --- /dev/null 2019-07-15 20:33:49.000000000 -0700 +++ new/test/hotspot/jtreg/containers/docker/EventGeneratorLoop.java 2019-07-15 20:33:49.000000000 -0700 @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2019, 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. + */ +import jdk.jfr.Event; +import jdk.jfr.Description; +import jdk.jfr.Label; + + +// This class generates simple event in a loop +// for a specified time. +// Pass the time in seconds as a parameter. +public class EventGeneratorLoop { + + @Label("SimpleEvent") + @Description("Simple custom event") + static class SimpleEvent extends Event { + @Label("Message") + String msg; + + @Label("Count") + int count; + } + + + public static void main(String[] args) throws Exception { + if ((args.length < 1) || (args[0] == null)) { + throw new IllegalArgumentException("Expecting one argument: time to run (seconds)"); + } + int howLong = Integer.parseInt(args[0]); + + for (int i=0; i < howLong; i++) { + SimpleEvent ev = new SimpleEvent(); + ev.msg = "Hello"; + ev.count = i; + ev.commit(); + + try { Thread.sleep(1000); } catch (InterruptedException e) {} + System.out.print("."); + } + } +} --- /dev/null 2019-07-15 20:33:50.000000000 -0700 +++ new/test/hotspot/jtreg/containers/docker/TestJcmdWithSideCar.java 2019-07-15 20:33:49.000000000 -0700 @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2019, 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 + * @library /test/lib + * @modules java.base/jdk.internal.misc + * java.management + * jdk.jartool/sun.tools.jar + * @build EventGeneratorLoop + * @run driver TestJcmdWithSideCar + */ +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +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; + + +public class TestJcmdWithSideCar { + private static final String IMAGE_NAME = Common.imageName("jfr-jcmd"); + private static final Path WORK_DIR = Paths.get(".").toAbsolutePath(); + private static final int TIME_TO_RUN_MAIN_PROCESS = (int) (30 * Utils.TIMEOUT_FACTOR); // seconds + private static final String MAIN_CONTAINER_NAME = "test-container-main"; + + public static void main(String[] args) throws Exception { + if (!DockerTestUtils.canTestDocker()) { + return; + } + + DockerTestUtils.buildJdkDockerImage(IMAGE_NAME, "Dockerfile-BasicTest", "jdk-docker"); + + try { + // Start the loop process in the "main" container, then run test cases + // using a sidecar container. + DockerThread t = startMainContainer(); + + waitForMainContainerToStart(500, 10); + t.checkForErrors(); + + OutputAnalyzer jcmdOut = testCase01(); + long mainProcPid = findProcess(jcmdOut, "EventGeneratorLoop"); + + t.assertIsAlive(); + testCase02(mainProcPid); + + // JCMD does not work in sidecar configuration, except for "jcmd -l". + // Including this test case to assist in reproduction of the problem. + // t.assertIsAlive(); + // testCase03(mainProcPid); + + t.join(TIME_TO_RUN_MAIN_PROCESS * 1000); + t.checkForErrors(); + t.out.shouldHaveExitValue(0); + } finally { + DockerTestUtils.removeDockerImage(IMAGE_NAME); + } + } + + + // Run "jcmd -l" in a sidecar container and find a process that runs EventGeneratorLoop + private static OutputAnalyzer testCase01() throws Exception { + return runSideCar(MAIN_CONTAINER_NAME, "/jdk/bin/jcmd", "-l") + .shouldHaveExitValue(0) + .shouldContain("sun.tools.jcmd.JCmd") + .shouldContain("EventGeneratorLoop"); + } + + // run jhsdb jinfo (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"); + } + + private static DockerThread startMainContainer() throws Exception { + // start "main" container (the observee) + DockerRunOptions opts = commonDockerOpts("EventGeneratorLoop"); + opts.addDockerOpts("--cap-add=SYS_PTRACE") + .addDockerOpts("--name", MAIN_CONTAINER_NAME) + .addDockerOpts("-v", "/tmp") + .addJavaOpts("-XX:+UsePerfData") + .addClassOptions("" + TIME_TO_RUN_MAIN_PROCESS); + DockerThread t = new DockerThread(opts); + t.start(); + + return t; + } + + private static void waitForMainContainerToStart(int delayMillis, int count) throws Exception { + boolean started = false; + for(int i=0; i < count; i++) { + try { + Thread.sleep(delayMillis); + } catch (InterruptedException e) {} + if (isMainContainerRunning()) { + started = true; + break; + } + System.out.println("The main container has not started yet, count = " + i); + } + if (!started) { + throw new RuntimeException("Main container did not start"); + } + } + + private static boolean isMainContainerRunning() throws Exception { + OutputAnalyzer out = + DockerTestUtils.execute(DockerTestUtils.DOCKER_COMMAND, + "ps", "--no-trunc", + "--filter", "name=" + MAIN_CONTAINER_NAME); + return out.getStdout().contains(MAIN_CONTAINER_NAME); + } + + // The "sidecar" container shares "/tmp" directory with the "main" container for the + // JVM attach mechanism to work. JCMD relies on the attach mechanism (com.sun.tools.attach), + // which in turn relies on JVMSTAT mechanism, which uses Unix socket file hsperf_ residing in + // the /tmp directory. + private static OutputAnalyzer runSideCar(String MAIN_CONTAINER_NAME, String whatToRun, + String... args) throws Exception { + List cmd = new ArrayList<>(); + String[] command = new String[] { + DockerTestUtils.DOCKER_COMMAND, "run", + "--tty=true", "--rm", + "--cap-add=SYS_PTRACE", "--sig-proxy=true", + "--pid=container:" + MAIN_CONTAINER_NAME, + "--volumes-from", MAIN_CONTAINER_NAME, + IMAGE_NAME, whatToRun + }; + + cmd.addAll(Arrays.asList(command)); + cmd.addAll(Arrays.asList(args)); + return DockerTestUtils.execute(cmd); + } + + private static long findProcess(OutputAnalyzer out, String name) throws Exception { + List l = out.asLines() + .stream() + .filter(s -> s.contains(name)) + .collect(Collectors.toList()); + if (l.isEmpty()) { + throw new RuntimeException("Could not find matching process"); + } + 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/"); + } + + + static class DockerThread extends Thread { + DockerRunOptions runOpts; + OutputAnalyzer out; + Exception exception; + + DockerThread(DockerRunOptions opts) { + runOpts = opts; + } + + public void run() { + try { + out = DockerTestUtils.dockerRunJava(runOpts); + } catch (Exception e) { + exception = e; + } + } + + public void assertIsAlive() throws Exception { + if (!isAlive()) { + throw new RuntimeException("DockerThread stopped unexpectedly"); + } + } + + public void checkForErrors() throws Exception { + if (exception != null) { + throw new RuntimeException("DockerThread threw exception" + + exception.getMessage()); + } + } + } + +}