1 /* 2 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 25 /* 26 * @test 27 * @summary Test JCMD with side car pattern. 28 * Sidecar is a common pattern used in the cloud environments for monitoring 29 * and other uses. In side car pattern the main application/service container 30 * is paired with a sidecar container by sharing certain aspects of container 31 * namespace such as PID namespace, specific sub-directories, IPC and more. 32 * @requires docker.support 33 * @library /test/lib 34 * @modules java.base/jdk.internal.misc 35 * java.management 36 * jdk.jartool/sun.tools.jar 37 * @build EventGeneratorLoop 38 * @run driver TestJcmdWithSideCar 39 */ 40 import java.nio.file.Path; 41 import java.nio.file.Paths; 42 import java.util.Arrays; 43 import java.util.ArrayList; 44 import java.util.List; 45 import java.util.stream.Collectors; 46 import jdk.test.lib.Container; 47 import jdk.test.lib.Utils; 48 import jdk.test.lib.containers.docker.Common; 49 import jdk.test.lib.containers.docker.DockerRunOptions; 50 import jdk.test.lib.containers.docker.DockerTestUtils; 51 import jdk.test.lib.process.OutputAnalyzer; 52 53 54 public class TestJcmdWithSideCar { 55 private static final String IMAGE_NAME = Common.imageName("jfr-jcmd"); 56 private static final int TIME_TO_RUN_MAIN_PROCESS = (int) (30 * Utils.TIMEOUT_FACTOR); // seconds 57 private static final String MAIN_CONTAINER_NAME = "test-container-main"; 58 59 public static void main(String[] args) throws Exception { 60 if (!DockerTestUtils.canTestDocker()) { 61 return; 62 } 63 64 DockerTestUtils.buildJdkDockerImage(IMAGE_NAME, "Dockerfile-BasicTest", "jdk-docker"); 65 66 try { 67 // Start the loop process in the "main" container, then run test cases 68 // using a sidecar container. 69 DockerThread t = startMainContainer(); 70 71 waitForMainContainerToStart(500, 10); 72 t.checkForErrors(); 73 74 long mainProcPid = testCase01(); 75 76 t.assertIsAlive(); 77 78 // Excluding the test case below until JDK-8228850 is fixed 79 // JDK-8228850: jhsdb jinfo fails with ClassCastException: 80 // s.j.h.oops.TypeArray cannot be cast to s.j.h.oops.Instance 81 // testCase02(mainProcPid); 82 83 // JCMD does not work in sidecar configuration, except for "jcmd -l". 84 // Including this test case to assist in reproduction of the problem. 85 // t.assertIsAlive(); 86 // testCase03(mainProcPid); 87 88 t.join(TIME_TO_RUN_MAIN_PROCESS * 1000); 89 t.checkForErrors(); 90 } finally { 91 DockerTestUtils.removeDockerImage(IMAGE_NAME); 92 } 93 } 94 95 96 // Run "jcmd -l" in a sidecar container, find a process that runs EventGeneratorLoop and 97 // return it's PID. Try several times. It is possible that container and JVM has started, 98 // but but the JVM has not loaded main class yet. In such case, JCMD reports 99 // "Unknown" as the name of the main class, but exits without failure. That is OK, just 100 // try again after a short delay. 101 private static long testCase01() throws Exception { 102 long pid = -1; 103 for (int i=1; i<5; i++) { 104 OutputAnalyzer out = runSideCar(MAIN_CONTAINER_NAME, "/jdk/bin/jcmd", "-l") 105 .shouldHaveExitValue(0) 106 .shouldContain("sun.tools.jcmd.JCmd"); 107 pid = findProcess(out, "EventGeneratorLoop"); 108 if (pid != -1) { 109 break; 110 } 111 try { 112 Thread.sleep(500); 113 } catch (InterruptedException e) {} 114 } 115 116 if (pid == -1) { 117 throw new RuntimeException("Could not find the target process"); 118 } 119 120 return pid; 121 } 122 123 // run jhsdb jinfo <PID> (jhsdb uses PTRACE) 124 private static void testCase02(long pid) throws Exception { 125 runSideCar(MAIN_CONTAINER_NAME, "/jdk/bin/jhsdb", "jinfo", "--pid", "" + pid) 126 .shouldHaveExitValue(0) 127 .shouldContain("Java System Properties") 128 .shouldContain("VM Flags"); 129 } 130 131 // test jcmd with some commands (help, start JFR recording) 132 // JCMD will use signal mechanism and Unix Socket 133 private static void testCase03(long pid) throws Exception { 134 runSideCar(MAIN_CONTAINER_NAME, "/jdk/bin/jcmd", "" + pid, "help") 135 .shouldHaveExitValue(0) 136 .shouldContain("VM.version"); 137 runSideCar(MAIN_CONTAINER_NAME, "/jdk/bin/jcmd", "" + pid, "JFR.start") 138 .shouldHaveExitValue(0) 139 .shouldContain("Started recording"); 140 } 141 142 private static DockerThread startMainContainer() throws Exception { 143 // start "main" container (the observee) 144 DockerRunOptions opts = commonDockerOpts("EventGeneratorLoop"); 145 opts.addDockerOpts("--cap-add=SYS_PTRACE") 146 .addDockerOpts("--name", MAIN_CONTAINER_NAME) 147 .addDockerOpts("-v", "/tmp") 148 .addJavaOpts("-XX:+UsePerfData") 149 .addClassOptions("" + TIME_TO_RUN_MAIN_PROCESS); 150 DockerThread t = new DockerThread(opts); 151 t.start(); 152 153 return t; 154 } 155 156 private static void waitForMainContainerToStart(int delayMillis, int count) throws Exception { 157 boolean started = false; 158 for(int i=0; i < count; i++) { 159 try { 160 Thread.sleep(delayMillis); 161 } catch (InterruptedException e) {} 162 if (isMainContainerRunning()) { 163 started = true; 164 break; 165 } 166 } 167 if (!started) { 168 throw new RuntimeException("Main container did not start"); 169 } 170 } 171 172 private static boolean isMainContainerRunning() throws Exception { 173 OutputAnalyzer out = 174 DockerTestUtils.execute(Container.ENGINE_COMMAND, 175 "ps", "--no-trunc", 176 "--filter", "name=" + MAIN_CONTAINER_NAME); 177 return out.getStdout().contains(MAIN_CONTAINER_NAME); 178 } 179 180 // JCMD relies on the attach mechanism (com.sun.tools.attach), 181 // which in turn relies on JVMSTAT mechanism, which puts its mapped 182 // buffers in /tmp directory (hsperfdata_<user>). Thus, in sidecar 183 // we mount /tmp via --volumes-from from the main container. 184 private static OutputAnalyzer runSideCar(String MAIN_CONTAINER_NAME, String whatToRun, 185 String... args) throws Exception { 186 List<String> cmd = new ArrayList<>(); 187 String[] command = new String[] { 188 Container.ENGINE_COMMAND, "run", 189 "--tty=true", "--rm", 190 "--cap-add=SYS_PTRACE", "--sig-proxy=true", 191 "--pid=container:" + MAIN_CONTAINER_NAME, 192 "--volumes-from", MAIN_CONTAINER_NAME, 193 IMAGE_NAME, whatToRun 194 }; 195 196 cmd.addAll(Arrays.asList(command)); 197 cmd.addAll(Arrays.asList(args)); 198 return DockerTestUtils.execute(cmd); 199 } 200 201 // will return -1 if matching process could not be found 202 private static long findProcess(OutputAnalyzer out, String name) throws Exception { 203 List<String> l = out.asLines() 204 .stream() 205 .filter(s -> s.contains(name)) 206 .collect(Collectors.toList()); 207 if (l.isEmpty()) { 208 return -1; 209 } 210 String psInfo = l.get(0); 211 System.out.println("findProcess(): psInfo: " + psInfo); 212 String pid = psInfo.substring(0, psInfo.indexOf(' ')); 213 System.out.println("findProcess(): pid: " + pid); 214 return Long.parseLong(pid); 215 } 216 217 private static DockerRunOptions commonDockerOpts(String className) { 218 return new DockerRunOptions(IMAGE_NAME, "/jdk/bin/java", className) 219 .addDockerOpts("--volume", Utils.TEST_CLASSES + ":/test-classes/") 220 .addJavaOpts("-cp", "/test-classes/"); 221 } 222 223 224 static class DockerThread extends Thread { 225 DockerRunOptions runOpts; 226 Exception exception; 227 228 DockerThread(DockerRunOptions opts) { 229 runOpts = opts; 230 } 231 232 public void run() { 233 try { 234 DockerTestUtils.dockerRunJava(runOpts); 235 } catch (Exception e) { 236 exception = e; 237 } 238 } 239 240 public void assertIsAlive() throws Exception { 241 if (!isAlive()) { 242 throw new RuntimeException("DockerThread stopped unexpectedly"); 243 } 244 } 245 246 public void checkForErrors() throws Exception { 247 if (exception != null) { 248 throw new RuntimeException("DockerThread threw exception" 249 + exception.getMessage()); 250 } 251 } 252 } 253 254 }