8297444: Refactor the javacserver build tool

Reviewed-by: erikj, cstein
This commit is contained in:
Magnus Ihse Bursie 2022-12-01 23:41:58 +00:00
parent e846b0438c
commit 257aa15f15
29 changed files with 905 additions and 1918 deletions

View File

@ -219,31 +219,35 @@ define SetupJavaCompilationBody
# Use java server if it is enabled, and the user does not want a specialized
# class path.
ifeq ($$(ENABLE_JAVAC_SERVER)+$$($1_CLASSPATH), true+)
$1_JAVAC := $$(INTERIM_LANGTOOLS_ARGS) -cp $(BUILDTOOLS_OUTPUTDIR)/langtools_javacserver_classes javacserver.Main
# Create a configuration file with the needed information for the javac
# server to function properly.
$1_JAVAC_SERVER_CONFIG := $$($1_BIN)$$($1_MODULE_SUBDIR)/_the.$$($1_SAFE_NAME)-server.conf
$1_JAVAC_SERVER_CONFIG := $$($1_BIN)$$($1_MODULE_SUBDIR)/_the.$$($1_SAFE_NAME)-javacserver.conf
# Arguments needed to launch the javacserver client, as well as for the
# client to launch the server.
$1_JAVAC_SERVER_ARGS := $$(INTERIM_LANGTOOLS_ARGS) \
-cp $(BUILDTOOLS_OUTPUTDIR)/langtools_javacserver_classes
# The portfile contains the tcp/ip on which the server listens
# and the cookie necessary to talk to the server.
$1_JAVAC_PORT_FILE := $$(call FixPath, $$(JAVAC_SERVER_DIR)/server.port)
# The servercmd specifies how to launch the server. This will be executed
# by the client, if needed.
$1_JAVAC_SERVER_CMD := $$(call FixPath, $$(JAVA) $$($1_JAVA_FLAGS) $$($1_JAVAC))
# The javacmd tells the client how to run java to launch the server.
$1_JAVAC_SERVER_JAVA_CMD := $$(call FixPath, $$(JAVA) $$($1_JAVA_FLAGS) \
$$($1_JAVAC_SERVER_ARGS))
$1_CONFIG_VARDEPS := $$($1_JAVAC_PORT_FILE) $$($1_JAVAC_SERVER_CMD)
$1_CONFIG_VARDEPS := $$($1_JAVAC_PORT_FILE) $$($1_JAVAC_SERVER_JAVA_CMD)
$1_CONFIG_VARDEPS_FILE := $$(call DependOnVariable, $1_CONFIG_VARDEPS, \
$$($1_BIN)$$($1_MODULE_SUBDIR)/_the.$1.config_vardeps)
# Write these values to a config file
$$($1_JAVAC_SERVER_CONFIG): $$($1_CONFIG_VARDEPS_FILE)
$(ECHO) portfile=$$($1_JAVAC_PORT_FILE) > $$@
$(ECHO) servercmd=$$($1_JAVAC_SERVER_CMD) >> $$@
$(ECHO) javacmd=$$($1_JAVAC_SERVER_JAVA_CMD) >> $$@
# Always use small java to launch client
$1_JAVAC_CMD := $$(JAVA_SMALL) $$($1_JAVA_FLAGS) $$($1_JAVAC) \
--server:conf=$$($1_JAVAC_SERVER_CONFIG)
$1_JAVAC_CMD := $$(JAVA_SMALL) $$($1_JAVA_FLAGS) $$($1_JAVAC_SERVER_ARGS) \
javacserver.Main --conf=$$($1_JAVAC_SERVER_CONFIG)
else
# No javac server
$1_JAVAC := $$(INTERIM_LANGTOOLS_ARGS) -m jdk.compiler.interim/com.sun.tools.javac.Main

View File

@ -25,34 +25,13 @@
package javacserver;
import java.util.Arrays;
import javacserver.client.ClientMain;
import javacserver.server.ServerMain;
import static javacserver.options.Option.STARTSERVER;
import javacserver.client.Client;
/**
* The application entry point of the smart javac wrapper tool.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
* The application entry point of the javacserver build tool.
*/
public class Main {
public static void main(String... args) {
System.exit(go(args));
}
public static int go(String[] args) {
// Server or client mode?
boolean serverMode = Arrays.asList(args)
.stream()
.anyMatch(arg -> arg.startsWith(STARTSERVER.arg));
return serverMode ? ServerMain.run(args) : ClientMain.run(args);
public static void main(String... args) {
Client.main(args);
}
}

View File

@ -1,148 +0,0 @@
/*
* Copyright (c) 2012, 2022, 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 javacserver;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Stream;
/**
* Utilities.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public class Util {
public static String extractStringOption(String opName, String s) {
return extractStringOption(opName, s, null);
}
private static String extractStringOptionWithDelimiter(String opName, String s, String deflt, char delimiter) {
int p = s.indexOf(opName+"=");
if (p == -1) return deflt;
p+=opName.length()+1;
int pe = s.indexOf(delimiter, p);
if (pe == -1) pe = s.length();
return s.substring(p, pe);
}
public static String extractStringOption(String opName, String s, String deflt) {
return extractStringOptionWithDelimiter(opName, s, deflt, ',');
}
public static String extractStringOptionLine(String opName, String s, String deflt) {
return extractStringOptionWithDelimiter(opName, s, deflt, '\n').strip();
}
public static int extractIntOption(String opName, String s, int deflt) {
int p = s.indexOf(opName+"=");
if (p == -1) return deflt;
p+=opName.length()+1;
int pe = s.indexOf(',', p);
if (pe == -1) pe = s.length();
int v = 0;
try {
v = Integer.parseInt(s.substring(p, pe));
} catch (Exception e) {}
return v;
}
/**
* Convenience method to create a set with strings.
*/
public static Set<String> set(String... ss) {
Set<String> set = new HashSet<>();
set.addAll(Arrays.asList(ss));
return set;
}
/**
* Normalize windows drive letter paths to upper case to enable string
* comparison.
*
* @param file File name to normalize
* @return The normalized string if file has a drive letter at the beginning,
* otherwise the original string.
*/
public static String normalizeDriveLetter(String file) {
if (file.length() > 2 && file.charAt(1) == ':') {
return Character.toUpperCase(file.charAt(0)) + file.substring(1);
} else if (file.length() > 3 && file.charAt(0) == '*'
&& file.charAt(2) == ':') {
// Handle a wildcard * at the beginning of the string.
return file.substring(0, 1) + Character.toUpperCase(file.charAt(1))
+ file.substring(2);
}
return file;
}
public static <E> Set<E> union(Set<? extends E> s1,
Set<? extends E> s2) {
Set<E> union = new HashSet<>();
union.addAll(s1);
union.addAll(s2);
return union;
}
public static <E> Set<E> subtract(Set<? extends E> orig,
Set<? extends E> toSubtract) {
Set<E> difference = new HashSet<>(orig);
difference.removeAll(toSubtract);
return difference;
}
public static String getStackTrace(Throwable t) {
StringWriter sw = new StringWriter();
t.printStackTrace(new PrintWriter(sw));
return sw.toString();
}
public static <E> Set<E> intersection(Collection<? extends E> c1,
Collection<? extends E> c2) {
Set<E> intersection = new HashSet<E>(c1);
intersection.retainAll(c2);
return intersection;
}
public static Stream<String> getLines(String str) {
return str.isEmpty()
? Stream.empty()
: Stream.of(str.split(Pattern.quote(System.lineSeparator())));
}
}

View File

