ScriptingFunctions.java revision 1407:ad3c4bdf0cf6
1/*
2 * Copyright (c) 2010, 2015, 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.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.nashorn.internal.runtime;
27
28import static jdk.nashorn.internal.lookup.Lookup.MH;
29import static jdk.nashorn.internal.runtime.ECMAErrors.rangeError;
30import static jdk.nashorn.internal.runtime.ECMAErrors.typeError;
31import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED;
32
33import java.io.BufferedReader;
34import java.io.File;
35import java.io.IOException;
36import java.io.InputStreamReader;
37import java.io.OutputStreamWriter;
38import java.io.StreamTokenizer;
39import java.io.StringReader;
40import java.lang.invoke.MethodHandle;
41import java.lang.invoke.MethodHandles;
42import java.util.ArrayList;
43import java.util.Arrays;
44import java.util.List;
45import java.util.Map;
46import jdk.nashorn.internal.objects.NativeArray;
47
48/**
49 * Global functions supported only in scripting mode.
50 */
51public final class ScriptingFunctions {
52
53    /** Handle to implementation of {@link ScriptingFunctions#readLine} - Nashorn extension */
54    public static final MethodHandle READLINE = findOwnMH("readLine", Object.class, Object.class, Object.class);
55
56    /** Handle to implementation of {@link ScriptingFunctions#readFully} - Nashorn extension */
57    public static final MethodHandle READFULLY = findOwnMH("readFully",     Object.class, Object.class, Object.class);
58
59    /** Handle to implementation of {@link ScriptingFunctions#exec} - Nashorn extension */
60    public static final MethodHandle EXEC = findOwnMH("exec",     Object.class, Object.class, Object[].class);
61
62    /** EXEC name - special property used by $EXEC API. */
63    public static final String EXEC_NAME = "$EXEC";
64
65    /** OUT name - special property used by $EXEC API. */
66    public static final String OUT_NAME  = "$OUT";
67
68    /** ERR name - special property used by $EXEC API. */
69    public static final String ERR_NAME  = "$ERR";
70
71    /** EXIT name - special property used by $EXEC API. */
72    public static final String EXIT_NAME = "$EXIT";
73
74    /** THROW_ON_ERROR name - special property of the $EXEC function used by $EXEC API. */
75    public static final String THROW_ON_ERROR_NAME = "throwOnError";
76
77    /** Names of special properties used by $ENV API. */
78    public  static final String ENV_NAME  = "$ENV";
79
80    /** Name of the environment variable for the current working directory. */
81    public static final String PWD_NAME  = "PWD";
82
83    private ScriptingFunctions() {
84    }
85
86    /**
87     * Nashorn extension: global.readLine (scripting-mode-only)
88     * Read one line of input from the standard input.
89     *
90     * @param self   self reference
91     * @param prompt String used as input prompt
92     *
93     * @return line that was read
94     *
95     * @throws IOException if an exception occurs
96     */
97    public static Object readLine(final Object self, final Object prompt) throws IOException {
98        if (prompt != UNDEFINED) {
99            System.out.print(JSType.toString(prompt));
100        }
101        final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
102        return reader.readLine();
103    }
104
105    /**
106     * Nashorn extension: Read the entire contents of a text file and return as String.
107     *
108     * @param self self reference
109     * @param file The input file whose content is read.
110     *
111     * @return String content of the input file.
112     *
113     * @throws IOException if an exception occurs
114     */
115    public static Object readFully(final Object self, final Object file) throws IOException {
116        File f = null;
117
118        if (file instanceof File) {
119            f = (File)file;
120        } else if (JSType.isString(file)) {
121            f = new java.io.File(((CharSequence)file).toString());
122        }
123
124        if (f == null || !f.isFile()) {
125            throw typeError("not.a.file", ScriptRuntime.safeToString(file));
126        }
127
128        return new String(Source.readFully(f));
129    }
130
131    /**
132     * Nashorn extension: exec a string in a separate process.
133     *
134     * @param self   self reference
135     * @param args   string to execute, input and additional arguments, to be appended to {@code string}. Additional
136     *               arguments can be passed as either one JavaScript array, whose elements will be converted to
137     *               strings; or as a sequence of varargs, each of which will be converted to a string.
138     *
139     * @return output string from the request
140     *
141     * @throws IOException           if any stream access fails
142     * @throws InterruptedException  if execution is interrupted
143     */
144    public static Object exec(final Object self, final Object... args) throws IOException, InterruptedException {
145        // Current global is need to fetch additional inputs and for additional results.
146        final ScriptObject global = Context.getGlobal();
147        final Object string = args.length > 0? args[0] : UNDEFINED;
148        final Object input = args.length > 1? args[1] : UNDEFINED;
149        final Object[] argv = (args.length > 2)? Arrays.copyOfRange(args, 2, args.length) : ScriptRuntime.EMPTY_ARRAY;
150        // Assemble command line, process additional arguments.
151        final List<String> cmdLine = tokenizeString(JSType.toString(string));
152        final Object[] additionalArgs = argv.length == 1 && argv[0] instanceof NativeArray ?
153                ((NativeArray) argv[0]).asObjectArray() :
154                argv;
155        for (Object arg : additionalArgs) {
156            cmdLine.add(JSType.toString(arg));
157        }
158
159        // Set up initial process.
160        final ProcessBuilder processBuilder = new ProcessBuilder(cmdLine);
161
162        // Current ENV property state.
163        final Object env = global.get(ENV_NAME);
164        if (env instanceof ScriptObject) {
165            final ScriptObject envProperties = (ScriptObject)env;
166
167            // If a working directory is present, use it.
168            final Object pwd = envProperties.get(PWD_NAME);
169            if (pwd != UNDEFINED) {
170                final File pwdFile = new File(JSType.toString(pwd));
171                if (pwdFile.exists()) {
172                    processBuilder.directory(pwdFile);
173                }
174            }
175
176            // Set up ENV variables.
177            final Map<String, String> environment = processBuilder.environment();
178            environment.clear();
179            for (final Map.Entry<Object, Object> entry : envProperties.entrySet()) {
180                environment.put(JSType.toString(entry.getKey()), JSType.toString(entry.getValue()));
181            }
182        }
183
184        // Start the process.
185        final Process process = processBuilder.start();
186        final IOException exception[] = new IOException[2];
187
188        // Collect output.
189        final StringBuilder outBuffer = new StringBuilder();
190        final Thread outThread = new Thread(new Runnable() {
191            @Override
192            public void run() {
193                final char buffer[] = new char[1024];
194                try (final InputStreamReader inputStream = new InputStreamReader(process.getInputStream())) {
195                    for (int length; (length = inputStream.read(buffer, 0, buffer.length)) != -1; ) {
196                        outBuffer.append(buffer, 0, length);
197                    }
198                } catch (final IOException ex) {
199                    exception[0] = ex;
200                }
201            }
202        }, "$EXEC output");
203
204        // Collect errors.
205        final StringBuilder errBuffer = new StringBuilder();
206        final Thread errThread = new Thread(new Runnable() {
207            @Override
208            public void run() {
209                final char buffer[] = new char[1024];
210                try (final InputStreamReader inputStream = new InputStreamReader(process.getErrorStream())) {
211                    for (int length; (length = inputStream.read(buffer, 0, buffer.length)) != -1; ) {
212                        errBuffer.append(buffer, 0, length);
213                    }
214                } catch (final IOException ex) {
215                    exception[1] = ex;
216                }
217            }
218        }, "$EXEC error");
219
220        // Start gathering output.
221        outThread.start();
222        errThread.start();
223
224        // If input is present, pass on to process.
225        if (!JSType.nullOrUndefined(input)) {
226            try (OutputStreamWriter outputStream = new OutputStreamWriter(process.getOutputStream())) {
227                final String in = JSType.toString(input);
228                outputStream.write(in, 0, in.length());
229            } catch (final IOException ex) {
230                // Process was not expecting input.  May be normal state of affairs.
231            }
232        }
233
234        // Wait for the process to complete.
235        final int exit = process.waitFor();
236        outThread.join();
237        errThread.join();
238
239        final String out = outBuffer.toString();
240        final String err = errBuffer.toString();
241
242        // Set globals for secondary results.
243        global.set(OUT_NAME, out, 0);
244        global.set(ERR_NAME, err, 0);
245        global.set(EXIT_NAME, exit, 0);
246
247        // Propagate exception if present.
248        for (final IOException element : exception) {
249            if (element != null) {
250                throw element;
251            }
252        }
253
254        // if we got a non-zero exit code ("failure"), then we have to decide to throw error or not
255        if (exit != 0) {
256            // get the $EXEC function object from the global object
257            final Object exec = global.get(EXEC_NAME);
258            assert exec instanceof ScriptObject : EXEC_NAME + " is not a script object!";
259
260            // Check if the user has set $EXEC.throwOnError property to true. If so, throw RangeError
261            // If that property is not set or set to false, then silently proceed with the rest.
262            if (JSType.toBoolean(((ScriptObject)exec).get(THROW_ON_ERROR_NAME))) {
263                throw rangeError("exec.returned.non.zero", ScriptRuntime.safeToString(exit));
264            }
265        }
266
267        // Return the result from stdout.
268        return out;
269    }
270
271    private static MethodHandle findOwnMH(final String name, final Class<?> rtype, final Class<?>... types) {
272        return MH.findStatic(MethodHandles.lookup(), ScriptingFunctions.class, name, MH.type(rtype, types));
273    }
274
275    /**
276     * Break a string into tokens, honoring quoted arguments and escaped spaces.
277     *
278     * @param str a {@link String} to tokenize.
279     * @return a {@link List} of {@link String}s representing the tokens that
280     * constitute the string.
281     * @throws IOException in case {@link StreamTokenizer#nextToken()} raises it.
282     */
283    public static List<String> tokenizeString(final String str) throws IOException {
284        final StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(str));
285        tokenizer.resetSyntax();
286        tokenizer.wordChars(0, 255);
287        tokenizer.whitespaceChars(0, ' ');
288        tokenizer.commentChar('#');
289        tokenizer.quoteChar('"');
290        tokenizer.quoteChar('\'');
291        final List<String> tokenList = new ArrayList<>();
292        final StringBuilder toAppend = new StringBuilder();
293        while (tokenizer.nextToken() != StreamTokenizer.TT_EOF) {
294            final String s = tokenizer.sval;
295            // The tokenizer understands about honoring quoted strings and recognizes
296            // them as one token that possibly contains multiple space-separated words.
297            // It does not recognize quoted spaces, though, and will split after the
298            // escaping \ character. This is handled here.
299            if (s.endsWith("\\")) {
300                // omit trailing \, append space instead
301                toAppend.append(s.substring(0, s.length() - 1)).append(' ');
302            } else {
303                tokenList.add(toAppend.append(s).toString());
304                toAppend.setLength(0);
305            }
306        }
307        if (toAppend.length() != 0) {
308            tokenList.add(toAppend.toString());
309        }
310        return tokenList;
311    }
312}
313