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
26package com.sun.tools.sjavac.server;
27
28import java.io.FileNotFoundException;
29import java.io.FileWriter;
30import java.io.IOException;
31import java.io.PrintStream;
32import java.io.PrintWriter;
33import java.net.InetAddress;
34import java.net.InetSocketAddress;
35import java.net.ServerSocket;
36import java.net.Socket;
37import java.net.SocketException;
38import java.util.HashMap;
39import java.util.Map;
40import java.util.Random;
41import java.util.concurrent.atomic.AtomicBoolean;
42
43import com.sun.tools.javac.main.Main;
44import com.sun.tools.javac.main.Main.Result;
45import com.sun.tools.sjavac.Log;
46import com.sun.tools.sjavac.Util;
47import com.sun.tools.sjavac.client.PortFileInaccessibleException;
48import com.sun.tools.sjavac.comp.PooledSjavac;
49import 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 */
59public class SjavacServer implements Terminable {
60
61    // Prefix of line containing return code.
62    public final static String LINE_TYPE_RC = "RC";
63
64    final private String portfilename;
65    final private int poolsize;
66    final private 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
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
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     */
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     */
138    long getCookie() {
139        return myCookie;
140    }
141
142    /**
143     * Get the port used for this server.
144     */
145    int getPort() {
146        return serverSocket.getLocalPort();
147    }
148
149    /**
150     * Sum up the total build time for this javac server.
151     */
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     */
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
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