/* * 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 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); } } }