1 /*
2  * Copyright (c) 2011, 2016, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package com.sun.tools.sjavac.server;
27 
28 import java.io.FileNotFoundException;
29 import java.io.FileWriter;
30 import java.io.IOException;
31 import java.io.PrintStream;
32 import java.io.PrintWriter;
33 import java.net.InetAddress;
34 import java.net.InetSocketAddress;
35 import java.net.ServerSocket;
36 import java.net.Socket;
37 import java.net.SocketException;
38 import java.util.HashMap;
39 import java.util.Map;
40 import java.util.Random;
41 import java.util.concurrent.atomic.AtomicBoolean;
42 
43 import com.sun.tools.javac.main.Main;
44 import com.sun.tools.javac.main.Main.Result;
45 import com.sun.tools.sjavac.Log;
46 import com.sun.tools.sjavac.Util;
47 import com.sun.tools.sjavac.client.PortFileInaccessibleException;
48 import com.sun.tools.sjavac.comp.PooledSjavac;
49 import com.sun.tools.sjavac.comp.SjavacImpl;
50 
51 /**
52  * The JavacServer class contains methods both to setup a server that responds to requests and methods to connect to this server.
53  *
54  *  <p><b>This is NOT part of any supported API.
55  *  If you write code that depends on this, you do so at your own risk.
56  *  This code and its internal interfaces are subject to change or
57  *  deletion without notice.</b>
58  */
59 public class SjavacServer implements Terminable {
60 
61     // Prefix of line containing return code.
62     public static final String LINE_TYPE_RC = "RC";
63 
64     private final String portfilename;
65     private final int poolsize;
66     private final int keepalive;
67 
68     // The secret cookie shared between server and client through the port file.
69     // Used to prevent clients from believing that they are communicating with
70     // an old server when a new server has started and reused the same port as
71     // an old server.
72     private final long myCookie;
73 
74     // Accumulated build time, not counting idle time, used for logging purposes
75     private long totalBuildTime;
76 
77     // The sjavac implementation to delegate requests to
78     Sjavac sjavac;
79 
80     private ServerSocket serverSocket;
81 
82     private PortFile portFile;
83     private PortFileMonitor portFileMonitor;
84 
85     // Set to false break accept loop
86     final AtomicBoolean keepAcceptingRequests = new AtomicBoolean();
87 
88     // For the client, all port files fetched, one per started javac server.
89     // Though usually only one javac server is started by a client.
90     private static Map<String, PortFile> allPortFiles;
91 
SjavacServer(String settings)92     public SjavacServer(String settings) throws FileNotFoundException {
93         this(Util.extractStringOption("portfile", settings),
94              Util.extractIntOption("poolsize", settings, Runtime.getRuntime().availableProcessors()),
95              Util.extractIntOption("keepalive", settings, 120));
96     }
97 
SjavacServer(String portfilename, int poolsize, int keepalive)98     public SjavacServer(String portfilename,
99                         int poolsize,
100                         int keepalive)
101                                 throws FileNotFoundException {
102         this.portfilename = portfilename;
103         this.poolsize = poolsize;
104         this.keepalive = keepalive;
105         this.myCookie = new Random().nextLong();
106     }
107 
108 
109     /**
110      * Acquire the port file. Synchronized since several threads inside an smart javac wrapper client acquires the same port file at the same time.
111      */
getPortFile(String filename)112     public static synchronized PortFile getPortFile(String filename) {
113         if (allPortFiles == null) {
114             allPortFiles = new HashMap<>();
115         }
116         PortFile pf = allPortFiles.get(filename);
117 
118         // Port file known. Does it still exist?
119         if (pf != null) {
120             try {
121                 if (!pf.exists())
122                     pf = null;
123             } catch (IOException ioex) {
124                 ioex.printStackTrace();
125             }
126         }
127 
128         if (pf == null) {
129             pf = new PortFile(filename);
130             allPortFiles.put(filename, pf);
131         }
132         return pf;
133     }
134 
135     /**
136      * Get the cookie used for this server.
137      */
getCookie()138     long getCookie() {
139         return myCookie;
140     }
141 
142     /**
143      * Get the port used for this server.
144      */
getPort()145     int getPort() {
146         return serverSocket.getLocalPort();
147     }
148 
149     /**
150      * Sum up the total build time for this javac server.
151      */
addBuildTime(long inc)152     public void addBuildTime(long inc) {
153         totalBuildTime += inc;
154     }
155 
156     /**
157      * Start a server using a settings string. Typically: "--startserver:portfile=/tmp/myserver,poolsize=3" and the string "portfile=/tmp/myserver,poolsize=3"
158      * is sent as the settings parameter. Returns 0 on success, -1 on failure.
159      */
startServer()160     public int startServer() throws IOException, InterruptedException {
161         long serverStart = System.currentTimeMillis();
162 
163         // The port file is locked and the server port and cookie is written into it.
164         portFile = getPortFile(portfilename);
165 
166         synchronized (portFile) {
167             portFile.lock();
168             portFile.getValues();
169             if (portFile.containsPortInfo()) {
170                 Log.debug("Javac server not started because portfile exists!");
171                 portFile.unlock();
172                 return Result.ERROR.exitCode;
173             }
174 
175             //           .-----------.   .--------.   .------.
176             // socket -->| IdleReset |-->| Pooled |-->| Impl |--> javac
177             //           '-----------'   '--------'   '------'
178             sjavac = new SjavacImpl();
179             sjavac = new PooledSjavac(sjavac, poolsize);
180             sjavac = new IdleResetSjavac(sjavac,
181                                          this,
182                                          keepalive * 1000);
183 
184             serverSocket = new ServerSocket();
185             InetAddress localhost = InetAddress.getByName(null);
186             serverSocket.bind(new InetSocketAddress(localhost, 0));
187 
188             // At this point the server accepts connections, so it is  now safe
189             // to publish the port / cookie information
190             portFile.setValues(getPort(), getCookie());
191             portFile.unlock();
192         }
193 
194         portFileMonitor = new PortFileMonitor(portFile, this);
195         portFileMonitor.start();
196 
197         Log.debug("Sjavac server started. Accepting connections...");
198         Log.debug("    port: " + getPort());
199         Log.debug("    time: " + new java.util.Date());
200         Log.debug("    poolsize: " + poolsize);
201 
202 
203         keepAcceptingRequests.set(true);
204         do {
205             try {
206                 Socket socket = serverSocket.accept();
207                 new RequestHandler(socket, sjavac).start();
208             } catch (SocketException se) {
209                 // Caused by serverSocket.close() and indicates shutdown
210             }
211         } while (keepAcceptingRequests.get());
212 
213         Log.debug("Shutting down.");
214 
215         // No more connections accepted. If any client managed to connect after
216         // the accept() was interrupted but before the server socket is closed
217         // here, any attempt to read or write to the socket will result in an
218         // IOException on the client side.
219 
220         long realTime = System.currentTimeMillis() - serverStart;
221         Log.debug("Total wall clock time " + realTime + "ms build time " + totalBuildTime + "ms");
222 
223         // Shut down
224         sjavac.shutdown();
225 
226         return Result.OK.exitCode;
227     }
228 
229     @Override
shutdown(String quitMsg)230     public void shutdown(String quitMsg) {
231         if (!keepAcceptingRequests.compareAndSet(true, false)) {
232             // Already stopped, no need to shut down again
233             return;
234         }
235 
236         Log.debug("Quitting: " + quitMsg);
237 
238         portFileMonitor.shutdown(); // No longer any need to monitor port file
239 
240         // Unpublish port before shutting down socket to minimize the number of
241         // failed connection attempts
242         try {
243             portFile.delete();
244         } catch (IOException | InterruptedException e) {
245             Log.error(e);
246         }
247         try {
248             serverSocket.close();
249         } catch (IOException e) {
250             Log.error(e);
251         }
252     }
253 }
254