@ -0,0 +1,191 @@
/*
* Copyright (c) 2014, 2022, 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 javacserver.client;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Reader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javacserver.server.Server;
import javacserver.shared.PortFileInaccessibleException;
import javacserver.shared.Protocol;
import javacserver.shared.Result;
import javacserver.util.AutoFlushWriter;
import javacserver.util.Log;
/**
* The javacserver client. This is called from the makefiles, and is responsible for passing the command
* line on to a server instance running javac, starting a new server if needed.
*/
public class Client {
private static final Log.Level LOG_LEVEL = Log.Level.INFO;
// Wait 2 seconds for response, before giving up on javac server.
private static final int CONNECTION_TIMEOUT = 2000;
private static final int MAX_CONNECT_ATTEMPTS = 3;
private static final int WAIT_BETWEEN_CONNECT_ATTEMPTS = 2000;
private final ClientConfiguration conf;
public Client(ClientConfiguration conf) {
this.conf = conf;
}
public static void main(String... args) {
Log.setLogForCurrentThread(new Log(
new AutoFlushWriter(new OutputStreamWriter(System.out)),
new AutoFlushWriter(new OutputStreamWriter(System.err))));
Log.setLogLevel(LOG_LEVEL);
ClientConfiguration conf = ClientConfiguration.fromCommandLineArguments(args);
if (conf == null) {
System.exit(Result.CMDERR.exitCode);
}
Client client = new Client(conf);
int exitCode = client.dispatchToServer();
System.exit(exitCode);
}
private int dispatchToServer() {
try {
// Check if server seems to be already running
if (!conf.portFile().hasValidValues()) {
// Fork a new server and wait for it to start
startNewServer();
}
try (Socket socket = tryConnect()) {
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
Protocol.sendCommand(out, conf.javacArgs());
int exitCode = Protocol.readResponse(in);
return exitCode;
}
} catch (PortFileInaccessibleException e) {
Log.error("Port file inaccessible.");
return Result.ERROR.exitCode;
} catch (IOException ioe) {
Log.error("IOException caught during compilation: " + ioe.getMessage());
Log.debug(ioe);
return Result.ERROR.exitCode;
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // Restore interrupt
Log.error("Compilation interrupted.");
Log.debug(ie);
return Result.ERROR.exitCode;
}
}
/*
* Makes MAX_CONNECT_ATTEMPTS attempts to connect to server.
*/
private Socket tryConnect() throws IOException, InterruptedException {
int attempt = 0;
while (true) {
Log.debug("Trying to connect. Attempt " + (++attempt) + " of " + MAX_CONNECT_ATTEMPTS);
try {
Socket socket = new Socket();
InetAddress localhost = InetAddress.getByName(null);
InetSocketAddress address = new InetSocketAddress(localhost, conf.portFile().getPort());
socket.connect(address, CONNECTION_TIMEOUT);
Log.debug("Connected");
return socket;
} catch (IOException ex) {
Log.error("Connection attempt failed: " + ex.getMessage());
if (attempt >= MAX_CONNECT_ATTEMPTS) {
Log.error("Giving up");
throw new IOException("Could not connect to server", ex);
}
}
Thread.sleep(WAIT_BETWEEN_CONNECT_ATTEMPTS);
}
}
/*
* Fork a server process and wait for server to come around
*/
private void startNewServer() throws IOException, InterruptedException {
List<String> cmd = new ArrayList<>();
// conf.javaCommand() is how to start java in the way we want to run
// the server
cmd.addAll(Arrays.asList(conf.javaCommand().split(" ")));
// javacserver.server.Server is the server main class
cmd.add(Server.class.getName());
// and it expects a port file path
cmd.add(conf.portFile().getFilename());
Process serverProcess;
Log.debug("Starting server. Command: " + String.join(" ", cmd));
try {
// If the cmd for some reason can't be executed (file is not found,
// or is not executable for instance) this will throw an
// IOException
serverProcess = new ProcessBuilder(cmd).redirectErrorStream(true).start();
} catch (IOException ex) {
// Message is typically something like:
// Cannot run program "xyz": error=2, No such file or directory
Log.error("Failed to create server process: " + ex.getMessage());
Log.debug(ex);
throw new IOException(ex);
}
// serverProcess != null at this point.
try {
// Throws an IOException if no valid values materialize
conf.portFile().waitForValidValues();
} catch (IOException ex) {
// Process was started, but server failed to initialize. This could
// for instance be due to the JVM not finding the server class,
// or the server running in to some exception early on.
Log.error("javacserver server process failed to initialize: " + ex.getMessage());
Log.error("Process output:");
Reader serverStdoutStderr = new InputStreamReader(serverProcess.getInputStream());
try (BufferedReader br = new BufferedReader(serverStdoutStderr)) {
br.lines().forEach(Log::error);
}
Log.error("<End of process output>");
try {
Log.error("Process exit code: " + serverProcess.exitValue());
} catch (IllegalThreadStateException e) {
// Server is presumably still running.
}
throw new IOException("Server failed to initialize: " + ex.getMessage(), ex);
}
}
}

View File

@ -0,0 +1,131 @@
/*
* Copyright (c) 2014, 2022, 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 javacserver.client;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import javacserver.shared.PortFile;
import javacserver.util.Log;
/**
* Description of the arguments needed to start a javacserver client, as extracted from
* the command line and configuration file.
*/
public record ClientConfiguration(PortFile portFile, String javaCommand, String[] javacArgs) {
static ClientConfiguration fromCommandLineArguments(String... args) {
String confFileName = getConfFileName(args);
if (confFileName == null) {
return null;
}
String confFileContent = getConfFileContent(confFileName);
if (confFileContent == null) {
return null;
}
String portFileName = getPortFileName(confFileContent);
if (portFileName == null) {
return null;
}
String javaCommand = getJavaCommandString(confFileContent);
if (javaCommand == null) {
return null;
}
PortFile portFile = new PortFile(portFileName);
String[] javacArgs = Arrays.copyOfRange(args, 1, args.length);
ClientConfiguration conf = new ClientConfiguration(portFile, javaCommand, javacArgs);
return conf;
}
private static String getConfFileName(String[] args) {
if (args.length < 1) {
Log.error("Error: javacserver client: missing --conf=<conf file> argument");
return null;
}
String[] conf = args[0].split("=", 2);
if (conf.length != 2 || !conf[0].equalsIgnoreCase("--conf")) {
Log.error("Error: javacserver client: first argument must be --conf=<conf file>");
return null;
}
String confFileName = conf[1];
if (!Files.exists(Path.of(confFileName))) {
Log.error("Error: javacserver client: specified conf file does not exist");
return null;
}
return confFileName;
}
private static String getConfFileContent(String confFile) {
try {
List<String> confFileLines = Files.readAllLines(Path.of(confFile));
String confFileContent = String.join("\n", confFileLines);
return confFileContent;
} catch (IOException e) {
Log.error("Cannot read configuration file " + confFile);
Log.debug(e);
return null;
}
}
private static String getJavaCommandString(String confFileContent) {
String serverCommandString = getConfValue("javacmd", confFileContent);
if (serverCommandString.isEmpty()) {
Log.error("Configuration file missing value for 'javacmd'");
return null;
} else {
return serverCommandString;
}
}
private static String getPortFileName(String confFileContent) {
String portfileName = getConfValue("portfile", confFileContent);
if (portfileName.isEmpty()) {
Log.error("Configuration file missing value for 'portfile'");
return null;
} else {
return portfileName;
}
}
private static String getConfValue(String optionName, String content) {
String result;
int p = content.indexOf(optionName + "=");
if (p == -1) {
result = "";
} else {
p += optionName.length() + 1;
int pe = content.indexOf('\n', p);
if (pe == -1) pe = content.length();
result = content.substring(p, pe);
}
return result.strip();
}
}

View File

