1/* 2 * Copyright (c) 2013, 2017, 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 24import java.io.BufferedReader; 25import java.io.File; 26import java.io.IOException; 27import java.io.InputStreamReader; 28import java.nio.file.Files; 29import java.nio.file.Path; 30import java.nio.file.Paths; 31import java.security.Permission; 32import java.security.Principal; 33import java.util.ArrayList; 34import java.util.Arrays; 35import java.util.Base64; 36import java.util.Collections; 37import java.util.HashMap; 38import java.util.List; 39import java.util.Map; 40import java.util.Map.Entry; 41import java.util.stream.Collectors; 42import java.util.stream.Stream; 43 44/** 45 * This is a test library that makes writing a Java test that spawns multiple 46 * Java processes easily. 47 * 48 * Usage: 49 * 50 * Proc.create("Clazz") // The class to launch 51 * .args("x") // with args 52 * .env("env", "value") // and an environment variable 53 * .prop("key","value") // and a system property 54 * .grant(file) // grant codes in this codebase 55 * .perm(perm) // with the permission 56 * .start(); // and start 57 * 58 * create/start must be called, args/env/prop/perm can be called zero or 59 * multiple times between create and start. 60 * 61 * The controller can call inheritIO to share its I/O to the process. 62 * Otherwise, it can send data into a proc's stdin with write/println, and 63 * read its stdout with readLine. stderr is always redirected to a file 64 * unless nodump() is called. A protocol is designed to make 65 * data exchange among the controller and the processes super easy, in which 66 * useful data are always printed with a special prefix ("PROCISFUN:"). 67 * If the data is binary, make it BASE64. 68 * 69 * For example: 70 * 71 * - A producer Proc calls Proc.binOut() or Proc.textOut() to send out data. 72 * This method would prints to the stdout something like 73 * 74 * PROCISFUN:[raw text or base64 binary] 75 * 76 * - The controller calls producer.readData() to get the content. This method 77 * ignores all other output and only reads lines starting with "PROCISFUN:". 78 * 79 * - The controller does not care if the context is text or base64, it simply 80 * feeds the data to a consumer Proc by calling consumer.println(data). 81 * This will be printed into System.in of the consumer process. 82 * 83 * - The consumer Proc calls Proc.binIn() or Proc.textIn() to read the data. 84 * The first method de-base64 the input and return a byte[] block. 85 * 86 * Please note only plain ASCII is supported in raw text at the moment. 87 * 88 * As the Proc objects are hidden so deeply, two static methods, d(String) and 89 * d(Throwable) are provided to output info into stderr, where they will 90 * normally be appended messages to a debug file (unless nodump() is called). 91 * Developers can view the messages in real time by calling 92 * 93 * {@code tail -f stderr.<debug>} 94 * 95 * TODO: 96 * 97 * . launch java tools, say, keytool 98 * . launch another version of java 99 * . start in another directory 100 * . start and finish using one method 101 * 102 * This is not a test, but is the core of 103 * JDK-8009977: A test library to launch multiple Java processes 104 */ 105public class Proc { 106 private Process p; 107 private BufferedReader br; // the stdout of a process 108 private String launcher; // Optional: the java program 109 110 private List<String> args = new ArrayList<>(); 111 private Map<String,String> env = new HashMap<>(); 112 private Map<String,String> prop = new HashMap(); 113 private boolean inheritIO = false; 114 private boolean noDump = false; 115 116 private List<String> cp; // user-provided classpath 117 private String clazz; // Class to launch 118 private String debug; // debug flag, controller will show data 119 // transfer between procs. If debug is set, 120 // it MUST be different between Procs. 121 122 final private static String PREFIX = "PROCISFUN:"; 123 124 // policy file 125 final private StringBuilder perms = new StringBuilder(); 126 // temporary saving the grant line in a policy file 127 final private StringBuilder grant = new StringBuilder(); 128 129 // The following methods are called by controllers 130 131 // Creates a Proc by the Java class name, launcher is an optional 132 // argument to specify the java program 133 public static Proc create(String clazz, String... launcher) { 134 Proc pc = new Proc(); 135 pc.clazz = clazz; 136 if (launcher.length > 0) { 137 pc.launcher = launcher[0]; 138 } 139 return pc; 140 } 141 // Sets inheritIO flag to proc. If set, proc will same I/O channels as 142 // teh controller. Otherwise, its stdin/stdout is untouched, and its 143 // stderr is redirected to DFILE. 144 public Proc inheritIO() { 145 inheritIO = true; 146 return this; 147 } 148 // When called, stderr inherits parent stderr, otherwise, append to a file 149 public Proc nodump() { 150 noDump = true; 151 return this; 152 } 153 // Specifies some args. Can be called multiple times. 154 public Proc args(String... args) { 155 for (String c: args) { 156 this.args.add(c); 157 } 158 return this; 159 } 160 // Returns debug prefix 161 public String debug() { 162 return debug; 163 } 164 // Enables debug with prefix 165 public Proc debug(String title) { 166 debug = title; 167 return this; 168 } 169 // Specifies an env var. Can be called multiple times. 170 public Proc env(String a, String b) { 171 env.put(a, b); 172 return this; 173 } 174 // Specifies a Java system property. Can be called multiple times. 175 public Proc prop(String a, String b) { 176 prop.put(a, b); 177 return this; 178 } 179 // Inherit the value of a system property 180 public Proc inheritProp(String k) { 181 String v = System.getProperty(k); 182 if (v != null) { 183 prop.put(k, v); 184 } 185 return this; 186 } 187 // Sets classpath. If not called, Proc will choose a classpath. If called 188 // with no arg, no classpath will be used. Can be called multiple times. 189 public Proc cp(String... s) { 190 if (cp == null) { 191 cp = new ArrayList<>(); 192 } 193 cp.addAll(Arrays.asList(s)); 194 return this; 195 } 196 // Adds a permission to policy. Can be called multiple times. 197 // All perm() calls after a series of grant() calls are grouped into 198 // a single grant block. perm() calls before any grant() call are grouped 199 // into a grant block with no restriction. 200 // Please note that in order to make permissions effective, also call 201 // prop("java.security.manager", ""). 202 public Proc perm(Permission p) { 203 if (grant.length() != 0) { // Right after grant(s) 204 if (perms.length() != 0) { // Not first block 205 perms.append("};\n"); 206 } 207 perms.append("grant ").append(grant).append(" {\n"); 208 grant.setLength(0); 209 } else { 210 if (perms.length() == 0) { // First block w/o restriction 211 perms.append("grant {\n"); 212 } 213 } 214 if (p.getActions().isEmpty()) { 215 String s = String.format("%s \"%s\"", 216 p.getClass().getCanonicalName(), 217 p.getName() 218 .replace("\\", "\\\\").replace("\"", "\\\"")); 219 perms.append(" permission ").append(s).append(";\n"); 220 } else { 221 String s = String.format("%s \"%s\", \"%s\"", 222 p.getClass().getCanonicalName(), 223 p.getName() 224 .replace("\\", "\\\\").replace("\"", "\\\""), 225 p.getActions()); 226 perms.append(" permission ").append(s).append(";\n"); 227 } 228 return this; 229 } 230 231 // Adds a grant option to policy. If called in a row, a single grant block 232 // with all options will be created. If there are perm() call(s) between 233 // grant() calls, they belong to different grant blocks 234 235 // grant on a principal 236 public Proc grant(Principal p) { 237 grant.append("principal ").append(p.getClass().getName()) 238 .append(" \"").append(p.getName()).append("\", "); 239 return this; 240 } 241 // grant on a codebase 242 public Proc grant(File f) { 243 grant.append("codebase \"").append(f.toURI()).append("\", "); 244 return this; 245 } 246 // arbitrary grant 247 public Proc grant(String v) { 248 grant.append(v).append(", "); 249 return this; 250 } 251 // Starts the proc 252 public Proc start() throws IOException { 253 List<String> cmd = new ArrayList<>(); 254 boolean hasModules; 255 if (launcher != null) { 256 cmd.add(launcher); 257 File base = new File(launcher).getParentFile().getParentFile(); 258 hasModules = new File(base, "modules").exists() || 259 new File(base, "jmods").exists(); 260 } else { 261 cmd.add(new File(new File(System.getProperty("java.home"), "bin"), 262 "java").getPath()); 263 hasModules = true; 264 } 265 266 if (hasModules) { 267 Stream.of(jdk.internal.misc.VM.getRuntimeArguments()) 268 .filter(arg -> arg.startsWith("--add-exports=") || 269 arg.startsWith("--add-opens=")) 270 .forEach(cmd::add); 271 } 272 273 Collections.addAll(cmd, splitProperty("test.vm.opts")); 274 Collections.addAll(cmd, splitProperty("test.java.opts")); 275 276 if (cp == null) { 277 cmd.add("-cp"); 278 cmd.add(System.getProperty("test.class.path") + File.pathSeparator + 279 System.getProperty("test.src.path")); 280 } else if (!cp.isEmpty()) { 281 cmd.add("-cp"); 282 cmd.add(cp.stream().collect(Collectors.joining(File.pathSeparator))); 283 } 284 285 for (Entry<String,String> e: prop.entrySet()) { 286 cmd.add("-D" + e.getKey() + "=" + e.getValue()); 287 } 288 if (perms.length() > 0) { 289 Path p = Paths.get(getId("policy")).toAbsolutePath(); 290 perms.append("};\n"); 291 Files.write(p, perms.toString().getBytes()); 292 cmd.add("-Djava.security.policy=" + p.toString()); 293 } 294 cmd.add(clazz); 295 for (String s: args) { 296 cmd.add(s); 297 } 298 if (debug != null) { 299 System.out.println("PROC: " + debug + " cmdline: " + cmd); 300 for (String c : cmd) { 301 if (c.indexOf('\\') >= 0 || c.indexOf(' ') > 0) { 302 System.out.print('\'' + c + '\''); 303 } else { 304 System.out.print(c); 305 } 306 System.out.print(' '); 307 } 308 System.out.println(); 309 } 310 ProcessBuilder pb = new ProcessBuilder(cmd); 311 for (Entry<String,String> e: env.entrySet()) { 312 pb.environment().put(e.getKey(), e.getValue()); 313 } 314 if (inheritIO) { 315 pb.inheritIO(); 316 } else if (noDump) { 317 pb.redirectError(ProcessBuilder.Redirect.INHERIT); 318 } else { 319 pb.redirectError(ProcessBuilder.Redirect 320 .appendTo(new File(getId("stderr")))); 321 } 322 p = pb.start(); 323 br = new BufferedReader(new InputStreamReader(p.getInputStream())); 324 return this; 325 } 326 String getId(String prefix) { 327 if (debug != null) return prefix + "." + debug; 328 else return prefix + "." + System.identityHashCode(this); 329 } 330 // Reads a line from stdout of proc 331 public String readLine() throws IOException { 332 String s = br.readLine(); 333 if (debug != null) { 334 System.out.println("PROC: " + debug + " readline: " + 335 (s == null ? "<EOF>" : s)); 336 } 337 return s; 338 } 339 // Reads a special line from stdout of proc 340 public String readData() throws Exception { 341 while (true) { 342 String s = readLine(); 343 if (s == null) { 344 if (p.waitFor() != 0) { 345 throw new Exception("Proc abnormal end"); 346 } else { 347 return s; 348 } 349 } 350 if (s.startsWith(PREFIX)) { 351 return s.substring(PREFIX.length()); 352 } 353 } 354 } 355 // Writes text into stdin of proc 356 public void println(String s) throws IOException { 357 if (debug != null) { 358 System.out.println("PROC: " + debug + " println: " + s); 359 } 360 write((s + "\n").getBytes()); 361 } 362 // Writes data into stdin of proc 363 public void write(byte[] b) throws IOException { 364 p.getOutputStream().write(b); 365 p.getOutputStream().flush(); 366 } 367 // Reads all output and wait for process end 368 public int waitFor() throws Exception { 369 while (true) { 370 String s = readLine(); 371 if (s == null) { 372 break; 373 } 374 } 375 return p.waitFor(); 376 } 377 378 // The following methods are used inside a proc 379 380 // Writes out a BASE64 binary with a prefix 381 public static void binOut(byte[] data) { 382 System.out.println(PREFIX + Base64.getEncoder().encodeToString(data)); 383 } 384 // Reads in a line of BASE64 binary 385 public static byte[] binIn() throws Exception { 386 return Base64.getDecoder().decode(textIn()); 387 } 388 // Writes out a text with a prefix 389 public static void textOut(String data) { 390 System.out.println(PREFIX + data); 391 } 392 // Reads in a line of text 393 public static String textIn() throws Exception { 394 StringBuilder sb = new StringBuilder(); 395 boolean isEmpty = true; 396 while (true) { 397 int i = System.in.read(); 398 if (i == -1) break; 399 isEmpty = false; 400 if (i == '\n') break; 401 if (i != 13) { 402 // Force it to a char, so only simple ASCII works. 403 sb.append((char)i); 404 } 405 } 406 return isEmpty ? null : sb.toString(); 407 } 408 // Sends string to stderr. If inheritIO is not called, they will 409 // be collected into DFILE 410 public static void d(String s) throws IOException { 411 System.err.println(s); 412 } 413 // Sends an exception to stderr 414 public static void d(Throwable e) throws IOException { 415 e.printStackTrace(); 416 } 417 418 private static String[] splitProperty(String prop) { 419 String s = System.getProperty(prop); 420 if (s == null || s.trim().isEmpty()) { 421 return new String[] {}; 422 } 423 return s.trim().split("\\s+"); 424 } 425} 426