jdk-24/make/langtools/tools/javacserver/server/Server.java
2024-04-09 14:10:58 +00:00

284 lines
10 KiB
Java

/*
* 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.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.Optional;
import java.util.Random;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.spi.ToolProvider;
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 threapool thread
compilerThreadPool.execute(() -> handleRequest(socket));
} 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
int exitCode = runCompiler(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.
ex.printStackTrace();
Log.error(ex);
} finally {
Log.setLogForCurrentThread(null);
}
}
public static int runCompiler(String[] args) {
// Direct logging to our byte array stream.
StringWriter strWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(strWriter);
// Compile
Optional<ToolProvider> tool = ToolProvider.findFirst("javac");
if (tool.isEmpty()) {
Log.error("Can't find tool javac");
return Result.ERROR.exitCode;
}
int exitcode = tool.get().run(printWriter, printWriter, args);
// 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);
}
}
}