@ -1,80 +0,0 @@
/*
* Copyright (c) 2014, 2022, 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 javacserver.client;
import java.io.OutputStreamWriter;
import java.io.Writer;
import javacserver.AutoFlushWriter;
import javacserver.Log;
import javacserver.Result;
import javacserver.comp.SjavacImpl;
import javacserver.options.Options;
import javacserver.server.Sjavac;
/**
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public class ClientMain {
public static int run(String[] args) {
return run(args,
new AutoFlushWriter(new OutputStreamWriter(System.out)),
new AutoFlushWriter(new OutputStreamWriter(System.err)));
}
public static int run(String[] args, Writer out, Writer err) {
Log.setLogForCurrentThread(new Log(out, err));
Options options;
try {
options = Options.parseArgs(args);
} catch (IllegalArgumentException e) {
Log.error(e.getMessage());
return Result.CMDERR.exitCode;
}
Log.setLogLevel(options.getLogLevel());
// Prepare sjavac object
boolean useServer = options.getServerConf() != null;
Sjavac sjavac = useServer ? new SjavacClient(options) : new SjavacImpl();
// Perform compilation
Result result = sjavac.compile(args);
// If sjavac is running in the foreground we should shut it down at this point
if (!useServer) {
sjavac.shutdown();
}
return result.exitCode;
}
}

View File

@ -1,280 +0,0 @@
/*
* Copyright (c) 2014, 2022, 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 javacserver.client;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Reader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javacserver.Log;
import javacserver.Result;
import javacserver.Util;
import javacserver.options.Options;
import javacserver.server.PortFile;
import javacserver.server.Sjavac;
import javacserver.server.SjavacServer;
/**
* Sjavac implementation that delegates requests to a SjavacServer.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public class SjavacClient implements Sjavac {
private PortFile portFile;
// The servercmd option specifies how the server part of sjavac is spawned.
// It should point to a javacserver.Main that supports --startserver
private String serverCommand;
// Accept 120 seconds of inactivity before quitting.
private static final int KEEPALIVE = 120;
private static final int POOLSIZE = Runtime.getRuntime().availableProcessors();
// Wait 2 seconds for response, before giving up on javac server.
private static final int CONNECTION_TIMEOUT = 2000;
private static final int MAX_CONNECT_ATTEMPTS = 3;
private static final int WAIT_BETWEEN_CONNECT_ATTEMPTS = 2000;
public SjavacClient(Options options) {
String serverConf = options.getServerConf();
String configFile = Util.extractStringOption("conf", serverConf, "");
try {
List<String> configFileLines = Files.readAllLines(Path.of(configFile));
String configFileContent = String.join("\n", configFileLines);
String portfileName = Util.extractStringOptionLine("portfile", configFileContent, "");
if (portfileName.isEmpty()) {
Log.error("Configuration file missing value for 'portfile'");
portFile = null;
} else {
portFile = SjavacServer.getPortFile(portfileName);
}
String serverCommandString = Util.extractStringOptionLine("servercmd", configFileContent, "");
if (serverCommandString.isEmpty()) {
Log.error("Configuration file missing value for 'servercmd'");
serverCommand = null;
} else {
serverCommand = serverCommandString;
}
} catch (IOException e) {
Log.error("Cannot read configuration file " + configFile);
Log.debug(e);
portFile = null;
serverCommand = null;
}
}
@Override
public Result compile(String[] args) {
if (portFile == null || serverCommand == null) {
Log.error("Incorrect configuration, portfile and/or servercmd missing");
return Result.ERROR;
}
Result result = null;
try (Socket socket = tryConnect()) {
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// Send args array to server
out.println(args.length);
for (String arg : args)
out.println(arg);
out.flush();
// Read server response line by line
String line;
while (null != (line = in.readLine())) {
if (!line.contains(":")) {
throw new AssertionError("Could not parse protocol line: >>\"" + line + "\"<<");
}
String[] typeAndContent = line.split(":", 2);
String type = typeAndContent[0];
String content = typeAndContent[1];
try {
if (Log.isDebugging()) {
// Distinguish server generated output if debugging.
content = "[sjavac-server] " + content;
}
Log.log(Log.Level.valueOf(type), content);
continue;
} catch (IllegalArgumentException e) {
// Parsing of 'type' as log level failed.
}
if (type.equals(SjavacServer.LINE_TYPE_RC)) {
result = Result.valueOf(content);
}
}
} catch (PortFileInaccessibleException e) {
Log.error("Port file inaccessible.");
result = Result.ERROR;
} catch (IOException ioe) {
Log.error("IOException caught during compilation: " + ioe.getMessage());
Log.debug(ioe);
result = Result.ERROR;
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // Restore interrupt
Log.error("Compilation interrupted.");
Log.debug(ie);
result = Result.ERROR;
}
if (result == null) {
// No LINE_TYPE_RC was found.
result = Result.ERROR;
}
return result;
}
/*
* Makes MAX_CONNECT_ATTEMPTS attempts to connect to server.
*/
private Socket tryConnect() throws IOException, InterruptedException {
makeSureServerIsRunning();
int attempt = 0;
while (true) {
Log.debug("Trying to connect. Attempt " + (++attempt) + " of " + MAX_CONNECT_ATTEMPTS);
try {
return makeConnectionAttempt();
} catch (IOException ex) {
Log.error("Connection attempt failed: " + ex.getMessage());
if (attempt >= MAX_CONNECT_ATTEMPTS) {
Log.error("Giving up");
throw new IOException("Could not connect to server", ex);
}
}
Thread.sleep(WAIT_BETWEEN_CONNECT_ATTEMPTS);
}
}
private Socket makeConnectionAttempt() throws IOException {
Socket socket = new Socket();
InetAddress localhost = InetAddress.getByName(null);
InetSocketAddress address = new InetSocketAddress(localhost, portFile.getPort());
socket.connect(address, CONNECTION_TIMEOUT);
Log.debug("Connected");
return socket;
}
/*
* Will return immediately if a server already seems to be running,
* otherwise fork a new server and block until it seems to be running.
*/
private void makeSureServerIsRunning()
throws IOException, InterruptedException {
if (portFile.exists()) {
portFile.lock();
portFile.getValues();
portFile.unlock();
if (portFile.containsPortInfo()) {
// Server seems to already be running
return;
}
}
// Fork a new server and wait for it to start
startNewServer();
}
@Override
public void shutdown() {
// Nothing to clean up
}
/*
* Fork a server process process and wait for server to come around
*/
public void startNewServer()
throws IOException, InterruptedException {
List<String> cmd = new ArrayList<>();
cmd.addAll(Arrays.asList(serverCommand.split(" ")));
cmd.add("--startserver:"
+ "portfile=" + portFile.getFilename()
+ ",poolsize=" + POOLSIZE
+ ",keepalive="+ KEEPALIVE);
Process serverProcess;
Log.debug("Starting server. Command: " + String.join(" ", cmd));
try {
// If the cmd for some reason can't be executed (file is not found,
// or is not executable for instance) this will throw an
// IOException and p == null.
serverProcess = new ProcessBuilder(cmd)
.redirectErrorStream(true)
.start();
} catch (IOException ex) {
// Message is typically something like:
// Cannot run program "xyz": error=2, No such file or directory
Log.error("Failed to create server process: " + ex.getMessage());
Log.debug(ex);
throw new IOException(ex);
}
// serverProcess != null at this point.
try {
// Throws an IOException if no valid values materialize
portFile.waitForValidValues();
} catch (IOException ex) {
// Process was started, but server failed to initialize. This could
// for instance be due to the JVM not finding the server class,
// or the server running in to some exception early on.
Log.error("Sjavac server failed to initialize: " + ex.getMessage());
Log.error("Process output:");
Reader serverStdoutStderr = new InputStreamReader(serverProcess.getInputStream());
try (BufferedReader br = new BufferedReader(serverStdoutStderr)) {
br.lines().forEach(Log::error);
}
Log.error("<End of process output>");
try {
Log.error("Process exit code: " + serverProcess.exitValue());
} catch (IllegalThreadStateException e) {
// Server is presumably still running.
}
throw new IOException("Server failed to initialize: " + ex.getMessage(), ex);
}
}
}

View File

@ -1,78 +0,0 @@
/*
* Copyright (c) 2014, 2022, 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 javacserver.comp;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.stream.Stream;
import com.sun.tools.javac.Main;
import javacserver.Log;
import javacserver.Result;
import javacserver.Util;
import javacserver.options.Option;
import javacserver.server.Sjavac;
/**
* The sjavac implementation that interacts with javac and performs the actual
* compilation.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public class SjavacImpl implements Sjavac {
@Override
@SuppressWarnings("deprecated")
public Result compile(String[] args) {
// Direct logging to our byte array stream.
StringWriter strWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(strWriter);
// Prepare arguments
String[] passThroughArgs = Stream.of(args)
.filter(arg -> !arg.startsWith(Option.SERVER.arg))
.toArray(String[]::new);
// Compile
int exitcode = Main.compile(passThroughArgs, printWriter);
Result result = Result.of(exitcode);
// Process compiler output (which is always errors)
printWriter.flush();
Util.getLines(strWriter.toString()).forEach(Log::error);
return result;
}
@Override
public void shutdown() {
// Nothing to clean up
}
}

View File

@ -1,89 +0,0 @@
/*
* Copyright (c) 2014, 2022, 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 javacserver.options;
import java.util.Iterator;
/**
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public class ArgumentIterator implements Iterator<String> {
/** The underlying argument iterator */
private Iterator<String> iter;
/** Extra state used to implement peek and current */
private String current;
private String buffered;
public ArgumentIterator(Iterable<String> iter) {
this.iter = iter.iterator();
}
@Override
public boolean hasNext() {
return buffered != null || iter.hasNext();
}
@Override
public String next() {
fillBuffer();
current = buffered;
buffered = null;
return current;
}
/**
* @return the last element returned by next() (or {@code null} if next has
* never been invoked on this iterator).
*/
public String current() {
return current;
}
/** Can't remove current element, since we may have buffered it. */
@Override
public void remove() {
throw new UnsupportedOperationException();
}
/**
* @return Returns the next element without advancing the iterator
*/
public String peek() {
fillBuffer();
return buffered;
}
private void fillBuffer() {
if (buffered == null && iter.hasNext())
buffered = iter.next();
}
}

View File

