SjavacServer.java revision 2598:bad77727fa11
1/*
2 * Copyright (c) 2011, 2014, 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 */
25package com.sun.tools.sjavac.server;
26
27import java.io.File;
28import java.io.FileNotFoundException;
29import java.io.IOException;
30import java.io.PrintStream;
31import java.io.PrintWriter;
32import java.net.InetAddress;
33import java.net.InetSocketAddress;
34import java.net.ServerSocket;
35import java.net.Socket;
36import java.net.SocketException;
37import java.util.ArrayList;
38import java.util.HashMap;
39import java.util.Map;
40import java.util.Random;
41import java.util.concurrent.atomic.AtomicBoolean;
42
43import com.sun.tools.sjavac.ProblemException;
44import com.sun.tools.sjavac.Util;
45import com.sun.tools.sjavac.comp.SjavacImpl;
46import com.sun.tools.sjavac.comp.PooledSjavac;
47
48/**
49 * The JavacServer class contains methods both to setup a server that responds to requests and methods to connect to this server.
50 *
51 *  <p><b>This is NOT part of any supported API.
52 *  If you write code that depends on this, you do so at your own risk.
53 *  This code and its internal interfaces are subject to change or
54 *  deletion without notice.</b>
55 */
56public class SjavacServer implements Terminable {
57
58    // Used in protocol to indicate which method to invoke
59    public final static String CMD_COMPILE = "compile";
60    public final static String CMD_SYS_INFO = "sys-info";
61
62    final private String portfilename;
63    final private String logfile;
64    final private String stdouterrfile;
65    final private int poolsize;
66    final private int keepalive;
67    final private PrintStream err;
68
69    // The secret cookie shared between server and client through the port file.
70    // Used to prevent clients from believing that they are communicating with
71    // an old server when a new server has started and reused the same port as
72    // an old server.
73    private final long myCookie;
74
75    // Accumulated build time, not counting idle time, used for logging purposes
76    private long totalBuildTime;
77
78    // The javac server specific log file.
79    PrintWriter theLog;
80
81    // The sjavac implementation to delegate requests to
82    Sjavac sjavac;
83
84    private ServerSocket serverSocket;
85
86    private PortFile portFile;
87    private PortFileMonitor portFileMonitor;
88
89    // Set to false break accept loop
90    final AtomicBoolean keepAcceptingRequests = new AtomicBoolean();
91
92    // For the client, all port files fetched, one per started javac server.
93    // Though usually only one javac server is started by a client.
94    private static Map<String, PortFile> allPortFiles;
95    private static Map<String, Long> maxServerMemory;
96
97    public SjavacServer(String settings, PrintStream err) throws FileNotFoundException {
98        // Extract options. TODO: Change to proper constructor args
99        portfilename = Util.extractStringOption("portfile", settings);
100        logfile = Util.extractStringOption("logfile", settings);
101        stdouterrfile = Util.extractStringOption("stdouterrfile", settings);
102        keepalive = Util.extractIntOption("keepalive", settings, 120);
103        poolsize = Util.extractIntOption("poolsize", settings,
104                                         Runtime.getRuntime().availableProcessors());
105        this.err = err;
106
107        myCookie = new Random().nextLong();
108        theLog = new PrintWriter(logfile);
109    }
110
111
112    /**
113     * Acquire the port file. Synchronized since several threads inside an smart javac wrapper client acquires the same port file at the same time.
114     */
115    public static synchronized PortFile getPortFile(String filename) throws FileNotFoundException {
116        if (allPortFiles == null) {
117            allPortFiles = new HashMap<>();
118        }
119        PortFile pf = allPortFiles.get(filename);
120
121        // Port file known. Does it still exist?
122        if (pf != null) {
123            try {
124                if (!pf.exists())
125                    pf = null;
126            } catch (IOException ioex) {
127                ioex.printStackTrace();
128            }
129        }
130
131        if (pf == null) {
132            pf = new PortFile(filename);
133            allPortFiles.put(filename, pf);
134        }
135        return pf;
136    }
137
138    /**
139     * Get the cookie used for this server.
140     */
141    long getCookie() {
142        return myCookie;
143    }
144
145    /**
146     * Get the port used for this server.
147     */
148    int getPort() {
149        return serverSocket.getLocalPort();
150    }
151
152    /**
153     * Sum up the total build time for this javac server.
154     */
155    public void addBuildTime(long inc) {
156        totalBuildTime += inc;
157    }
158
159    /**
160     * Log this message.
161     */
162    public void log(String msg) {
163        if (theLog != null) {
164            theLog.println(msg);
165        } else {
166            System.err.println(msg);
167        }
168    }
169
170    /**
171     * Make sure the log is flushed.
172     */
173    public void flushLog() {
174        if (theLog != null) {
175            theLog.flush();
176        }
177    }
178
179    /**
180     * Start a server using a settings string. Typically: "--startserver:portfile=/tmp/myserver,poolsize=3" and the string "portfile=/tmp/myserver,poolsize=3"
181     * is sent as the settings parameter. Returns 0 on success, -1 on failure.
182     */
183    public int startServer() throws IOException {
184        long serverStart = System.currentTimeMillis();
185
186        // The port file is locked and the server port and cookie is written into it.
187        portFile = getPortFile(portfilename);
188
189        synchronized (portFile) {
190            portFile.lock();
191            portFile.getValues();
192            if (portFile.containsPortInfo()) {
193                err.println("Javac server not started because portfile exists!");
194                portFile.unlock();
195                return -1;
196            }
197
198            //           .-----------.   .--------.   .------.
199            // socket -->| IdleReset |-->| Pooled |-->| Impl |--> javac
200            //           '-----------'   '--------'   '------'
201            sjavac = new SjavacImpl();
202            sjavac = new PooledSjavac(sjavac, poolsize);
203            sjavac = new IdleResetSjavac(sjavac,
204                                         this,
205                                         keepalive * 1000);
206
207            serverSocket = new ServerSocket();
208            InetAddress localhost = InetAddress.getByName(null);
209            serverSocket.bind(new InetSocketAddress(localhost, 0));
210
211            // At this point the server accepts connections, so it is  now safe
212            // to publish the port / cookie information
213            portFile.setValues(getPort(), getCookie());
214            portFile.unlock();
215        }
216
217        portFileMonitor = new PortFileMonitor(portFile, this);
218        portFileMonitor.start();
219
220        log("Sjavac server started. Accepting connections...");
221        log("    port: " + getPort());
222        log("    time: " + new java.util.Date());
223        log("    poolsize: " + poolsize);
224        flushLog();
225
226        keepAcceptingRequests.set(true);
227        do {
228            try {
229                Socket socket = serverSocket.accept();
230                new Thread(new RequestHandler(socket, sjavac)).start();
231            } catch (SocketException se) {
232                // Caused by serverSocket.close() and indicates shutdown
233            }
234        } while (keepAcceptingRequests.get());
235
236        log("Shutting down.");
237
238        // No more connections accepted. If any client managed to connect after
239        // the accept() was interrupted but before the server socket is closed
240        // here, any attempt to read or write to the socket will result in an
241        // IOException on the client side.
242
243        long realTime = System.currentTimeMillis() - serverStart;
244        log("Total wall clock time " + realTime + "ms build time " + totalBuildTime + "ms");
245        flushLog();
246
247        // Shut down
248        sjavac.shutdown();
249
250        return 0;
251    }
252
253    /**
254     * Fork a background process. Returns the command line used that can be printed if something failed.
255     */
256    public static String fork(String sjavac, String portfile, String logfile, int poolsize, int keepalive,
257            final PrintStream err, String stdouterrfile, boolean background)
258            throws IOException, ProblemException {
259        if (stdouterrfile != null && stdouterrfile.trim().equals("")) {
260            stdouterrfile = null;
261        }
262        final String startserver = "--startserver:portfile=" + portfile + ",logfile=" + logfile + ",stdouterrfile=" + stdouterrfile + ",poolsize=" + poolsize + ",keepalive="+ keepalive;
263
264        if (background) {
265            sjavac += "%20" + startserver;
266            sjavac = sjavac.replaceAll("%20", " ");
267            sjavac = sjavac.replaceAll("%2C", ",");
268            // If the java/sh/cmd launcher fails the failure will be captured by stdouterr because of the redirection here.
269            String[] cmd = {"/bin/sh", "-c", sjavac + " >> " + stdouterrfile + " 2>&1"};
270            if (!(new File("/bin/sh")).canExecute()) {
271                ArrayList<String> wincmd = new ArrayList<>();
272                wincmd.add("cmd");
273                wincmd.add("/c");
274                wincmd.add("start");
275                wincmd.add("cmd");
276                wincmd.add("/c");
277                wincmd.add(sjavac + " >> " + stdouterrfile + " 2>&1");
278                cmd = wincmd.toArray(new String[wincmd.size()]);
279            }
280            Process pp = null;
281            try {
282                pp = Runtime.getRuntime().exec(cmd);
283            } catch (Exception e) {
284                e.printStackTrace(err);
285                e.printStackTrace(new PrintWriter(stdouterrfile));
286            }
287            StringBuilder rs = new StringBuilder();
288            for (String s : cmd) {
289                rs.append(s + " ");
290            }
291            return rs.toString();
292        }
293
294        // Do not spawn a background server, instead run it within the same JVM.
295        Thread t = new Thread() {
296            @Override
297            public void run() {
298                try {
299                    SjavacServer server = new SjavacServer(startserver, err);
300                    server.startServer();
301                } catch (Throwable t) {
302                    t.printStackTrace(err);
303                }
304            }
305        };
306        t.setDaemon(true);
307        t.start();
308        return "";
309    }
310
311    @Override
312    public void shutdown(String quitMsg) {
313        if (!keepAcceptingRequests.compareAndSet(true, false)) {
314            // Already stopped, no need to shut down again
315            return;
316        }
317
318        log("Quitting: " + quitMsg);
319        flushLog();
320
321        portFileMonitor.shutdown(); // No longer any need to monitor port file
322
323        // Unpublish port before shutting down socket to minimize the number of
324        // failed connection attempts
325        try {
326            portFile.delete();
327        } catch (IOException e) {
328            e.printStackTrace(theLog);
329        }
330        try {
331            serverSocket.close();
332        } catch (IOException e) {
333            e.printStackTrace(theLog);
334        }
335    }
336
337    public static void cleanup(String... args) {
338        String settings = Util.findServerSettings(args);
339        if (settings == null) return;
340        String portfile = Util.extractStringOption("portfile", settings);
341        String background = Util.extractStringOption("background", settings);
342        if (background != null && background.equals("false")) {
343            // If the server runs within this jvm, then delete the portfile,
344            // since this jvm is about to exit soon.
345            File f = new File(portfile);
346            f.delete();
347        }
348    }
349}
350