Merge
This commit is contained in:
commit
348cbe8b0a
nashorn
src/jdk.scripting.nashorn/share/classes/jdk/nashorn
internal
codegen/types
objects
runtime
tools
test
script
src/jdk/nashorn/internal/test/framework
9
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/codegen/types/BooleanType.java
9
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/codegen/types/BooleanType.java
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2010, 2013, 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
|
||||
@ -134,8 +134,11 @@ public final class BooleanType extends Type {
|
||||
@Override
|
||||
public Type add(final MethodVisitor method, final int programPoint) {
|
||||
// Adding booleans in JavaScript is perfectly valid, they add as if false=0 and true=1
|
||||
assert programPoint == INVALID_PROGRAM_POINT;
|
||||
method.visitInsn(IADD);
|
||||
if(programPoint == INVALID_PROGRAM_POINT) {
|
||||
method.visitInsn(IADD);
|
||||
} else {
|
||||
method.visitInvokeDynamicInsn("iadd", "(II)I", MATHBOOTSTRAP, programPoint);
|
||||
}
|
||||
return INT;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2010, 2013, 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
|
||||
@ -155,8 +155,7 @@ class IntType extends BitwiseType {
|
||||
if(programPoint == INVALID_PROGRAM_POINT) {
|
||||
method.visitInsn(IADD);
|
||||
} else {
|
||||
ldc(method, programPoint);
|
||||
JSType.ADD_EXACT.invoke(method);
|
||||
method.visitInvokeDynamicInsn("iadd", "(II)I", MATHBOOTSTRAP, programPoint);
|
||||
}
|
||||
return INT;
|
||||
}
|
||||
@ -215,8 +214,7 @@ class IntType extends BitwiseType {
|
||||
if(programPoint == INVALID_PROGRAM_POINT) {
|
||||
method.visitInsn(ISUB);
|
||||
} else {
|
||||
ldc(method, programPoint);
|
||||
JSType.SUB_EXACT.invoke(method);
|
||||
method.visitInvokeDynamicInsn("isub", "(II)I", MATHBOOTSTRAP, programPoint);
|
||||
}
|
||||
return INT;
|
||||
}
|
||||
@ -226,8 +224,7 @@ class IntType extends BitwiseType {
|
||||
if(programPoint == INVALID_PROGRAM_POINT) {
|
||||
method.visitInsn(IMUL);
|
||||
} else {
|
||||
ldc(method, programPoint);
|
||||
JSType.MUL_EXACT.invoke(method);
|
||||
method.visitInvokeDynamicInsn("imul", "(II)I", MATHBOOTSTRAP, programPoint);
|
||||
}
|
||||
return INT;
|
||||
}
|
||||
@ -237,8 +234,7 @@ class IntType extends BitwiseType {
|
||||
if (programPoint == INVALID_PROGRAM_POINT) {
|
||||
JSType.DIV_ZERO.invoke(method);
|
||||
} else {
|
||||
ldc(method, programPoint);
|
||||
JSType.DIV_EXACT.invoke(method);
|
||||
method.visitInvokeDynamicInsn("idiv", "(II)I", MATHBOOTSTRAP, programPoint);
|
||||
}
|
||||
return INT;
|
||||
}
|
||||
@ -248,8 +244,7 @@ class IntType extends BitwiseType {
|
||||
if (programPoint == INVALID_PROGRAM_POINT) {
|
||||
JSType.REM_ZERO.invoke(method);
|
||||
} else {
|
||||
ldc(method, programPoint);
|
||||
JSType.REM_EXACT.invoke(method);
|
||||
method.visitInvokeDynamicInsn("irem", "(II)I", MATHBOOTSTRAP, programPoint);
|
||||
}
|
||||
return INT;
|
||||
}
|
||||
@ -259,8 +254,7 @@ class IntType extends BitwiseType {
|
||||
if(programPoint == INVALID_PROGRAM_POINT) {
|
||||
method.visitInsn(INEG);
|
||||
} else {
|
||||
ldc(method, programPoint);
|
||||
JSType.NEGATE_EXACT.invoke(method);
|
||||
method.visitInvokeDynamicInsn("ineg", "(I)I", MATHBOOTSTRAP, programPoint);
|
||||
}
|
||||
return INT;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2010, 2013, 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
|
||||
@ -27,6 +27,7 @@ package jdk.nashorn.internal.codegen.types;
|
||||
|
||||
import static jdk.internal.org.objectweb.asm.Opcodes.L2D;
|
||||
import static jdk.internal.org.objectweb.asm.Opcodes.L2I;
|
||||
import static jdk.internal.org.objectweb.asm.Opcodes.LADD;
|
||||
import static jdk.internal.org.objectweb.asm.Opcodes.LCONST_0;
|
||||
import static jdk.internal.org.objectweb.asm.Opcodes.LCONST_1;
|
||||
import static jdk.internal.org.objectweb.asm.Opcodes.LLOAD;
|
||||
@ -34,6 +35,7 @@ import static jdk.internal.org.objectweb.asm.Opcodes.LRETURN;
|
||||
import static jdk.internal.org.objectweb.asm.Opcodes.LSTORE;
|
||||
import static jdk.nashorn.internal.codegen.CompilerConstants.staticCallNoLookup;
|
||||
import static jdk.nashorn.internal.runtime.JSType.UNDEFINED_LONG;
|
||||
import static jdk.nashorn.internal.runtime.UnwarrantedOptimismException.INVALID_PROGRAM_POINT;
|
||||
|
||||
import jdk.internal.org.objectweb.asm.MethodVisitor;
|
||||
import jdk.nashorn.internal.codegen.CompilerConstants;
|
||||
@ -123,7 +125,12 @@ class LongType extends Type {
|
||||
|
||||
@Override
|
||||
public Type add(final MethodVisitor method, final int programPoint) {
|
||||
throw new UnsupportedOperationException("add");
|
||||
if(programPoint == INVALID_PROGRAM_POINT) {
|
||||
method.visitInsn(LADD);
|
||||
} else {
|
||||
method.visitInvokeDynamicInsn("ladd", "(JJ)J", MATHBOOTSTRAP, programPoint);
|
||||
}
|
||||
return LONG;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2010, 2013, 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
|
||||
@ -33,6 +33,7 @@ import static jdk.internal.org.objectweb.asm.Opcodes.DUP2_X1;
|
||||
import static jdk.internal.org.objectweb.asm.Opcodes.DUP2_X2;
|
||||
import static jdk.internal.org.objectweb.asm.Opcodes.DUP_X1;
|
||||
import static jdk.internal.org.objectweb.asm.Opcodes.DUP_X2;
|
||||
import static jdk.internal.org.objectweb.asm.Opcodes.H_INVOKESTATIC;
|
||||
import static jdk.internal.org.objectweb.asm.Opcodes.IALOAD;
|
||||
import static jdk.internal.org.objectweb.asm.Opcodes.IASTORE;
|
||||
import static jdk.internal.org.objectweb.asm.Opcodes.INVOKESTATIC;
|
||||
@ -45,22 +46,28 @@ import static jdk.internal.org.objectweb.asm.Opcodes.SWAP;
|
||||
import static jdk.internal.org.objectweb.asm.Opcodes.T_DOUBLE;
|
||||
import static jdk.internal.org.objectweb.asm.Opcodes.T_INT;
|
||||
import static jdk.internal.org.objectweb.asm.Opcodes.T_LONG;
|
||||
import static jdk.nashorn.internal.codegen.CompilerConstants.staticCallNoLookup;
|
||||
|
||||
import java.io.DataInput;
|
||||
import java.io.DataOutput;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.lang.invoke.CallSite;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import jdk.internal.org.objectweb.asm.Handle;
|
||||
import jdk.internal.org.objectweb.asm.MethodVisitor;
|
||||
import jdk.nashorn.internal.codegen.CompilerConstants.Call;
|
||||
import jdk.nashorn.internal.runtime.Context;
|
||||
import jdk.nashorn.internal.runtime.ScriptObject;
|
||||
import jdk.nashorn.internal.runtime.Undefined;
|
||||
import jdk.nashorn.internal.runtime.linker.Bootstrap;
|
||||
|
||||
/**
|
||||
* This is the representation of a JavaScript type, disassociated from java
|
||||
@ -117,6 +124,10 @@ public abstract class Type implements Comparable<Type>, BytecodeOps, Serializabl
|
||||
/** Set way below Integer.MAX_VALUE to prevent overflow when adding weights. Objects are still heaviest. */
|
||||
protected static final int MAX_WEIGHT = 20;
|
||||
|
||||
static final Call BOOTSTRAP = staticCallNoLookup(Bootstrap.class, "mathBootstrap", CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, int.class);
|
||||
|
||||
static final Handle MATHBOOTSTRAP = new Handle(H_INVOKESTATIC, BOOTSTRAP.className(), "mathBootstrap", BOOTSTRAP.descriptor());
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
|
@ -2459,8 +2459,6 @@ public final class Global extends Scope {
|
||||
|
||||
final String execName = ScriptingFunctions.EXEC_NAME;
|
||||
value = ScriptFunction.createBuiltin(execName, ScriptingFunctions.EXEC);
|
||||
value.addOwnProperty(ScriptingFunctions.THROW_ON_ERROR_NAME, Attribute.NOT_ENUMERABLE, false);
|
||||
|
||||
addOwnProperty(execName, Attribute.NOT_ENUMERABLE, value);
|
||||
|
||||
// Nashorn extension: global.echo (scripting-mode-only)
|
||||
@ -2474,10 +2472,10 @@ public final class Global extends Scope {
|
||||
addOwnProperty("$OPTIONS", Attribute.NOT_ENUMERABLE, options);
|
||||
|
||||
// Nashorn extension: global.$ENV (scripting-mode-only)
|
||||
final ScriptObject env = newObject();
|
||||
if (System.getSecurityManager() == null) {
|
||||
// do not fill $ENV if we have a security manager around
|
||||
// Retrieve current state of ENV variables.
|
||||
final ScriptObject env = newObject();
|
||||
env.putAll(System.getenv(), scriptEnv._strict);
|
||||
|
||||
// Some platforms, e.g., Windows, do not define the PWD environment
|
||||
@ -2486,11 +2484,8 @@ public final class Global extends Scope {
|
||||
if (!env.containsKey(ScriptingFunctions.PWD_NAME)) {
|
||||
env.put(ScriptingFunctions.PWD_NAME, System.getProperty("user.dir"), scriptEnv._strict);
|
||||
}
|
||||
|
||||
addOwnProperty(ScriptingFunctions.ENV_NAME, Attribute.NOT_ENUMERABLE, env);
|
||||
} else {
|
||||
addOwnProperty(ScriptingFunctions.ENV_NAME, Attribute.NOT_ENUMERABLE, UNDEFINED);
|
||||
}
|
||||
addOwnProperty(ScriptingFunctions.ENV_NAME, Attribute.NOT_ENUMERABLE, env);
|
||||
|
||||
// add other special properties for exec support
|
||||
addOwnProperty(ScriptingFunctions.OUT_NAME, Attribute.NOT_ENUMERABLE, UNDEFINED);
|
||||
|
844
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/CommandExecutor.java
Normal file
844
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/CommandExecutor.java
Normal file
@ -0,0 +1,844 @@
|
||||
/*
|
||||
* Copyright (c) 2016, 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. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package jdk.nashorn.internal.runtime;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.StreamTokenizer;
|
||||
import java.io.StringReader;
|
||||
import java.lang.ProcessBuilder.Redirect;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.AccessController;
|
||||
import java.security.PrivilegedAction;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static jdk.nashorn.internal.runtime.CommandExecutor.RedirectType.*;
|
||||
import static jdk.nashorn.internal.runtime.ECMAErrors.rangeError;
|
||||
|
||||
/**
|
||||
* The CommandExecutor class provides support for Nashorn's $EXEC
|
||||
* builtin function. CommandExecutor provides support for command parsing,
|
||||
* I/O redirection, piping, completion timeouts, # comments, and simple
|
||||
* environment variable management (cd, setenv, and unsetenv).
|
||||
*/
|
||||
class CommandExecutor {
|
||||
// Size of byte buffers used for piping.
|
||||
private static final int BUFFER_SIZE = 1024;
|
||||
|
||||
// Test to see if running on Windows.
|
||||
private static final boolean IS_WINDOWS =
|
||||
AccessController.doPrivileged((PrivilegedAction<Boolean>)() -> {
|
||||
return System.getProperty("os.name").contains("Windows");
|
||||
});
|
||||
|
||||
// User's home directory
|
||||
private static final String HOME_DIRECTORY =
|
||||
AccessController.doPrivileged((PrivilegedAction<String>)() -> {
|
||||
return System.getProperty("user.home");
|
||||
});
|
||||
|
||||
// Various types of standard redirects.
|
||||
enum RedirectType {
|
||||
NO_REDIRECT,
|
||||
REDIRECT_INPUT,
|
||||
REDIRECT_OUTPUT,
|
||||
REDIRECT_OUTPUT_APPEND,
|
||||
REDIRECT_ERROR,
|
||||
REDIRECT_ERROR_APPEND,
|
||||
REDIRECT_OUTPUT_ERROR_APPEND,
|
||||
REDIRECT_ERROR_TO_OUTPUT
|
||||
};
|
||||
|
||||
// Prefix strings to standard redirects.
|
||||
private static final String[] redirectPrefixes = new String[] {
|
||||
"<",
|
||||
"0<",
|
||||
">",
|
||||
"1>",
|
||||
">>",
|
||||
"1>>",
|
||||
"2>",
|
||||
"2>>",
|
||||
"&>",
|
||||
"2>&1"
|
||||
};
|
||||
|
||||
// Map from redirectPrefixes to RedirectType.
|
||||
private static final RedirectType[] redirects = new RedirectType[] {
|
||||
REDIRECT_INPUT,
|
||||
REDIRECT_INPUT,
|
||||
REDIRECT_OUTPUT,
|
||||
REDIRECT_OUTPUT,
|
||||
REDIRECT_OUTPUT_APPEND,
|
||||
REDIRECT_OUTPUT_APPEND,
|
||||
REDIRECT_ERROR,
|
||||
REDIRECT_ERROR_APPEND,
|
||||
REDIRECT_OUTPUT_ERROR_APPEND,
|
||||
REDIRECT_ERROR_TO_OUTPUT
|
||||
};
|
||||
|
||||
/**
|
||||
* The RedirectInfo class handles checking the next token in a command
|
||||
* to see if it contains a redirect. If the redirect file does not butt
|
||||
* against the prefix, then the next token is consumed.
|
||||
*/
|
||||
private static class RedirectInfo {
|
||||
// true if a redirect was encountered on the current command.
|
||||
private boolean hasRedirects;
|
||||
// Redirect.PIPE or an input redirect from the command line.
|
||||
private Redirect inputRedirect;
|
||||
// Redirect.PIPE or an output redirect from the command line.
|
||||
private Redirect outputRedirect;
|
||||
// Redirect.PIPE or an error redirect from the command line.
|
||||
private Redirect errorRedirect;
|
||||
// true if the error stream should be merged with output.
|
||||
private boolean mergeError;
|
||||
|
||||
RedirectInfo() {
|
||||
this.hasRedirects = false;
|
||||
this.inputRedirect = Redirect.PIPE;
|
||||
this.outputRedirect = Redirect.PIPE;
|
||||
this.errorRedirect = Redirect.PIPE;
|
||||
this.mergeError = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* check - tests to see if the current token contains a redirect
|
||||
* @param token current command line token
|
||||
* @param iterator current command line iterator
|
||||
* @param cwd current working directory
|
||||
* @return true if token is consumed
|
||||
*/
|
||||
boolean check(String token, final Iterator<String> iterator, final String cwd) {
|
||||
// Iterate through redirect prefixes to file a match.
|
||||
for (int i = 0; i < redirectPrefixes.length; i++) {
|
||||
String prefix = redirectPrefixes[i];
|
||||
|
||||
// If a match is found.
|
||||
if (token.startsWith(prefix)) {
|
||||
// Indicate we have at least one redirect (efficiency.)
|
||||
hasRedirects = true;
|
||||
// Map prefix to RedirectType.
|
||||
RedirectType redirect = redirects[i];
|
||||
// Strip prefix from token
|
||||
token = token.substring(prefix.length());
|
||||
|
||||
// Get file from either current or next token.
|
||||
File file = null;
|
||||
if (redirect != REDIRECT_ERROR_TO_OUTPUT) {
|
||||
// Nothing left of current token.
|
||||
if (token.length() == 0) {
|
||||
if (iterator.hasNext()) {
|
||||
// Use next token.
|
||||
token = iterator.next();
|
||||
} else {
|
||||
// Send to null device if not provided.
|
||||
token = IS_WINDOWS ? "NUL:" : "/dev/null";
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect file.
|
||||
file = resolvePath(cwd, token).toFile();
|
||||
}
|
||||
|
||||
// Define redirect based on prefix.
|
||||
switch (redirect) {
|
||||
case REDIRECT_INPUT:
|
||||
inputRedirect = Redirect.from(file);
|
||||
break;
|
||||
case REDIRECT_OUTPUT:
|
||||
outputRedirect = Redirect.to(file);
|
||||
break;
|
||||
case REDIRECT_OUTPUT_APPEND:
|
||||
outputRedirect = Redirect.appendTo(file);
|
||||
break;
|
||||
case REDIRECT_ERROR:
|
||||
errorRedirect = Redirect.to(file);
|
||||
break;
|
||||
case REDIRECT_ERROR_APPEND:
|
||||
errorRedirect = Redirect.appendTo(file);
|
||||
break;
|
||||
case REDIRECT_OUTPUT_ERROR_APPEND:
|
||||
outputRedirect = Redirect.to(file);
|
||||
errorRedirect = Redirect.to(file);
|
||||
mergeError = true;
|
||||
break;
|
||||
case REDIRECT_ERROR_TO_OUTPUT:
|
||||
mergeError = true;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
// Indicate token is consumed.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// No redirect found.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* apply - apply the redirects to the current ProcessBuilder.
|
||||
* @param pb current ProcessBuilder
|
||||
*/
|
||||
void apply(final ProcessBuilder pb) {
|
||||
// Only if there was redirects (saves new structure in ProcessBuilder.)
|
||||
if (hasRedirects) {
|
||||
// If output and error are the same file then merge.
|
||||
File outputFile = outputRedirect.file();
|
||||
File errorFile = errorRedirect.file();
|
||||
|
||||
if (outputFile != null && outputFile.equals(errorFile)) {
|
||||
mergeError = true;
|
||||
}
|
||||
|
||||
// Apply redirects.
|
||||
pb.redirectInput(inputRedirect);
|
||||
pb.redirectOutput(outputRedirect);
|
||||
pb.redirectError(errorRedirect);
|
||||
pb.redirectErrorStream(mergeError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Piper class is responsible for copying from an InputStream to an
|
||||
* OutputStream without blocking the current thread.
|
||||
*/
|
||||
private static class Piper implements java.lang.Runnable {
|
||||
// Stream to copy from.
|
||||
private final InputStream input;
|
||||
// Stream to copy to.
|
||||
private final OutputStream output;
|
||||
|
||||
Piper(final InputStream input, final OutputStream output) {
|
||||
this.input = input;
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
/**
|
||||
* start - start the Piper in a new daemon thread
|
||||
*/
|
||||
void start() {
|
||||
Thread thread = new Thread(this, "$EXEC Piper");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* run - thread action
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// Buffer for copying.
|
||||
byte[] b = new byte[BUFFER_SIZE];
|
||||
// Read from the InputStream until EOF.
|
||||
int read;
|
||||
while (-1 < (read = input.read(b, 0, b.length))) {
|
||||
// Write available date to OutputStream.
|
||||
output.write(b, 0, read);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Assume the worst.
|
||||
throw new RuntimeException("Broken pipe", e);
|
||||
} finally {
|
||||
// Make sure the streams are closed.
|
||||
try {
|
||||
input.close();
|
||||
} catch (IOException e) {
|
||||
// Don't care.
|
||||
}
|
||||
try {
|
||||
output.close();
|
||||
} catch (IOException e) {
|
||||
// Don't care.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exit thread.
|
||||
}
|
||||
|
||||
// Process exit statuses.
|
||||
static final int EXIT_SUCCESS = 0;
|
||||
static final int EXIT_FAILURE = 1;
|
||||
|
||||
// Copy of environment variables used by all processes.
|
||||
private Map<String, String> environment;
|
||||
// Input string if provided on CommandExecutor call.
|
||||
private String inputString;
|
||||
// Output string if required from CommandExecutor call.
|
||||
private String outputString;
|
||||
// Error string if required from CommandExecutor call.
|
||||
private String errorString;
|
||||
// Last process exit code.
|
||||
private int exitCode;
|
||||
|
||||
// Input stream if provided on CommandExecutor call.
|
||||
private InputStream inputStream;
|
||||
// Output stream if provided on CommandExecutor call.
|
||||
private OutputStream outputStream;
|
||||
// Error stream if provided on CommandExecutor call.
|
||||
private OutputStream errorStream;
|
||||
|
||||
// Ordered collection of current or piped ProcessBuilders.
|
||||
private List<ProcessBuilder> processBuilders = new ArrayList<>();
|
||||
|
||||
CommandExecutor() {
|
||||
this.environment = new HashMap<>();
|
||||
this.inputString = "";
|
||||
this.outputString = "";
|
||||
this.errorString = "";
|
||||
this.exitCode = EXIT_SUCCESS;
|
||||
this.inputStream = null;
|
||||
this.outputStream = null;
|
||||
this.errorStream = null;
|
||||
this.processBuilders = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* envVarValue - return the value of the environment variable key, or
|
||||
* deflt if not found.
|
||||
* @param key name of environment variable
|
||||
* @param deflt value to return if not found
|
||||
* @return value of the environment variable
|
||||
*/
|
||||
private String envVarValue(final String key, final String deflt) {
|
||||
return environment.getOrDefault(key, deflt);
|
||||
}
|
||||
|
||||
/**
|
||||
* envVarLongValue - return the value of the environment variable key as a
|
||||
* long value.
|
||||
* @param key name of environment variable
|
||||
* @return long value of the environment variable
|
||||
*/
|
||||
private long envVarLongValue(final String key) {
|
||||
try {
|
||||
return Long.parseLong(envVarValue(key, "0"));
|
||||
} catch (NumberFormatException ex) {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* envVarBooleanValue - return the value of the environment variable key as a
|
||||
* boolean value. true if the value was non-zero, false otherwise.
|
||||
* @param key name of environment variable
|
||||
* @return boolean value of the environment variable
|
||||
*/
|
||||
private boolean envVarBooleanValue(final String key) {
|
||||
return envVarLongValue(key) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* stripQuotes - strip quotes from token if present. Quoted tokens kept
|
||||
* quotes to prevent search for redirects.
|
||||
* @param token token to strip
|
||||
* @return stripped token
|
||||
*/
|
||||
private static String stripQuotes(String token) {
|
||||
if ((token.startsWith("\"") && token.endsWith("\"")) ||
|
||||
token.startsWith("\'") && token.endsWith("\'")) {
|
||||
token = token.substring(1, token.length() - 1);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* resolvePath - resolves a path against a current working directory.
|
||||
* @param cwd current working directory
|
||||
* @param fileName name of file or directory
|
||||
* @return resolved Path to file
|
||||
*/
|
||||
private static Path resolvePath(final String cwd, final String fileName) {
|
||||
return Paths.get(cwd).resolve(fileName).normalize();
|
||||
}
|
||||
|
||||
/**
|
||||
* builtIn - checks to see if the command is a builtin and performs
|
||||
* appropriate action.
|
||||
* @param cmd current command
|
||||
* @param cwd current working directory
|
||||
* @return true if was a builtin command
|
||||
*/
|
||||
private boolean builtIn(final List<String> cmd, final String cwd) {
|
||||
switch (cmd.get(0)) {
|
||||
// Set current working directory.
|
||||
case "cd":
|
||||
// If zero args then use home dirrectory as cwd else use first arg.
|
||||
final String newCWD = cmd.size() < 2 ? HOME_DIRECTORY : cmd.get(1);
|
||||
// Normalize the cwd
|
||||
final Path cwdPath = resolvePath(cwd, newCWD);
|
||||
|
||||
// Check if is a directory.
|
||||
final File file = cwdPath.toFile();
|
||||
if (!file.exists()) {
|
||||
reportError("file.not.exist", file.toString());
|
||||
return true;
|
||||
} else if (!file.isDirectory()) {
|
||||
reportError("not.directory", file.toString());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Set PWD environment variable to be picked up as cwd.
|
||||
environment.put("PWD", cwdPath.toString());
|
||||
return true;
|
||||
|
||||
// Set an environment variable.
|
||||
case "setenv":
|
||||
if (3 <= cmd.size()) {
|
||||
final String key = cmd.get(1);
|
||||
final String value = cmd.get(2);
|
||||
environment.put(key, value);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
// Unset an environment variable.
|
||||
case "unsetenv":
|
||||
if (2 <= cmd.size()) {
|
||||
final String key = cmd.get(1);
|
||||
environment.remove(key);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* preprocessCommand - scan the command for redirects
|
||||
* @param tokens command tokens
|
||||
* @param cwd current working directory
|
||||
* @param redirectInfo redirection information
|
||||
* @return tokens remaining for actual command
|
||||
*/
|
||||
private List<String> preprocessCommand(final List<String> tokens,
|
||||
final String cwd, final RedirectInfo redirectInfo) {
|
||||
// Tokens remaining for actual command.
|
||||
final List<String> command = new ArrayList<>();
|
||||
|
||||
// iterate through all tokens.
|
||||
final Iterator<String> iterator = tokens.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
String token = iterator.next();
|
||||
|
||||
// Check if is a redirect.
|
||||
if (redirectInfo.check(token, iterator, cwd)) {
|
||||
// Don't add to the command.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strip quotes and add to command.
|
||||
command.add(stripQuotes(token));
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* createProcessBuilder - create a ProcessBuilder for the command.
|
||||
* @param command command tokens
|
||||
* @param cwd current working directory
|
||||
* @param redirectInfo redirect information
|
||||
*/
|
||||
private void createProcessBuilder(final List<String> command,
|
||||
final String cwd, final RedirectInfo redirectInfo) {
|
||||
// Create new ProcessBuilder.
|
||||
final ProcessBuilder pb = new ProcessBuilder(command);
|
||||
// Set current working directory.
|
||||
pb.directory(new File(cwd));
|
||||
|
||||
// Map environment variables.
|
||||
final Map<String, String> processEnvironment = pb.environment();
|
||||
processEnvironment.clear();
|
||||
processEnvironment.putAll(environment);
|
||||
|
||||
// Apply redirects.
|
||||
redirectInfo.apply(pb);
|
||||
// Add to current list of commands.
|
||||
processBuilders.add(pb);
|
||||
}
|
||||
|
||||
/**
|
||||
* command - process the command
|
||||
* @param tokens tokens of the command
|
||||
* @param isPiped true if the output of this command should be piped to the next
|
||||
*/
|
||||
private void command(final List<String> tokens, boolean isPiped) {
|
||||
// Test to see if we should echo the command to output.
|
||||
if (envVarBooleanValue("JJS_ECHO")) {
|
||||
System.out.println(String.join(" ", tokens));
|
||||
}
|
||||
|
||||
// Get the current working directory.
|
||||
final String cwd = envVarValue("PWD", HOME_DIRECTORY);
|
||||
// Preprocess the command for redirects.
|
||||
final RedirectInfo redirectInfo = new RedirectInfo();
|
||||
final List<String> command = preprocessCommand(tokens, cwd, redirectInfo);
|
||||
|
||||
// Skip if empty or a built in.
|
||||
if (command.isEmpty() || builtIn(command, cwd)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create ProcessBuilder with cwd and redirects set.
|
||||
createProcessBuilder(command, cwd, redirectInfo);
|
||||
|
||||
// If piped the wait for the next command.
|
||||
if (isPiped) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch first and last ProcessBuilder.
|
||||
final ProcessBuilder firstProcessBuilder = processBuilders.get(0);
|
||||
final ProcessBuilder lastProcessBuilder = processBuilders.get(processBuilders.size() - 1);
|
||||
|
||||
// Determine which streams have not be redirected from pipes.
|
||||
boolean inputIsPipe = firstProcessBuilder.redirectInput() == Redirect.PIPE;
|
||||
boolean outputIsPipe = lastProcessBuilder.redirectOutput() == Redirect.PIPE;
|
||||
boolean errorIsPipe = lastProcessBuilder.redirectError() == Redirect.PIPE;
|
||||
boolean inheritIO = envVarBooleanValue("JJS_INHERIT_IO");
|
||||
|
||||
// If not redirected and inputStream is current processes' input.
|
||||
if (inputIsPipe && (inheritIO || inputStream == System.in)) {
|
||||
// Inherit current processes' input.
|
||||
firstProcessBuilder.redirectInput(Redirect.INHERIT);
|
||||
inputIsPipe = false;
|
||||
}
|
||||
|
||||
// If not redirected and outputStream is current processes' output.
|
||||
if (outputIsPipe && (inheritIO || outputStream == System.out)) {
|
||||
// Inherit current processes' output.
|
||||
lastProcessBuilder.redirectOutput(Redirect.INHERIT);
|
||||
outputIsPipe = false;
|
||||
}
|
||||
|
||||
// If not redirected and errorStream is current processes' error.
|
||||
if (errorIsPipe && (inheritIO || errorStream == System.err)) {
|
||||
// Inherit current processes' error.
|
||||
lastProcessBuilder.redirectError(Redirect.INHERIT);
|
||||
errorIsPipe = false;
|
||||
}
|
||||
|
||||
// Start the processes.
|
||||
final List<Process> processes = new ArrayList<>();
|
||||
for (ProcessBuilder pb : processBuilders) {
|
||||
try {
|
||||
processes.add(pb.start());
|
||||
} catch (IOException ex) {
|
||||
reportError("unknown.command", String.join(" ", pb.command()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear processBuilders for next command.
|
||||
processBuilders.clear();
|
||||
|
||||
// Get first and last process.
|
||||
final Process firstProcess = processes.get(0);
|
||||
final Process lastProcess = processes.get(processes.size() - 1);
|
||||
|
||||
// Prepare for string based i/o if no redirection or provided streams.
|
||||
ByteArrayOutputStream byteOutputStream = null;
|
||||
ByteArrayOutputStream byteErrorStream = null;
|
||||
|
||||
// If input is not redirected.
|
||||
if (inputIsPipe) {
|
||||
// If inputStream other than System.in is provided.
|
||||
if (inputStream != null) {
|
||||
// Pipe inputStream to first process output stream.
|
||||
new Piper(inputStream, firstProcess.getOutputStream()).start();
|
||||
} else {
|
||||
// Otherwise assume an input string has been provided.
|
||||
new Piper(new ByteArrayInputStream(inputString.getBytes()), firstProcess.getOutputStream()).start();
|
||||
}
|
||||
}
|
||||
|
||||
// If output is not redirected.
|
||||
if (outputIsPipe) {
|
||||
// If outputStream other than System.out is provided.
|
||||
if (outputStream != null ) {
|
||||
// Pipe outputStream from last process input stream.
|
||||
new Piper(lastProcess.getInputStream(), outputStream).start();
|
||||
} else {
|
||||
// Otherwise assume an output string needs to be prepared.
|
||||
byteOutputStream = new ByteArrayOutputStream(BUFFER_SIZE);
|
||||
new Piper(lastProcess.getInputStream(), byteOutputStream).start();
|
||||
}
|
||||
}
|
||||
|
||||
// If error is not redirected.
|
||||
if (errorIsPipe) {
|
||||
// If errorStream other than System.err is provided.
|
||||
if (errorStream != null) {
|
||||
new Piper(lastProcess.getErrorStream(), errorStream).start();
|
||||
} else {
|
||||
// Otherwise assume an error string needs to be prepared.
|
||||
byteErrorStream = new ByteArrayOutputStream(BUFFER_SIZE);
|
||||
new Piper(lastProcess.getErrorStream(), byteErrorStream).start();
|
||||
}
|
||||
}
|
||||
|
||||
// Pipe commands in between.
|
||||
for (int i = 0, n = processes.size() - 1; i < n; i++) {
|
||||
final Process prev = processes.get(i);
|
||||
final Process next = processes.get(i + 1);
|
||||
new Piper(prev.getInputStream(), next.getOutputStream()).start();
|
||||
}
|
||||
|
||||
// Wind up processes.
|
||||
try {
|
||||
// Get the user specified timeout.
|
||||
long timeout = envVarLongValue("JJS_TIMEOUT");
|
||||
|
||||
// If user specified timeout (milliseconds.)
|
||||
if (timeout != 0) {
|
||||
// Wait for last process, with timeout.
|
||||
if (lastProcess.waitFor(timeout, TimeUnit.MILLISECONDS)) {
|
||||
// Get exit code of last process.
|
||||
exitCode = lastProcess.exitValue();
|
||||
} else {
|
||||
reportError("timeout", Long.toString(timeout));
|
||||
}
|
||||
} else {
|
||||
// Wait for last process and get exit code.
|
||||
exitCode = lastProcess.waitFor();
|
||||
}
|
||||
|
||||
// Accumulate the output and error streams.
|
||||
outputString += byteOutputStream != null ? byteOutputStream.toString() : "";
|
||||
errorString += byteErrorStream != null ? byteErrorStream.toString() : "";
|
||||
} catch (InterruptedException ex) {
|
||||
// Kill any living processes.
|
||||
processes.stream().forEach(p -> {
|
||||
if (p.isAlive()) {
|
||||
p.destroy();
|
||||
}
|
||||
|
||||
// Get the first error code.
|
||||
exitCode = exitCode == 0 ? p.exitValue() : exitCode;
|
||||
});
|
||||
}
|
||||
|
||||
// If we got a non-zero exit code then possibly throw an exception.
|
||||
if (exitCode != 0 && envVarBooleanValue("JJS_THROW_ON_EXIT")) {
|
||||
throw rangeError("exec.returned.non.zero", ScriptRuntime.safeToString(exitCode));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* createTokenizer - build up StreamTokenizer for the command script
|
||||
* @param script command script to parsed
|
||||
* @return StreamTokenizer for command script
|
||||
*/
|
||||
private static StreamTokenizer createTokenizer(final String script) {
|
||||
final StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(script));
|
||||
tokenizer.resetSyntax();
|
||||
// Default all characters to word.
|
||||
tokenizer.wordChars(0, 255);
|
||||
// Spaces and special characters are white spaces.
|
||||
tokenizer.whitespaceChars(0, ' ');
|
||||
// Ignore # comments.
|
||||
tokenizer.commentChar('#');
|
||||
// Handle double and single quote strings.
|
||||
tokenizer.quoteChar('"');
|
||||
tokenizer.quoteChar('\'');
|
||||
// Need to recognize the end of a command.
|
||||
tokenizer.eolIsSignificant(true);
|
||||
// Command separator.
|
||||
tokenizer.ordinaryChar(';');
|
||||
// Pipe separator.
|
||||
tokenizer.ordinaryChar('|');
|
||||
|
||||
return tokenizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* process - process a command string
|
||||
* @param script command script to parsed
|
||||
*/
|
||||
void process(final String script) {
|
||||
// Build up StreamTokenizer for the command script.
|
||||
final StreamTokenizer tokenizer = createTokenizer(script);
|
||||
|
||||
// Prepare to accumulate command tokens.
|
||||
final List<String> command = new ArrayList<>();
|
||||
// Prepare to acumulate partial tokens joined with "\ ".
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
|
||||
try {
|
||||
// Fetch next token until end of script.
|
||||
while (tokenizer.nextToken() != StreamTokenizer.TT_EOF) {
|
||||
// Next word token.
|
||||
String token = tokenizer.sval;
|
||||
|
||||
// If special token.
|
||||
if (token == null) {
|
||||
// Flush any partial token.
|
||||
if (sb.length() != 0) {
|
||||
command.add(sb.append(token).toString());
|
||||
sb.setLength(0);
|
||||
}
|
||||
|
||||
// Process a completed command.
|
||||
// Will be either ';' (command end) or '|' (pipe), true if '|'.
|
||||
command(command, tokenizer.ttype == '|');
|
||||
|
||||
if (exitCode != EXIT_SUCCESS) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start with a new set of tokens.
|
||||
command.clear();
|
||||
} else if (token.endsWith("\\")) {
|
||||
// Backslash followed by space.
|
||||
sb.append(token.substring(0, token.length() - 1)).append(' ');
|
||||
} else if (sb.length() == 0) {
|
||||
// If not a word then must be a quoted string.
|
||||
if (tokenizer.ttype != StreamTokenizer.TT_WORD) {
|
||||
// Quote string, sb is free to use (empty.)
|
||||
sb.append((char)tokenizer.ttype);
|
||||
sb.append(token);
|
||||
sb.append((char)tokenizer.ttype);
|
||||
token = sb.toString();
|
||||
sb.setLength(0);
|
||||
}
|
||||
|
||||
command.add(token);
|
||||
} else {
|
||||
// Partial token pending.
|
||||
command.add(sb.append(token).toString());
|
||||
sb.setLength(0);
|
||||
}
|
||||
}
|
||||
} catch (final IOException ex) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
// Partial token pending.
|
||||
if (sb.length() != 0) {
|
||||
command.add(sb.toString());
|
||||
}
|
||||
|
||||
// Process last command.
|
||||
command(command, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* process - process a command array of strings
|
||||
* @param script command script to be processed
|
||||
*/
|
||||
void process(final List<String> tokens) {
|
||||
// Prepare to accumulate command tokens.
|
||||
final List<String> command = new ArrayList<>();
|
||||
|
||||
// Iterate through tokens.
|
||||
final Iterator<String> iterator = tokens.iterator();
|
||||
while (iterator.hasNext() && exitCode == EXIT_SUCCESS) {
|
||||
// Next word token.
|
||||
String token = iterator.next();
|
||||
|
||||
if (token == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (token) {
|
||||
case "|":
|
||||
// Process as a piped command.
|
||||
command(command, true);
|
||||
// Start with a new set of tokens.
|
||||
command.clear();
|
||||
|
||||
continue;
|
||||
case ";":
|
||||
// Process as a normal command.
|
||||
command(command, false);
|
||||
// Start with a new set of tokens.
|
||||
command.clear();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
command.add(token);
|
||||
}
|
||||
|
||||
// Process last command.
|
||||
command(command, false);
|
||||
}
|
||||
|
||||
void reportError(final String msg, final String object) {
|
||||
errorString += ECMAErrors.getMessage("range.error.exec." + msg, object);
|
||||
exitCode = EXIT_FAILURE;
|
||||
}
|
||||
|
||||
String getOutputString() {
|
||||
return outputString;
|
||||
}
|
||||
|
||||
String getErrorString() {
|
||||
return errorString;
|
||||
}
|
||||
|
||||
int getExitCode() {
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
void setEnvironment(Map<String, String> environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
void setInputStream(InputStream inputStream) {
|
||||
this.inputStream = inputStream;
|
||||
}
|
||||
|
||||
void setInputString(String inputString) {
|
||||
this.inputString = inputString;
|
||||
}
|
||||
|
||||
void setOutputStream(OutputStream outputStream) {
|
||||
this.outputStream = outputStream;
|
||||
}
|
||||
|
||||
void setErrorStream(OutputStream errorStream) {
|
||||
this.errorStream = errorStream;
|
||||
}
|
||||
}
|
238
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/ScriptingFunctions.java
238
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/ScriptingFunctions.java
@ -26,24 +26,24 @@
|
||||
package jdk.nashorn.internal.runtime;
|
||||
|
||||
import static jdk.nashorn.internal.lookup.Lookup.MH;
|
||||
import static jdk.nashorn.internal.runtime.ECMAErrors.rangeError;
|
||||
import static jdk.nashorn.internal.runtime.ECMAErrors.typeError;
|
||||
import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.StreamTokenizer;
|
||||
import java.io.StringReader;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import jdk.nashorn.internal.objects.NativeArray;
|
||||
import static jdk.nashorn.internal.runtime.ECMAErrors.rangeError;
|
||||
|
||||
/**
|
||||
* Global functions supported only in scripting mode.
|
||||
@ -71,9 +71,6 @@ public final class ScriptingFunctions {
|
||||
/** EXIT name - special property used by $EXEC API. */
|
||||
public static final String EXIT_NAME = "$EXIT";
|
||||
|
||||
/** THROW_ON_ERROR name - special property of the $EXEC function used by $EXEC API. */
|
||||
public static final String THROW_ON_ERROR_NAME = "throwOnError";
|
||||
|
||||
/** Names of special properties used by $ENV API. */
|
||||
public static final String ENV_NAME = "$ENV";
|
||||
|
||||
@ -132,188 +129,97 @@ public final class ScriptingFunctions {
|
||||
* Nashorn extension: exec a string in a separate process.
|
||||
*
|
||||
* @param self self reference
|
||||
* @param args string to execute, input and additional arguments, to be appended to {@code string}. Additional
|
||||
* arguments can be passed as either one JavaScript array, whose elements will be converted to
|
||||
* strings; or as a sequence of varargs, each of which will be converted to a string.
|
||||
* @param args In one of four forms
|
||||
* 1. String script, String input
|
||||
* 2. String script, InputStream input, OutputStream output, OutputStream error
|
||||
* 3. Array scriptTokens, String input
|
||||
* 4. Array scriptTokens, InputStream input, OutputStream output, OutputStream error
|
||||
*
|
||||
* @return output string from the request
|
||||
*
|
||||
* @throws IOException if any stream access fails
|
||||
* @throws InterruptedException if execution is interrupted
|
||||
* @return output string from the request if in form of 1. or 3., empty string otherwise
|
||||
*/
|
||||
public static Object exec(final Object self, final Object... args) throws IOException, InterruptedException {
|
||||
// Current global is need to fetch additional inputs and for additional results.
|
||||
final ScriptObject global = Context.getGlobal();
|
||||
final Object string = args.length > 0? args[0] : UNDEFINED;
|
||||
final Object input = args.length > 1? args[1] : UNDEFINED;
|
||||
final Object[] argv = (args.length > 2)? Arrays.copyOfRange(args, 2, args.length) : ScriptRuntime.EMPTY_ARRAY;
|
||||
// Assemble command line, process additional arguments.
|
||||
final List<String> cmdLine = tokenizeString(JSType.toString(string));
|
||||
final Object[] additionalArgs = argv.length == 1 && argv[0] instanceof NativeArray ?
|
||||
((NativeArray) argv[0]).asObjectArray() :
|
||||
argv;
|
||||
for (Object arg : additionalArgs) {
|
||||
cmdLine.add(JSType.toString(arg));
|
||||
public static Object exec(final Object self, final Object... args) {
|
||||
final Object arg0 = args.length > 0 ? args[0] : UNDEFINED;
|
||||
final Object arg1 = args.length > 1 ? args[1] : UNDEFINED;
|
||||
final Object arg2 = args.length > 2 ? args[2] : UNDEFINED;
|
||||
final Object arg3 = args.length > 3 ? args[3] : UNDEFINED;
|
||||
|
||||
InputStream inputStream = null;
|
||||
OutputStream outputStream = null;
|
||||
OutputStream errorStream = null;
|
||||
String script = null;
|
||||
List<String> tokens = null;
|
||||
String inputString = null;
|
||||
|
||||
if (arg0 instanceof NativeArray) {
|
||||
String[] array = (String[])JSType.toJavaArray(arg0, String.class);
|
||||
tokens = new ArrayList<>();
|
||||
tokens.addAll(Arrays.asList(array));
|
||||
} else {
|
||||
script = JSType.toString(arg0);
|
||||
}
|
||||
|
||||
// Set up initial process.
|
||||
final ProcessBuilder processBuilder = new ProcessBuilder(cmdLine);
|
||||
if (arg1 instanceof InputStream) {
|
||||
inputStream = (InputStream)arg1;
|
||||
} else {
|
||||
inputString = JSType.toString(arg1);
|
||||
}
|
||||
|
||||
// Current ENV property state.
|
||||
if (arg2 instanceof OutputStream) {
|
||||
outputStream = (OutputStream)arg2;
|
||||
}
|
||||
|
||||
if (arg3 instanceof OutputStream) {
|
||||
errorStream = (OutputStream)arg3;
|
||||
}
|
||||
|
||||
// Current global is need to fetch additional inputs and for additional results.
|
||||
final ScriptObject global = Context.getGlobal();
|
||||
|
||||
// Capture ENV property state.
|
||||
final Map<String, String> environment = new HashMap<>();
|
||||
final Object env = global.get(ENV_NAME);
|
||||
|
||||
if (env instanceof ScriptObject) {
|
||||
final ScriptObject envProperties = (ScriptObject)env;
|
||||
|
||||
// If a working directory is present, use it.
|
||||
final Object pwd = envProperties.get(PWD_NAME);
|
||||
if (pwd != UNDEFINED) {
|
||||
final File pwdFile = new File(JSType.toString(pwd));
|
||||
if (pwdFile.exists()) {
|
||||
processBuilder.directory(pwdFile);
|
||||
}
|
||||
}
|
||||
|
||||
// Set up ENV variables.
|
||||
final Map<String, String> environment = processBuilder.environment();
|
||||
environment.clear();
|
||||
for (final Map.Entry<Object, Object> entry : envProperties.entrySet()) {
|
||||
// Copy ENV variables.
|
||||
envProperties.entrySet().stream().forEach((entry) -> {
|
||||
environment.put(JSType.toString(entry.getKey()), JSType.toString(entry.getValue()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Start the process.
|
||||
final Process process = processBuilder.start();
|
||||
final IOException exception[] = new IOException[2];
|
||||
// get the $EXEC function object from the global object
|
||||
final Object exec = global.get(EXEC_NAME);
|
||||
assert exec instanceof ScriptObject : EXEC_NAME + " is not a script object!";
|
||||
|
||||
// Collect output.
|
||||
final StringBuilder outBuffer = new StringBuilder();
|
||||
final Thread outThread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final char buffer[] = new char[1024];
|
||||
try (final InputStreamReader inputStream = new InputStreamReader(process.getInputStream())) {
|
||||
for (int length; (length = inputStream.read(buffer, 0, buffer.length)) != -1; ) {
|
||||
outBuffer.append(buffer, 0, length);
|
||||
}
|
||||
} catch (final IOException ex) {
|
||||
exception[0] = ex;
|
||||
}
|
||||
}
|
||||
}, "$EXEC output");
|
||||
// Execute the commands
|
||||
final CommandExecutor executor = new CommandExecutor();
|
||||
executor.setInputString(inputString);
|
||||
executor.setInputStream(inputStream);
|
||||
executor.setOutputStream(outputStream);
|
||||
executor.setErrorStream(errorStream);
|
||||
executor.setEnvironment(environment);
|
||||
|
||||
// Collect errors.
|
||||
final StringBuilder errBuffer = new StringBuilder();
|
||||
final Thread errThread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final char buffer[] = new char[1024];
|
||||
try (final InputStreamReader inputStream = new InputStreamReader(process.getErrorStream())) {
|
||||
for (int length; (length = inputStream.read(buffer, 0, buffer.length)) != -1; ) {
|
||||
errBuffer.append(buffer, 0, length);
|
||||
}
|
||||
} catch (final IOException ex) {
|
||||
exception[1] = ex;
|
||||
}
|
||||
}
|
||||
}, "$EXEC error");
|
||||
|
||||
// Start gathering output.
|
||||
outThread.start();
|
||||
errThread.start();
|
||||
|
||||
// If input is present, pass on to process.
|
||||
if (!JSType.nullOrUndefined(input)) {
|
||||
try (OutputStreamWriter outputStream = new OutputStreamWriter(process.getOutputStream())) {
|
||||
final String in = JSType.toString(input);
|
||||
outputStream.write(in, 0, in.length());
|
||||
} catch (final IOException ex) {
|
||||
// Process was not expecting input. May be normal state of affairs.
|
||||
}
|
||||
if (tokens != null) {
|
||||
executor.process(tokens);
|
||||
} else {
|
||||
executor.process(script);
|
||||
}
|
||||
|
||||
// Wait for the process to complete.
|
||||
final int exit = process.waitFor();
|
||||
outThread.join();
|
||||
errThread.join();
|
||||
|
||||
final String out = outBuffer.toString();
|
||||
final String err = errBuffer.toString();
|
||||
final String outString = executor.getOutputString();
|
||||
final String errString = executor.getErrorString();
|
||||
int exitCode = executor.getExitCode();
|
||||
|
||||
// Set globals for secondary results.
|
||||
global.set(OUT_NAME, out, 0);
|
||||
global.set(ERR_NAME, err, 0);
|
||||
global.set(EXIT_NAME, exit, 0);
|
||||
|
||||
// Propagate exception if present.
|
||||
for (final IOException element : exception) {
|
||||
if (element != null) {
|
||||
throw element;
|
||||
}
|
||||
}
|
||||
|
||||
// if we got a non-zero exit code ("failure"), then we have to decide to throw error or not
|
||||
if (exit != 0) {
|
||||
// get the $EXEC function object from the global object
|
||||
final Object exec = global.get(EXEC_NAME);
|
||||
assert exec instanceof ScriptObject : EXEC_NAME + " is not a script object!";
|
||||
|
||||
// Check if the user has set $EXEC.throwOnError property to true. If so, throw RangeError
|
||||
// If that property is not set or set to false, then silently proceed with the rest.
|
||||
if (JSType.toBoolean(((ScriptObject)exec).get(THROW_ON_ERROR_NAME))) {
|
||||
throw rangeError("exec.returned.non.zero", ScriptRuntime.safeToString(exit));
|
||||
}
|
||||
}
|
||||
global.set(OUT_NAME, outString, 0);
|
||||
global.set(ERR_NAME, errString, 0);
|
||||
global.set(EXIT_NAME, exitCode, 0);
|
||||
|
||||
// Return the result from stdout.
|
||||
return out;
|
||||
return outString;
|
||||
}
|
||||
|
||||
private static MethodHandle findOwnMH(final String name, final Class<?> rtype, final Class<?>... types) {
|
||||
return MH.findStatic(MethodHandles.lookup(), ScriptingFunctions.class, name, MH.type(rtype, types));
|
||||
}
|
||||
|
||||
/**
|
||||
* Break a string into tokens, honoring quoted arguments and escaped spaces.
|
||||
*
|
||||
* @param str a {@link String} to tokenize.
|
||||
* @return a {@link List} of {@link String}s representing the tokens that
|
||||
* constitute the string.
|
||||
*/
|
||||
public static List<String> tokenizeString(final String str) {
|
||||
final StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(str));
|
||||
tokenizer.resetSyntax();
|
||||
tokenizer.wordChars(0, 255);
|
||||
tokenizer.whitespaceChars(0, ' ');
|
||||
tokenizer.commentChar('#');
|
||||
tokenizer.quoteChar('"');
|
||||
tokenizer.quoteChar('\'');
|
||||
final List<String> tokenList = new ArrayList<>();
|
||||
final StringBuilder toAppend = new StringBuilder();
|
||||
while (nextToken(tokenizer) != StreamTokenizer.TT_EOF) {
|
||||
final String s = tokenizer.sval;
|
||||
// The tokenizer understands about honoring quoted strings and recognizes
|
||||
// them as one token that possibly contains multiple space-separated words.
|
||||
// It does not recognize quoted spaces, though, and will split after the
|
||||
// escaping \ character. This is handled here.
|
||||
if (s.endsWith("\\")) {
|
||||
// omit trailing \, append space instead
|
||||
toAppend.append(s.substring(0, s.length() - 1)).append(' ');
|
||||
} else {
|
||||
tokenList.add(toAppend.append(s).toString());
|
||||
toAppend.setLength(0);
|
||||
}
|
||||
}
|
||||
if (toAppend.length() != 0) {
|
||||
tokenList.add(toAppend.toString());
|
||||
}
|
||||
return tokenList;
|
||||
}
|
||||
|
||||
private static int nextToken(final StreamTokenizer tokenizer) {
|
||||
try {
|
||||
return tokenizer.nextToken();
|
||||
} catch (final IOException ioe) {
|
||||
return StreamTokenizer.TT_EOF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
39
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/linker/Bootstrap.java
39
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/linker/Bootstrap.java
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2010, 2013, 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
|
||||
@ -29,6 +29,7 @@ import static jdk.nashorn.internal.codegen.CompilerConstants.staticCallNoLookup;
|
||||
import static jdk.nashorn.internal.runtime.ECMAErrors.typeError;
|
||||
|
||||
import java.lang.invoke.CallSite;
|
||||
import java.lang.invoke.ConstantCallSite;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodHandles.Lookup;
|
||||
@ -202,6 +203,42 @@ public final class Bootstrap {
|
||||
return Context.getDynamicLinker(lookup.lookupClass()).link(LinkerCallSite.newLinkerCallSite(lookup, opDesc, type, flags));
|
||||
}
|
||||
|
||||
/**
|
||||
* Boostrapper for math calls that may overflow
|
||||
* @param lookup lookup
|
||||
* @param name name of operation
|
||||
* @param type method type
|
||||
* @param programPoint program point to bind to callsite
|
||||
*
|
||||
* @return callsite for a math intrinsic node
|
||||
*/
|
||||
public static CallSite mathBootstrap(final Lookup lookup, final String name, final MethodType type, final int programPoint) {
|
||||
final MethodHandle mh;
|
||||
switch (name) {
|
||||
case "iadd":
|
||||
mh = JSType.ADD_EXACT.methodHandle();
|
||||
break;
|
||||
case "isub":
|
||||
mh = JSType.SUB_EXACT.methodHandle();
|
||||
break;
|
||||
case "imul":
|
||||
mh = JSType.MUL_EXACT.methodHandle();
|
||||
break;
|
||||
case "idiv":
|
||||
mh = JSType.DIV_EXACT.methodHandle();
|
||||
break;
|
||||
case "irem":
|
||||
mh = JSType.REM_EXACT.methodHandle();
|
||||
break;
|
||||
case "ineg":
|
||||
mh = JSType.NEGATE_EXACT.methodHandle();
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError("unsupported math intrinsic");
|
||||
}
|
||||
return new ConstantCallSite(MH.insertArguments(mh, mh.type().parameterCount() - 1, programPoint));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a dynamic invoker for a specified dynamic operation using the
|
||||
* public lookup. You can use this method to create a method handle that
|
||||
|
@ -170,7 +170,11 @@ range.error.invalid.radix=radix argument must be in [2, 36]
|
||||
range.error.invalid.date=Invalid Date
|
||||
range.error.too.many.errors=Script contains too many errors: {0} errors
|
||||
range.error.concat.string.too.big=Concatenated String is too big
|
||||
range.error.exec.file.not.exist=$EXEC File or directory does not exist : {0}
|
||||
range.error.exec.not.directory=$EXEC Not a directory : {0}
|
||||
range.error.exec.returned.non.zero=$EXEC returned non-zero exit code: {0}
|
||||
range.error.exec.timeout=$EXEC Command timeout : {0}
|
||||
range.error.exec.unknown.command=$EXEC Unknown command : {0}
|
||||
|
||||
reference.error.not.defined="{0}" is not defined
|
||||
reference.error.cant.be.used.as.lhs="{0}" can not be used as the left-hand side of assignment
|
||||
|
@ -57,6 +57,8 @@ import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StreamTokenizer;
|
||||
import java.io.StringReader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@ -278,7 +280,7 @@ public class Shell implements PartialParser {
|
||||
// as it might actually contain several arguments. Mac OS X splits shebang arguments, other platforms don't.
|
||||
// This special handling is also only necessary if the first argument actually starts with an option.
|
||||
if (args[0].startsWith("-") && !System.getProperty("os.name", "generic").startsWith("Mac OS X")) {
|
||||
processedArgs.addAll(0, ScriptingFunctions.tokenizeString(processedArgs.remove(0)));
|
||||
processedArgs.addAll(0, tokenizeString(processedArgs.remove(0)));
|
||||
}
|
||||
|
||||
int shebangFilePos = -1; // -1 signifies "none found"
|
||||
@ -308,6 +310,44 @@ public class Shell implements PartialParser {
|
||||
return processedArgs.stream().toArray(String[]::new);
|
||||
}
|
||||
|
||||
public static List<String> tokenizeString(final String str) {
|
||||
final StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(str));
|
||||
tokenizer.resetSyntax();
|
||||
tokenizer.wordChars(0, 255);
|
||||
tokenizer.whitespaceChars(0, ' ');
|
||||
tokenizer.commentChar('#');
|
||||
tokenizer.quoteChar('"');
|
||||
tokenizer.quoteChar('\'');
|
||||
final List<String> tokenList = new ArrayList<>();
|
||||
final StringBuilder toAppend = new StringBuilder();
|
||||
while (nextToken(tokenizer) != StreamTokenizer.TT_EOF) {
|
||||
final String s = tokenizer.sval;
|
||||
// The tokenizer understands about honoring quoted strings and recognizes
|
||||
// them as one token that possibly contains multiple space-separated words.
|
||||
// It does not recognize quoted spaces, though, and will split after the
|
||||
// escaping \ character. This is handled here.
|
||||
if (s.endsWith("\\")) {
|
||||
// omit trailing \, append space instead
|
||||
toAppend.append(s.substring(0, s.length() - 1)).append(' ');
|
||||
} else {
|
||||
tokenList.add(toAppend.append(s).toString());
|
||||
toAppend.setLength(0);
|
||||
}
|
||||
}
|
||||
if (toAppend.length() != 0) {
|
||||
tokenList.add(toAppend.toString());
|
||||
}
|
||||
return tokenList;
|
||||
}
|
||||
|
||||
private static int nextToken(final StreamTokenizer tokenizer) {
|
||||
try {
|
||||
return tokenizer.nextToken();
|
||||
} catch (final IOException ioe) {
|
||||
return StreamTokenizer.TT_EOF;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles the given script files in the command line
|
||||
* This is called only when using the --compile-only flag
|
||||
|
91
nashorn/test/script/basic/JDK-8141209.js
Normal file
91
nashorn/test/script/basic/JDK-8141209.js
Normal file
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (c) 2016, 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* JDK-8141209 : $EXEC should allow streaming
|
||||
*
|
||||
* @test
|
||||
* @option -scripting
|
||||
* @runif os.not.windows
|
||||
* @run
|
||||
*/
|
||||
|
||||
|
||||
var System = Java.type("java.lang.System");
|
||||
var File = Java.type("java.io.File");
|
||||
var ByteArrayInputStream = Java.type("java.io.ByteArrayInputStream");
|
||||
var ByteArrayOutputStream = Java.type("java.io.ByteArrayOutputStream");
|
||||
|
||||
var input = <<<EOD
|
||||
There was an Old Man with a beard,
|
||||
Who said, It is just as I feared!
|
||||
Two Owls and a Hen,
|
||||
Four Larks and a Wren,
|
||||
Have all built their nests in my beard!
|
||||
EOD
|
||||
|
||||
function tempFile() {
|
||||
return File.createTempFile("JDK-8141209", ".txt").toString();
|
||||
}
|
||||
|
||||
`ls -l / | sed > ${tempFile()} -e '/^d/ d'`
|
||||
|
||||
$EXEC(["ls", "-l", "|", "sed", "-e", "/^d/ d", ">", tempFile()])
|
||||
|
||||
var t1 = tempFile();
|
||||
|
||||
$EXEC(<<<EOD)
|
||||
ls -l >${t1}
|
||||
sed <${t1} >${tempFile()} -e '/^d/ d'
|
||||
EOD
|
||||
|
||||
$EXEC(<<<EOD, `ls -l`)
|
||||
sed >${tempFile()} -e '/^d/ d'
|
||||
EOD
|
||||
|
||||
var instream = new ByteArrayInputStream(input.getBytes());
|
||||
var outstream = new ByteArrayOutputStream();
|
||||
var errstream = new ByteArrayOutputStream();
|
||||
$EXEC("sed -e '/beard/ d'", instream, outstream, errstream);
|
||||
var out = outstream.toString();
|
||||
var err = errstream.toString();
|
||||
|
||||
instream = new ByteArrayInputStream(input.getBytes());
|
||||
$EXEC("sed -e '/beard/ d'", instream, System.out, System.err);
|
||||
|
||||
|
||||
$EXEC(<<<EOD)
|
||||
cd .
|
||||
setenv TEMP 0
|
||||
unsetenv TEMP
|
||||
EOD
|
||||
|
||||
$ENV.JJS_THROW_ON_EXIT = "1";
|
||||
$ENV.JJS_TIMEOUT = "1000";
|
||||
$ENV.JJS_ECHO = "1";
|
||||
$ENV.JJS_INHERIT_IO = "1";
|
||||
|
||||
$EXEC("echo hello world", instream);
|
||||
|
||||
|
||||
|
5
nashorn/test/script/basic/JDK-8141209.js.EXPECTED
Normal file
5
nashorn/test/script/basic/JDK-8141209.js.EXPECTED
Normal file
@ -0,0 +1,5 @@
|
||||
Who said, It is just as I feared!
|
||||
Two Owls and a Hen,
|
||||
Four Larks and a Wren,
|
||||
echo hello world
|
||||
hello world
|
@ -46,9 +46,9 @@ function tryExec() {
|
||||
tryExec();
|
||||
|
||||
// turn on error with non-zero exit code
|
||||
$EXEC.throwOnError = true;
|
||||
$ENV.JJS_THROW_ON_EXIT = "1";
|
||||
tryExec();
|
||||
|
||||
// no exception after this
|
||||
$EXEC.throwOnError = false;
|
||||
$ENV.JJS_THROW_ON_EXIT = "0";
|
||||
tryExec();
|
||||
|
@ -68,7 +68,7 @@ import javax.xml.xpath.XPath;
|
||||
import javax.xml.xpath.XPathConstants;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
import javax.xml.xpath.XPathFactory;
|
||||
import jdk.nashorn.internal.runtime.ScriptingFunctions;
|
||||
import jdk.nashorn.tools.Shell;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
|
||||
@ -225,7 +225,7 @@ public final class TestFinder {
|
||||
boolean explicitOptimistic = false;
|
||||
|
||||
String allContent = new String(Files.readAllBytes(testFile));
|
||||
Iterator<String> scanner = ScriptingFunctions.tokenizeString(allContent).iterator();
|
||||
Iterator<String> scanner = Shell.tokenizeString(allContent).iterator();
|
||||
while (scanner.hasNext()) {
|
||||
// TODO: Scan for /ref=file qualifiers, etc, to determine run
|
||||
// behavior
|
||||
|
Loading…
x
Reference in New Issue
Block a user