@ -1,294 +0,0 @@
/*
* Copyright (c) 1999, 2022, 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 javacserver.options;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
/**
* Various utility methods for processing Java tool command line arguments.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public class CommandLine {
/**
* Process Win32-style command files for the specified command line
* arguments and return the resulting arguments. A command file argument
* is of the form '@file' where 'file' is the name of the file whose
* contents are to be parsed for additional arguments. The contents of
* the command file are parsed using StreamTokenizer and the original
* '@file' argument replaced with the resulting tokens. Recursive command
* files are not supported. The '@' character itself can be quoted with
* the sequence '@@'.
* @param args the arguments that may contain @files
* @return the arguments, with @files expanded
* @throws IOException if there is a problem reading any of the @files
*/
public static List<String> parse(List<String> args) throws IOException {
List<String> newArgs = new ArrayList<>();
appendParsedCommandArgs(newArgs, args);
return newArgs;
}
private static void appendParsedCommandArgs(List<String> newArgs, List<String> args) throws IOException {
for (String arg : args) {
if (arg.length() > 1 && arg.charAt(0) == '@') {
arg = arg.substring(1);
if (arg.charAt(0) == '@') {
newArgs.add(arg);
} else {
loadCmdFile(arg, newArgs);
}
} else {
newArgs.add(arg);
}
}
}
/**
* Process the given environment variable and appends any Win32-style
* command files for the specified command line arguments and return
* the resulting arguments. A command file argument
* is of the form '@file' where 'file' is the name of the file whose
* contents are to be parsed for additional arguments. The contents of
* the command file are parsed using StreamTokenizer and the original
* '@file' argument replaced with the resulting tokens. Recursive command
* files are not supported. The '@' character itself can be quoted with
* the sequence '@@'.
* @param envVariable the env variable to process
* @param args the arguments that may contain @files
* @return the arguments, with environment variable's content and expansion of @files
* @throws IOException if there is a problem reading any of the @files
* @throws UnmatchedQuote
*/
public static List<String> parse(String envVariable, List<String> args)
throws IOException, UnmatchedQuote {
List<String> inArgs = new ArrayList<>();
appendParsedEnvVariables(inArgs, envVariable);
inArgs.addAll(args);
List<String> newArgs = new ArrayList<>();
appendParsedCommandArgs(newArgs, inArgs);
return newArgs;
}
private static void loadCmdFile(String name, List<String> args) throws IOException {
try (Reader r = Files.newBufferedReader(Paths.get(name), Charset.defaultCharset())) {
Tokenizer t = new Tokenizer(r);
String s;
while ((s = t.nextToken()) != null) {
args.add(s);
}
}
}
public static class Tokenizer {
private final Reader in;
private int ch;
public Tokenizer(Reader in) throws IOException {
this.in = in;
ch = in.read();
}
public String nextToken() throws IOException {
skipWhite();
if (ch == -1) {
return null;
}
StringBuilder sb = new StringBuilder();
char quoteChar = 0;
while (ch != -1) {
switch (ch) {
case ' ':
case '\t':
case '\f':
if (quoteChar == 0) {
return sb.toString();
}
sb.append((char) ch);
break;
case '\n':
case '\r':
return sb.toString();
case '\'':
case '"':
if (quoteChar == 0) {
quoteChar = (char) ch;
} else if (quoteChar == ch) {
quoteChar = 0;
} else {
sb.append((char) ch);
}
break;
case '\\':
if (quoteChar != 0) {
ch = in.read();
switch (ch) {
case '\n':
case '\r':
while (ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t' || ch == '\f') {
ch = in.read();
}
continue;
case 'n':
ch = '\n';
break;
case 'r':
ch = '\r';
break;
case 't':
ch = '\t';
break;
case 'f':
ch = '\f';
break;
}
}
sb.append((char) ch);
break;
default:
sb.append((char) ch);
}
ch = in.read();
}
return sb.toString();
}
void skipWhite() throws IOException {
while (ch != -1) {
switch (ch) {
case ' ':
case '\t':
case '\n':
case '\r':
case '\f':
break;
case '#':
ch = in.read();
while (ch != '\n' && ch != '\r' && ch != -1) {
ch = in.read();
}
break;
default:
return;
}
ch = in.read();
}
}
}
@SuppressWarnings("fallthrough")
private static void appendParsedEnvVariables(List<String> newArgs, String envVariable)
throws UnmatchedQuote {
if (envVariable == null) {
return;
}
String in = System.getenv(envVariable);
if (in == null || in.trim().isEmpty()) {
return;
}
final char NUL = (char)0;
final int len = in.length();
int pos = 0;
StringBuilder sb = new StringBuilder();
char quote = NUL;
char ch;
loop:
while (pos < len) {
ch = in.charAt(pos);
switch (ch) {
case '\"': case '\'':
if (quote == NUL) {
quote = ch;
} else if (quote == ch) {
quote = NUL;
} else {
sb.append(ch);
}
pos++;
break;
case '\f': case '\n': case '\r': case '\t': case ' ':
if (quote == NUL) {
newArgs.add(sb.toString());
sb.setLength(0);
while (ch == '\f' || ch == '\n' || ch == '\r' || ch == '\t' || ch == ' ') {
pos++;
if (pos >= len) {
break loop;
}
ch = in.charAt(pos);
}
break;
}
// fall through
default:
sb.append(ch);
pos++;
}
}
if (sb.length() != 0) {
newArgs.add(sb.toString());
}
if (quote != NUL) {
throw new UnmatchedQuote(envVariable);
}
}
public static class UnmatchedQuote extends Exception {
private static final long serialVersionUID = 0;
public final String variableName;
UnmatchedQuote(String variable) {
this.variableName = variable;
}
}
}

View File

@ -1,100 +0,0 @@
/*
* Copyright (c) 2014, 2022, 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 javacserver.options;
/**
* Sjavac options can be classified as:
*
* (1) relevant only for sjavac, such as --server
* (2) relevant for sjavac and javac, such as -d, or
* (3) relevant only for javac, such as -g.
*
* This enum represents all options from (1) and (2). Note that instances of
* this enum only entail static information about the option. For storage of
* option values, refer to Options.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public enum Option {
SERVER("--server:", "Specify server configuration file of running server") {
@Override
protected void processMatching(ArgumentIterator iter, Options.ArgDecoderOptionHelper helper) {
helper.serverConf(iter.current().substring(arg.length()));
}
},
STARTSERVER("--startserver:", "Start server and use the given configuration file") {
@Override
protected void processMatching(ArgumentIterator iter, Options.ArgDecoderOptionHelper helper) {
helper.startServerConf(iter.current().substring(arg.length()));
}
};
public final String arg;
final String description;
private Option(String arg, String description) {
this.arg = arg;
this.description = description;
}
// Future cleanup: Change the "=" syntax to ":" syntax to be consistent and
// to follow the javac-option style.
public boolean hasOption() {
return arg.endsWith(":") || arg.endsWith("=");
}
/**
* Process current argument of argIter.
*
* It's final, since the option customization is typically done in
* processMatching.
*
* @param argIter Iterator to read current and succeeding arguments from.
* @param helper The helper to report back to.
* @return true iff the argument was processed by this option.
*/
public final boolean processCurrent(ArgumentIterator argIter,
Options.ArgDecoderOptionHelper helper) {
String fullArg = argIter.current(); // "-tr" or "-log=level"
if (hasOption() ? fullArg.startsWith(arg) : fullArg.equals(arg)) {
processMatching(argIter, helper);
return true;
}
// Did not match
return false;
}
/** Called by process if the current argument matches this option. */
protected abstract void processMatching(ArgumentIterator argIter,
Options.ArgDecoderOptionHelper helper);
}

View File

@ -1,121 +0,0 @@
/*
* Copyright (c) 2014, 2022, 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 javacserver.options;
import java.util.List;
/**
* Instances of this class represent values for sjavac command line options.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public class Options {
private String logLevel = "info";
private boolean startServer = false;
// Server configuration string
private String serverConf;
/** Get the log level. */
public String getLogLevel() {
return logLevel;
}
/** Return true iff a new server should be started */
public boolean startServerFlag() {
return startServer;
}
/** Return the server configuration string. */
public String getServerConf() {
return serverConf;
}
/**
* Parses the given argument array and returns a corresponding Options
* instance.
*/
public static Options parseArgs(String... args) {
Options options = new Options();
options.new ArgDecoderOptionHelper().traverse(args);
return options;
}
// OptionHelper that records the traversed options in this Options instance.
public class ArgDecoderOptionHelper {
public void reportError(String msg) {
throw new IllegalArgumentException(msg);
}
public void serverConf(String conf) {
if (serverConf != null)
reportError("Can not specify more than one server configuration.");
else
serverConf = conf;
}
public void startServerConf(String conf) {
if (serverConf != null)
reportError("Can not specify more than one server configuration.");
else {
startServer = true;
serverConf = conf;
}
}
/**
* Traverses an array of arguments and performs the appropriate callbacks.
*
* @param args the arguments to traverse.
*/
void traverse(String[] args) {
Iterable<String> allArgs;
try {
allArgs = CommandLine.parse(List.of(args)); // Detect @file and load it as a command line.
} catch (java.io.IOException e) {
throw new IllegalArgumentException("Problem reading @"+e.getMessage());
}
ArgumentIterator argIter = new ArgumentIterator(allArgs);
nextArg:
while (argIter.hasNext()) {
String arg = argIter.next();
if (arg.startsWith("-")) {
for (Option opt : Option.values()) {
if (opt.processCurrent(argIter, this))
continue nextArg;
}
}
}
}
}
}

