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