View File

@ -23,54 +23,38 @@
* questions.
*/
package javacserver.comp;
package javacserver.server;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import javacserver.Log;
import javacserver.Result;
import javacserver.server.Sjavac;
import javacserver.util.Log;
/**
* An sjavac implementation that limits the number of concurrent calls by
* wrapping invocations in Callables and delegating them to a FixedThreadPool.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
* Use a fixed thread pool to limit the amount of concurrent javac compilation
* that can happen.
*/
public class PooledSjavac implements Sjavac {
public class CompilerThreadPool {
private static final int POOLSIZE = Runtime.getRuntime().availableProcessors();
final Sjavac delegate;
final ExecutorService pool;
private final ExecutorService pool;
public PooledSjavac(Sjavac delegate, int poolsize) {
Objects.requireNonNull(delegate);
this.delegate = delegate;
pool = Executors.newFixedThreadPool(poolsize);
public CompilerThreadPool() {
this.pool = Executors.newFixedThreadPool(POOLSIZE);
}
@Override
public Result compile(String[] args) {
public int dispatchCompilation(String[] args) {
Log log = Log.get();
try {
return pool.submit(() -> {
Log.setLogForCurrentThread(log);
return delegate.compile(args);
}).get();
return pool.submit(() -> Server.runCompiler(log, args)).get();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Error during compile", e);
}
}
@Override
public void shutdown() {
Log.debug("Shutting down PooledSjavac");
Log.debug("Shutting down javacserver thread pool");
pool.shutdown(); // Disable new tasks from being submitted
try {
// Wait a while for existing tasks to terminate
@ -78,16 +62,17 @@ public class PooledSjavac implements Sjavac {
pool.shutdownNow(); // Cancel currently executing tasks
// Wait a while for tasks to respond to being cancelled
if (!pool.awaitTermination(60, TimeUnit.SECONDS))
Log.error("ThreadPool did not terminate");
Log.error("Thread pool did not terminate");
}
} catch (InterruptedException ie) {
// (Re-)Cancel if current thread also interrupted
pool.shutdownNow();
// Preserve interrupt status
Thread.currentThread().interrupt();
// (Re-)Cancel if current thread also interrupted
pool.shutdownNow();
// Preserve interrupt status
Thread.currentThread().interrupt();
}
delegate.shutdown();
}
public int poolSize() {
return POOLSIZE;
}
}

View File

@ -27,53 +27,30 @@ package javacserver.server;
import java.util.Timer;
import java.util.TimerTask;
import javacserver.Log;
import javacserver.Result;
import java.util.function.Consumer;
import javacserver.util.RunnableTimerTask;
/**
* An sjavac implementation that keeps track of idleness and shuts down the
* given Terminable upon idleness timeout.
*
* An idleness timeout kicks in {@code idleTimeout} milliseconds after the last
* request is completed.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
* Monitors the javacserver daemon, shutting it down if it recieves no new requests
* after a certain amount of time.
*/
public class IdleResetSjavac implements Sjavac {
public class IdleMonitor {
// Accept 120 seconds of inactivity before quitting.
private static final int KEEPALIVE = 120;
private final Sjavac delegate;
private final Terminable toShutdown;
private final Consumer<String> onShutdown;
private final Timer idlenessTimer = new Timer();
private final long idleTimeout;
private int outstandingCalls = 0;
// Class invariant: idlenessTimerTask != null <-> idlenessTimerTask is scheduled
private TimerTask idlenessTimerTask;
public IdleResetSjavac(Sjavac delegate,
Terminable toShutdown,
long idleTimeout) {
this.delegate = delegate;
this.toShutdown = toShutdown;
this.idleTimeout = idleTimeout;
public IdleMonitor(Consumer<String> onShutdown) {
this.onShutdown = onShutdown;
scheduleTimeout();
}
@Override
public Result compile(String[] args) {
startCall();
try {
return delegate.compile(args);
} finally {
endCall();
}
}
private synchronized void startCall() {
public synchronized void startCall() {
// Was there no outstanding calls before this call?
if (++outstandingCalls == 1) {
// Then the timer task must have been scheduled
@ -85,7 +62,7 @@ public class IdleResetSjavac implements Sjavac {
}
}
private synchronized void endCall() {
public synchronized void endCall() {
if (--outstandingCalls == 0) {
// No more outstanding calls. Schedule timeout.
scheduleTimeout();
@ -95,19 +72,14 @@ public class IdleResetSjavac implements Sjavac {
private void scheduleTimeout() {
if (idlenessTimerTask != null)
throw new IllegalStateException("Idle timeout already scheduled");
idlenessTimerTask = new TimerTask() {
public void run() {
Log.setLogForCurrentThread(ServerMain.getErrorLog());
toShutdown.shutdown("Server has been idle for " + (idleTimeout / 1000) + " seconds.");
}
};
idlenessTimer.schedule(idlenessTimerTask, idleTimeout);
idlenessTimerTask = new RunnableTimerTask(() -> {
Server.restoreServerErrorLog();
onShutdown.accept("Server has been idle for " + KEEPALIVE + " seconds.");
});
idlenessTimer.schedule(idlenessTimerTask, KEEPALIVE * 1000);
}
@Override
public void shutdown() {
idlenessTimer.cancel();
delegate.shutdown();
}
}

View File

@ -27,68 +27,61 @@ package javacserver.server;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
import javacserver.Log;
import java.util.function.Consumer;
import javacserver.shared.PortFile;
import javacserver.util.Log;
import javacserver.util.RunnableTimerTask;
/**
* Monitors the presence of a port file and shuts down the given SjavacServer
* Monitors the presence of a port file and shuts down the server
* whenever the port file is deleted or invalidated.
*
* TODO: JDK-8046882
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public class PortFileMonitor {
// Check if the portfile is gone, every 5 seconds.
private static final int CHECK_PORTFILE_INTERVAL = 5000;
private final Timer timer = new Timer();
private final PortFile portFile;
private final SjavacServer server;
private final Consumer<String> onShutdown;
public PortFileMonitor(PortFile portFile,
SjavacServer server) {
Consumer<String> onShutdown) {
this.portFile = portFile;
this.server = server;
this.onShutdown = onShutdown;
}
public void start() {
Log log = Log.get();
TimerTask shutdownCheck = new TimerTask() {
public void run() {
Log.setLogForCurrentThread(log);
Log.debug("Checking port file status...");
try {
if (!portFile.exists()) {
// Time to quit because the portfile was deleted by another
// process, probably by the makefile that is done building.
server.shutdown("Quitting because portfile was deleted!");
} else if (portFile.markedForStop()) {
// Time to quit because another process touched the file
// server.port.stop to signal that the server should stop.
// This is necessary on some operating systems that lock
// the port file hard!
server.shutdown("Quitting because a portfile.stop file was found!");
} else if (!portFile.stillMyValues()) {
// Time to quit because another build has started.
server.shutdown("Quitting because portfile is now owned by another javac server!");
}
} catch (IOException e) {
Log.error("IOException caught in PortFileMonitor.");
Log.debug(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error(e);
}
}
};
timer.schedule(new RunnableTimerTask(() -> checkPortFile(log)), 0, CHECK_PORTFILE_INTERVAL);
}
timer.schedule(shutdownCheck, 0, CHECK_PORTFILE_INTERVAL);
private void checkPortFile(Log log) {
Log.setLogForCurrentThread(log);
Log.debug("Checking port file status...");
try {
if (!portFile.exists()) {
// Time to quit because the portfile was deleted by another
// process, probably by the makefile that is done building.
onShutdown.accept("Quitting because portfile was deleted!");
} else if (portFile.markedForStop()) {
// Time to quit because another process touched the file
// server.port.stop to signal that the server should stop.
// This is necessary on some operating systems that lock
// the port file hard!
onShutdown.accept("Quitting because a portfile.stop file was found!");
} else if (!portFile.stillMyValues()) {
// Time to quit because another build has started.
onShutdown.accept("Quitting because portfile is now owned by another javac server!");
}
} catch (IOException e) {
Log.error("IOException caught in PortFileMonitor.");
Log.debug(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error(e);
}
}
public void shutdown() {

View File

@ -1,128 +0,0 @@
/*
* Copyright (c) 2014, 2022, 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 javacserver.server;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.nio.file.Path;
import javacserver.Log;
import javacserver.Result;
import javacserver.Util;
import static javacserver.server.SjavacServer.LINE_TYPE_RC;
/**
* A RequestHandler handles requests performed over a socket. Specifically it
* - Reads the command string specifying which method is to be invoked
* - Reads the appropriate arguments
* - Delegates the actual invocation to the given sjavac implementation
* - Writes the result back to the socket output stream
*
* None of the work performed by this class is really bound by the CPU. It
* should be completely fine to have a large number of RequestHandlers active.
* To limit the number of concurrent compilations, use PooledSjavac.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public class RequestHandler extends Thread {
private final Socket socket;
private final Sjavac sjavac;
public RequestHandler(Socket socket, Sjavac sjavac) {
this.socket = socket;
this.sjavac = sjavac;
}
@Override
public void run() {
try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
// Set up logging for this thread. Stream back logging messages to
// client on the format format "level:msg".
Log.setLogForCurrentThread(new Log(out, out) {
@Override
protected boolean isLevelLogged(Level l) {
// Make sure it is up to the client to decide whether or
// not this message should be displayed.
return true;
}
@Override
protected void printLogMsg(Level msgLevel, String msg) {
// Follow sjavac server/client protocol: Send one line
// at a time and prefix with message with "level:".
Util.getLines(msg)
.map(line -> msgLevel + ":" + line)
.forEach(line -> super.printLogMsg(msgLevel, line));
}
});
// Read argument array
int n = Integer.parseInt(in.readLine());
String[] args = new String[n];
for (int i = 0; i < n; i++) {
args[i] = in.readLine();
}
// If there has been any internal errors, notify client
checkInternalErrorLog();
// Perform compilation
Result rc = sjavac.compile(args);
// Send return code back to client
out.println(LINE_TYPE_RC + ":" + rc.name());
// Check for internal errors again.
checkInternalErrorLog();
} catch (Exception ex) {
// Not much to be done at this point. The client side request
// code will most likely throw an IOException and the
// compilation will fail.
Log.error(ex);
} finally {
Log.setLogForCurrentThread(null);
}
}
private void checkInternalErrorLog() {
Path errorLog = ServerMain.getErrorLog().getLogDestination();
if (errorLog != null) {
Log.error("Server has encountered an internal error. See " + errorLog.toAbsolutePath()
+ " for details.");
}
}
}

View File

@ -0,0 +1,281 @@
/*
* Copyright (c) 2011, 2022, 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 javacserver.server;
import com.sun.tools.javac.Main;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.nio.file.Path;
import java.util.Random;
import java.util.concurrent.atomic.AtomicBoolean;
import javacserver.shared.PortFile;
import javacserver.shared.Protocol;
import javacserver.shared.Result;
import javacserver.util.LazyInitFileLog;
import javacserver.util.Log;
import javacserver.util.LoggingOutputStream;
import javacserver.util.Util;
/**
* Start a new server main thread, that will listen to incoming connection requests from the client,
* and dispatch these on to worker threads in a thread pool, running javac.
*/
public class Server {
private ServerSocket serverSocket;
private PortFile portFile;
private PortFileMonitor portFileMonitor;
private IdleMonitor idleMonitor;
private CompilerThreadPool compilerThreadPool;
// Set to false break accept loop
final AtomicBoolean keepAcceptingRequests = new AtomicBoolean();
// For logging server internal (non request specific) errors.
private static LazyInitFileLog errorLog;
public static void main(String... args) {
initLogging();
try {
PortFile portFile = getPortFileFromArguments(args);
if (portFile == null) {
System.exit(Result.CMDERR.exitCode);
return;
}
Server server = new Server(portFile);
if (!server.start()) {
System.exit(Result.ERROR.exitCode);
} else {
System.exit(Result.OK.exitCode);
}
} catch (IOException | InterruptedException ex) {
ex.printStackTrace();
System.exit(Result.ERROR.exitCode);
}
}
private static void initLogging() {
// Under normal operation, all logging messages generated server-side
// are due to compilation requests. These logging messages should
// be relayed back to the requesting client rather than written to the
// server log. The only messages that should be written to the server
// log (in production mode) should be errors,
errorLog = new LazyInitFileLog("server.log");
Log.setLogForCurrentThread(errorLog);
Log.setLogLevel(Log.Level.ERROR); // should be set to ERROR.
// Make sure no exceptions go under the radar
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
restoreServerErrorLog();
Log.error(e);
});
// Inevitably someone will try to print messages using System.{out,err}.
// Make sure this output also ends up in the log.
System.setOut(new PrintStream(new LoggingOutputStream(System.out, Log.Level.INFO, "[stdout] ")));
System.setErr(new PrintStream(new LoggingOutputStream(System.err, Log.Level.ERROR, "[stderr] ")));
}
private static PortFile getPortFileFromArguments(String[] args) {
if (args.length != 1) {
Log.error("javacserver daemon incorrectly called");
return null;
}
String portfilename = args[0];
PortFile portFile = new PortFile(portfilename);
return portFile;
}
public Server(PortFile portFile) throws FileNotFoundException {
this.portFile = portFile;
}
/**
* Start the daemon, unless another one is already running, in which it returns
* false and exits immediately.
*/
private boolean start() throws IOException, InterruptedException {
// The port file is locked and the server port and cookie is written into it.
portFile.lock();
portFile.getValues();
if (portFile.containsPortInfo()) {
Log.debug("javacserver daemon not started because portfile exists!");
portFile.unlock();
return false;
}
serverSocket = new ServerSocket();
InetAddress localhost = InetAddress.getByName(null);
serverSocket.bind(new InetSocketAddress(localhost, 0));
// At this point the server accepts connections, so it is now safe
// to publish the port / cookie information
// The secret cookie shared between server and client through the port file.
// Used to prevent clients from believing that they are communicating with
// an old server when a new server has started and reused the same port as
// an old server.
long myCookie = new Random().nextLong();
portFile.setValues(serverSocket.getLocalPort(), myCookie);
portFile.unlock();
portFileMonitor = new PortFileMonitor(portFile, this::shutdownServer);
portFileMonitor.start();
compilerThreadPool = new CompilerThreadPool();
idleMonitor = new IdleMonitor(this::shutdownServer);
Log.debug("javacserver daemon started. Accepting connections...");
Log.debug(" port: " + serverSocket.getLocalPort());
Log.debug(" time: " + new java.util.Date());
Log.debug(" poolsize: " + compilerThreadPool.poolSize());
keepAcceptingRequests.set(true);
do {
try {
Socket socket = serverSocket.accept();
// Handle each incoming request in a separate thread. This is just for socket communication,
// the actual compilation will be done by the threadpool.
Thread requestHandler = new Thread(() -> handleRequest(socket));
requestHandler.start();
} catch (SocketException se) {
// Caused by serverSocket.close() and indicates shutdown
}
} while (keepAcceptingRequests.get());
Log.debug("Shutting down.");
// No more connections accepted. If any client managed to connect after
// the accept() was interrupted but before the server socket is closed
// here, any attempt to read or write to the socket will result in an
// IOException on the client side.
// Shut down
idleMonitor.shutdown();
compilerThreadPool.shutdown();
return true;
}
private void handleRequest(Socket socket) {
try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
try {
idleMonitor.startCall();
// Set up logging for this thread. Stream back logging messages to
// client on the format "level:msg".
Log.setLogForCurrentThread(new Protocol.ProtocolLog(out));
String[] args = Protocol.readCommand(in);
// If there has been any internal errors, notify client
checkInternalErrorLog();
// Perform compilation. This will call runCompiler() on a
// thread in the thread pool
int exitCode = compilerThreadPool.dispatchCompilation(args);
Protocol.sendExitCode(out, exitCode);
// Check for internal errors again.
checkInternalErrorLog();
} finally {
idleMonitor.endCall();
}
} catch (Exception ex) {
// Not much to be done at this point. The client side request
// code will most likely throw an IOException and the
// compilation will fail.
Log.error(ex);
} finally {
Log.setLogForCurrentThread(null);
}
}
@SuppressWarnings("deprecated")
public static int runCompiler(Log log, String[] args) {
Log.setLogForCurrentThread(log);
// Direct logging to our byte array stream.
StringWriter strWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(strWriter);
// Compile
int exitcode = Main.compile(args, printWriter);
// Process compiler output (which is always errors)
printWriter.flush();
Util.getLines(strWriter.toString()).forEach(Log::error);
return exitcode;
}
private void checkInternalErrorLog() {
Path errorLogPath = errorLog.getLogDestination();
if (errorLogPath != null) {
Log.error("Server has encountered an internal error. See " + errorLogPath.toAbsolutePath()
+ " for details.");
}
}
public static void restoreServerErrorLog() {
Log.setLogForCurrentThread(errorLog);
}
public void shutdownServer(String quitMsg) {
if (!keepAcceptingRequests.compareAndSet(true, false)) {
// Already stopped, no need to shut down again
return;
}
Log.debug("Quitting: " + quitMsg);
portFileMonitor.shutdown(); // No longer any need to monitor port file
// Unpublish port before shutting down socket to minimize the number of
// failed connection attempts
try {
portFile.delete();
} catch (IOException | InterruptedException e) {
Log.error(e);
}
try {
serverSocket.close();
} catch (IOException e) {
Log.error(e);
}
}
}

View File

@ -1,92 +0,0 @@
/*
* Copyright (c) 2014, 2022, 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 javacserver.server;
import java.io.IOException;
import java.io.PrintStream;
import javacserver.Log;
import javacserver.Result;
import javacserver.server.log.LazyInitFileLog;
import javacserver.server.log.LoggingOutputStream;
import static javacserver.Log.Level.ERROR;
import static javacserver.Log.Level.INFO;
/**
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public class ServerMain {
// For logging server internal (non request specific) errors.
private static LazyInitFileLog errorLog;
public static int run(String[] args) {
// Under normal operation, all logging messages generated server-side
// are due to compilation requests. These logging messages should
// be relayed back to the requesting client rather than written to the
// server log. The only messages that should be written to the server
// log (in production mode) should be errors,
Log.setLogForCurrentThread(errorLog = new LazyInitFileLog("server.log"));
Log.setLogLevel(ERROR); // should be set to ERROR.
// Make sure no exceptions go under the radar
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
Log.setLogForCurrentThread(errorLog);
Log.error(e);
});
// Inevitably someone will try to print messages using System.{out,err}.
// Make sure this output also ends up in the log.
System.setOut(new PrintStream(new LoggingOutputStream(System.out, INFO, "[stdout] ")));
System.setErr(new PrintStream(new LoggingOutputStream(System.err, ERROR, "[stderr] ")));
// Any options other than --startserver?
if (args.length > 1) {
Log.error("When spawning a background server, only a single --startserver argument is allowed.");
return Result.CMDERR.exitCode;
}
int exitCode;
try {
SjavacServer server = new SjavacServer(args[0]);
exitCode = server.startServer();
} catch (IOException | InterruptedException ex) {
ex.printStackTrace();
exitCode = Result.ERROR.exitCode;
}
return exitCode;
}
public static LazyInitFileLog getErrorLog() {
return errorLog;
}
}

View File

@ -1,248 +0,0 @@
/*
* Copyright (c) 2011, 2022, 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 javacserver.server;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.AtomicBoolean;
import javacserver.Log;
import javacserver.Result;
import javacserver.Util;
import javacserver.comp.PooledSjavac;
import javacserver.comp.SjavacImpl;
/**
* The JavacServer class contains methods both to setup a server that responds to requests and methods to connect to this server.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public class SjavacServer implements Terminable {
// Prefix of line containing return code.
public static final String LINE_TYPE_RC = "RC";
private final String portfilename;
private final int poolsize;
private final int keepalive;
// The secret cookie shared between server and client through the port file.
// Used to prevent clients from believing that they are communicating with
// an old server when a new server has started and reused the same port as
// an old server.
private final long myCookie;
// Accumulated build time, not counting idle time, used for logging purposes
private long totalBuildTime;
// The sjavac implementation to delegate requests to
Sjavac sjavac;
private ServerSocket serverSocket;
private PortFile portFile;
private PortFileMonitor portFileMonitor;
// Set to false break accept loop
final AtomicBoolean keepAcceptingRequests = new AtomicBoolean();
// For the client, all port files fetched, one per started javac server.
// Though usually only one javac server is started by a client.
private static Map<String, PortFile> allPortFiles;
public SjavacServer(String settings) throws FileNotFoundException {
this(Util.extractStringOption("portfile", settings),
Util.extractIntOption("poolsize", settings, Runtime.getRuntime().availableProcessors()),
Util.extractIntOption("keepalive", settings, 120));
}
public SjavacServer(String portfilename,
int poolsize,
int keepalive)
throws FileNotFoundException {
this.portfilename = portfilename;
this.poolsize = poolsize;
this.keepalive = keepalive;
this.myCookie = new Random().nextLong();
}
/**
* Acquire the port file. Synchronized since several threads inside an smart javac wrapper client acquires the same port file at the same time.
*/
public static synchronized PortFile getPortFile(String filename) {
if (allPortFiles == null) {
allPortFiles = new HashMap<>();
}
PortFile pf = allPortFiles.get(filename);
// Port file known. Does it still exist?
if (pf != null) {
try {
if (!pf.exists())
pf = null;
} catch (IOException ioex) {
ioex.printStackTrace();
}
}
if (pf == null) {
pf = new PortFile(filename);
allPortFiles.put(filename, pf);
}
return pf;
}
/**
* Get the cookie used for this server.
*/
long getCookie() {
return myCookie;
}
/**
* Get the port used for this server.
*/
int getPort() {
return serverSocket.getLocalPort();
}
/**
* Sum up the total build time for this javac server.
*/
public void addBuildTime(long inc) {
totalBuildTime += inc;
}
/**
* Start a server using a settings string. Typically: "--startserver:portfile=/tmp/myserver,poolsize=3" and the string "portfile=/tmp/myserver,poolsize=3"
* is sent as the settings parameter. Returns 0 on success, -1 on failure.
*/
public int startServer() throws IOException, InterruptedException {
long serverStart = System.currentTimeMillis();
// The port file is locked and the server port and cookie is written into it.
portFile = getPortFile(portfilename);
synchronized (portFile) {
portFile.lock();
portFile.getValues();
if (portFile.containsPortInfo()) {
Log.debug("Javac server not started because portfile exists!");
portFile.unlock();
return Result.ERROR.exitCode;
}
// .-----------. .--------. .------.
// socket -->| IdleReset |-->| Pooled |-->| Impl |--> javac
// '-----------' '--------' '------'
sjavac = new SjavacImpl();
sjavac = new PooledSjavac(sjavac, poolsize);
sjavac = new IdleResetSjavac(sjavac,
this,
keepalive * 1000);
serverSocket = new ServerSocket();
InetAddress localhost = InetAddress.getByName(null);
serverSocket.bind(new InetSocketAddress(localhost, 0));
// At this point the server accepts connections, so it is now safe
// to publish the port / cookie information
portFile.setValues(getPort(), getCookie());
portFile.unlock();
}
portFileMonitor = new PortFileMonitor(portFile, this);
portFileMonitor.start();
Log.debug("Sjavac server started. Accepting connections...");
Log.debug(" port: " + getPort());
Log.debug(" time: " + new java.util.Date());
Log.debug(" poolsize: " + poolsize);
keepAcceptingRequests.set(true);
do {
try {
Socket socket = serverSocket.accept();
new RequestHandler(socket, sjavac).start();
} catch (SocketException se) {
// Caused by serverSocket.close() and indicates shutdown
}
} while (keepAcceptingRequests.get());
Log.debug("Shutting down.");
// No more connections accepted. If any client managed to connect after
// the accept() was interrupted but before the server socket is closed
// here, any attempt to read or write to the socket will result in an
// IOException on the client side.
long realTime = System.currentTimeMillis() - serverStart;
Log.debug("Total wall clock time " + realTime + "ms build time " + totalBuildTime + "ms");
// Shut down
sjavac.shutdown();
return Result.OK.exitCode;
}
@Override
public void shutdown(String quitMsg) {
if (!keepAcceptingRequests.compareAndSet(true, false)) {
// Already stopped, no need to shut down again
return;
}
Log.debug("Quitting: " + quitMsg);
portFileMonitor.shutdown(); // No longer any need to monitor port file
// Unpublish port before shutting down socket to minimize the number of
// failed connection attempts
try {
portFile.delete();
} catch (IOException | InterruptedException e) {
Log.error(e);
}
try {
serverSocket.close();
} catch (IOException e) {
Log.error(e);
}
}
}

View File

@ -23,7 +23,7 @@
* questions.
*/
package javacserver.server;
package javacserver.shared;
import java.io.File;
import java.io.FileNotFoundException;
@ -34,23 +34,15 @@ import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.FileLockInterruptionException;
import java.util.concurrent.Semaphore;
import javacserver.Log;
import javacserver.client.PortFileInaccessibleException;
import javacserver.util.Log;
/**
* The PortFile class mediates access to a short binary file containing the tcp/ip port (for the localhost)
* and a cookie necessary for the server answering on that port. The file can be locked using file system
* primitives to avoid race conditions when several javac clients are started at the same. Note that file
* system locking is not always supported on a all operating systems and/or file systems.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
* system locking is not always supported on all operating systems and/or file systems.
*/
public class PortFile {
// Port file format:
// byte ordering: high byte first = big endian
// Magic nr, 4 byte int, first in file.
@ -225,6 +217,19 @@ public class PortFile {
lockSem.release();
}
public boolean hasValidValues() throws IOException, InterruptedException {
if (exists()) {
lock();
getValues();
unlock();
if (containsPortInfo()) {
return true;
}
}
return false;
}
/**
* Wait for the port file to contain values that look valid.
*/
@ -277,7 +282,7 @@ public class PortFile {
continue;
}
catch (ClosedChannelException e) {
// The channel has been closed since sjavac is exiting.
// The channel has been closed since the server is exiting.
return false;
}
}

View File

@ -23,7 +23,7 @@
* questions.
*/
package javacserver.client;
package javacserver.shared;
import java.io.IOException;

View File

@ -0,0 +1,144 @@
/*
* Copyright (c) 2014, 2022, 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 javacserver.shared;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import javacserver.util.Log;
import javacserver.util.Util;
/**
* Implementation of the wire protocol used by the javacserver client and daemon to communicate.
* Basically, the client sends the argument to javac, one line per string. The server responds
* with log lines (if there is any output), and the exit code from javac.
*/
public class Protocol {
// Prefix of line containing return code.
private static final String LINE_TYPE_RC = "RC";
public static void sendCommand(PrintWriter out, String[] args) throws IOException {
// Send args array to server
out.println(args.length);
for (String arg : args)
out.println(arg);
out.flush();
}
public static String[] readCommand(BufferedReader in) throws IOException {
// Read argument array
int n = Integer.parseInt(in.readLine());
String[] args = new String[n];
for (int i = 0; i < n; i++) {
args[i] = in.readLine();
}
return args;
}
public static void sendExitCode(PrintWriter out, int exitCode) {
// Send return code back to client
out.println(LINE_TYPE_RC + ":" + exitCode);
}
public static int readResponse(BufferedReader in) throws IOException {
// Read server response line by line
String line;
while (null != (line = in.readLine())) {
Line parsedLine = new Line(line);
try {
String content = parsedLine.getContent();
if (Log.isDebugging()) {
// Distinguish server generated output if debugging.
content = "[javacserver] " + content;
}
Log.log(Log.Level.valueOf(parsedLine.getType()), content);
continue;
} catch (IllegalArgumentException e) {
// Parsing of 'type' as log level failed.
}
if (parsedLine.isExitCode()) {
return parsedLine.getExitCode();
}
}
// No exit code was found.
return Result.ERROR.exitCode;
}
public static class Line {
private final String type;
public String getType() {
return type;
}
public String getContent() {
return content;
}
public boolean isExitCode() {
return type.equals(LINE_TYPE_RC);
}
public int getExitCode() {
return Integer.parseInt(content);
}
private final String content;
public Line(String line) {
if (!line.contains(":")) {
throw new AssertionError("Could not parse protocol line: >>\"" + line + "\"<<");
}
String[] typeAndContent = line.split(":", 2);
type = typeAndContent[0];
content = typeAndContent[1];
}
}
public static class ProtocolLog extends Log {
public ProtocolLog(PrintWriter out) {
super(out, out);
}
@Override
protected boolean isLevelLogged(Level l) {
// Make sure it is up to the client to decide whether or
// not this message should be displayed.
return true;
}
@Override
protected void printLogMsg(Level msgLevel, String msg) {
// Follow the server/client protocol: Send one line
// at a time and prefix with message with "level:".
Util.getLines(msg)
.map(line -> msgLevel + ":" + line)
.forEach(line -> super.printLogMsg(msgLevel, line));
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1999, 2022, 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
@ -23,14 +23,19 @@
* questions.
*/
package javacserver.server;
package javacserver.shared;
/**
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
* Result codes.
*/
public interface Terminable {
void shutdown(String quitMsg);
public enum Result {
OK(0), // Compilation completed with no errors.
ERROR(1), // Completed but reported errors.
CMDERR(2); // Bad command-line arguments
public final int exitCode;
Result(int exitCode) {
this.exitCode = exitCode;
}
}

View File

@ -23,14 +23,13 @@
* questions.
*/
package javacserver;
package javacserver.util;
import java.io.FilterWriter;
import java.io.IOException;
import java.io.Writer;
public class AutoFlushWriter extends FilterWriter {
public AutoFlushWriter(Writer out) {
super(out);
}

View File

@ -23,7 +23,7 @@
* questions.
*/
package javacserver.server.log;
package javacserver.util;
import java.io.FileWriter;
import java.io.IOException;
@ -32,10 +32,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import javacserver.Log;
public class LazyInitFileLog extends Log {
String baseFilename;
Path destination = null;

View File

@ -23,17 +23,16 @@
* questions.
*/
package javacserver;
package javacserver.util;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Locale;
/**
* Utility class only for sjavac logging.
* Utility class only for javacserver logging.
*
* Logging in sjavac has special requirements when running in server/client
* Logging in javacserver has special requirements when running in server/client
* mode. Most of the log messages is generated server-side, but the server
* is typically spawned by the client in the background, so the user usually
* does not see the server stdout/stderr. For this reason log messages needs
@ -42,16 +41,9 @@ import java.util.Locale;
* instance so that each connected client can have its own instance that
* relays messages back to the requesting client.
*
* On the client-side (or when running sjavac without server-mode) there will
* typically just be one Log instance.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
* On the client-side there will typically just be one Log instance.
*/
public class Log {
public enum Level {
ERROR,
WARN,
@ -61,7 +53,7 @@ public class Log {
}
private static Log stdOutErr = new Log(new PrintWriter(System.out), new PrintWriter(System.err));
private static ThreadLocal<Log> loggers = new ThreadLocal<>();
private static ThreadLocal<Log> logger = new ThreadLocal<>();
protected PrintWriter err; // Used for error and warning messages
protected PrintWriter out; // Used for other messages
@ -73,31 +65,19 @@ public class Log {
}
public static void setLogForCurrentThread(Log log) {
loggers.set(log);
}
public static void setLogLevel(String l) {
setLogLevel(Level.valueOf(l.toUpperCase(Locale.US)));
logger.set(log);
}
public static void setLogLevel(Level l) {
get().level = l;
}
public static void trace(String msg) {
log(Level.TRACE, msg);
}
public static void debug(String msg) {
log(Level.DEBUG, msg);
}
public static void info(String msg) {
log(Level.INFO, msg);
}
public static void warn(String msg) {
log(Level.WARN, msg);
public static void debug(Throwable t) {
log(Level.DEBUG, t);
}
public static void error(String msg) {
@ -112,10 +92,6 @@ public class Log {
get().printLogMsg(l, msg);
}
public static void debug(Throwable t) {
log(Level.DEBUG, t);
}
public static void log(Level l, Throwable t) {
StringWriter sw = new StringWriter();
t.printStackTrace(new PrintWriter(sw, true));
@ -131,7 +107,7 @@ public class Log {
}
public static Log get() {
Log log = loggers.get();
Log log = logger.get();
return log != null ? log : stdOutErr;
}

View File

@ -23,17 +23,14 @@
* questions.
*/
package javacserver.server.log;
package javacserver.util;
import java.io.ByteArrayOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import javacserver.Log;
public class LoggingOutputStream extends FilterOutputStream {
private static final byte[] LINE_SEP = System.lineSeparator().getBytes();
private final Log.Level level;

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 1999, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2022, 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
@ -23,34 +23,22 @@
* questions.
*/
package javacserver;
package javacserver.util;
/** Result codes.
import java.util.TimerTask;
/**
* Wrapper class since TimerTask is not up to modern standards
*/
public enum Result {
OK(0), // Compilation completed with no errors.
ERROR(1), // Completed but reported errors.
CMDERR(2), // Bad command-line arguments
SYSERR(3), // System error or resource exhaustion.
ABNORMAL(4); // Compiler terminated abnormally
public class RunnableTimerTask extends TimerTask {
private final Runnable task;
Result(int exitCode) {
this.exitCode = exitCode;
public RunnableTimerTask(Runnable task) {
this.task = task;
}
public static Result of(int exitcode) {
for (Result result : values()) {
if (result.exitCode == exitcode) {
return result;
}
}
return ABNORMAL;
@Override
public void run() {
task.run();
}
public boolean isOK() {
return (exitCode == 0);
}
public final int exitCode;
}

View File

@ -23,20 +23,18 @@
* questions.
*/
package javacserver.server;
package javacserver.util;
import javacserver.Result;
import java.util.regex.Pattern;
import java.util.stream.Stream;
/**
* Interface of the SjavacImpl, the sjavac client and all wrappers such as
* PooledSjavac etc.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public interface Sjavac {
Result compile(String[] args);
void shutdown();
public class Util {
/**
* Return a stream of strings, where the input string is split at line separators.
*/
public static Stream<String> getLines(String str) {
return str.isEmpty()
? Stream.empty()
: Stream.of(str.split(Pattern.quote(System.lineSeparator())));
}
}