bed7f6bf41a3bda6662c914af034c6199779db0b
[idea/community.git] / jps / jps-builders / src / org / jetbrains / jps / javac / ExternalJavacManager.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.jetbrains.jps.javac;
17
18 import com.intellij.execution.process.BaseOSProcessHandler;
19 import com.intellij.execution.process.ProcessAdapter;
20 import com.intellij.execution.process.ProcessEvent;
21 import com.intellij.execution.process.ProcessOutputTypes;
22 import com.intellij.openapi.diagnostic.Logger;
23 import com.intellij.openapi.util.Key;
24 import com.intellij.openapi.util.SystemInfo;
25 import com.intellij.openapi.util.io.FileUtil;
26 import com.intellij.openapi.util.text.StringUtil;
27 import com.intellij.util.concurrency.Semaphore;
28 import io.netty.bootstrap.ServerBootstrap;
29 import io.netty.channel.*;
30 import io.netty.channel.group.ChannelGroup;
31 import io.netty.channel.group.ChannelGroupFuture;
32 import io.netty.channel.group.DefaultChannelGroup;
33 import io.netty.channel.nio.NioEventLoopGroup;
34 import io.netty.channel.socket.nio.NioServerSocketChannel;
35 import io.netty.handler.codec.protobuf.ProtobufDecoder;
36 import io.netty.handler.codec.protobuf.ProtobufEncoder;
37 import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
38 import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;
39 import io.netty.util.AttributeKey;
40 import io.netty.util.concurrent.ImmediateEventExecutor;
41 import org.jetbrains.annotations.NotNull;
42 import org.jetbrains.annotations.Nullable;
43 import org.jetbrains.jps.api.CanceledStatus;
44 import org.jetbrains.jps.builders.java.JavaCompilingTool;
45 import org.jetbrains.jps.cmdline.ClasspathBootstrap;
46 import org.jetbrains.jps.incremental.GlobalContextKey;
47 import org.jetbrains.jps.service.SharedThreadPool;
48
49 import javax.tools.*;
50 import java.io.File;
51 import java.net.InetAddress;
52 import java.net.UnknownHostException;
53 import java.util.*;
54 import java.util.concurrent.TimeUnit;
55
56 /**
57  * @author Eugene Zhuravlev
58  *         Date: 1/22/12                       
59  */
60 @SuppressWarnings("UseOfSystemOutOrSystemErr")
61 public class ExternalJavacManager {
62   private static final Logger LOG = Logger.getInstance("#org.jetbrains.jps.javac.ExternalJavacServer");
63   public static final GlobalContextKey<ExternalJavacManager> KEY = GlobalContextKey.create("_external_javac_server_");
64   
65   public static final int DEFAULT_SERVER_PORT = 7878;
66   public static final String STDOUT_LINE_PREFIX = "JAVAC_PROCESS[STDOUT]";
67   public static final String STDERR_LINE_PREFIX = "JAVAC_PROCESS[STDERR]";
68   private static final AttributeKey<JavacProcessDescriptor> SESSION_DESCRIPTOR = AttributeKey.valueOf("ExternalJavacServer.JavacProcessDescriptor");
69   @NotNull
70   private final File myWorkingDir;
71   @NotNull
72   private final ChannelRegistrar myChannelRegistrar;
73   private final Map<UUID, JavacProcessDescriptor> myMessageHandlers = new HashMap<UUID, JavacProcessDescriptor>();
74   private int myListenPort = DEFAULT_SERVER_PORT;
75
76   public ExternalJavacManager(@NotNull final File workingDir) {
77     myWorkingDir = workingDir;
78     myChannelRegistrar = new ChannelRegistrar();
79   }
80
81   @NotNull
82   public File getWorkingDir() {
83     return myWorkingDir;
84   }
85
86   public void start(int listenPort) {
87     final ServerBootstrap bootstrap = new ServerBootstrap().group(new NioEventLoopGroup(1, SharedThreadPool.getInstance())).channel(NioServerSocketChannel.class);
88     bootstrap.childOption(ChannelOption.TCP_NODELAY, true).childOption(ChannelOption.SO_KEEPALIVE, true);
89     final ChannelHandler compilationRequestsHandler = new CompilationRequestsHandler();
90     bootstrap.childHandler(new ChannelInitializer() {
91       @Override
92       protected void initChannel(Channel channel) throws Exception {
93         channel.pipeline().addLast(myChannelRegistrar,
94                                    new ProtobufVarint32FrameDecoder(),
95                                    new ProtobufDecoder(JavacRemoteProto.Message.getDefaultInstance()),
96                                    new ProtobufVarint32LengthFieldPrepender(),
97                                    new ProtobufEncoder(),
98                                    compilationRequestsHandler);
99       }
100     });
101     try {
102       final InetAddress loopback = InetAddress.getByName(null);
103       myChannelRegistrar.add(bootstrap.bind(loopback, listenPort).syncUninterruptibly().channel());
104       myListenPort = listenPort;
105     }
106     catch (UnknownHostException e) {
107       throw new RuntimeException(e);
108     }
109   }
110   
111
112   public boolean forkJavac(final String javaHome, final int heapSize, List<String> vmOptions, List<String> options,
113                            Collection<File> platformCp,
114                            Collection<File> classpath,
115                            Collection<File> sourcePath,
116                            Collection<File> files,
117                            Map<File, Set<File>> outs,
118                            final DiagnosticOutputConsumer diagnosticSink, OutputFileConsumer outputSink,
119                            final JavaCompilingTool compilingTool,
120                            final CanceledStatus cancelStatus) {
121     final ExternalJavacMessageHandler rh = new ExternalJavacMessageHandler(diagnosticSink, outputSink, getEncodingName(options));
122     final JavacRemoteProto.Message.Request request = JavacProtoUtil.createCompilationRequest(options, files, classpath, platformCp, sourcePath, outs);
123     final UUID uuid = UUID.randomUUID();
124     final JavacProcessDescriptor processDescriptor = new JavacProcessDescriptor(uuid, rh, request);
125     synchronized (myMessageHandlers) {
126       myMessageHandlers.put(uuid, processDescriptor);
127     }
128     try {
129       final ExternalJavacProcessHandler processHandler = launchExternalJavacProcess(
130         uuid, javaHome, heapSize, myListenPort, myWorkingDir, vmOptions, compilingTool
131       );
132       processHandler.addProcessListener(new ProcessAdapter() {
133         public void onTextAvailable(ProcessEvent event, Key outputType) {
134           final String text = event.getText();
135           if (!StringUtil.isEmptyOrSpaces(text)) {
136             String prefix = null;
137             if (outputType == ProcessOutputTypes.STDOUT) {
138               prefix = STDOUT_LINE_PREFIX;
139             }
140             else if (outputType == ProcessOutputTypes.STDERR) {
141               prefix = STDERR_LINE_PREFIX;
142             }
143             if (prefix != null) {
144               diagnosticSink.outputLineAvailable(prefix + ": " + text);
145             }
146           }
147         }
148       });
149       processHandler.startNotify();
150
151       while (!processDescriptor.waitFor(300L)) {
152         if (processHandler.isProcessTerminated() && processDescriptor.channel == null && processHandler.getExitCode() != 0) {
153           // process terminated abnormally and no communication took place
154           processDescriptor.setDone();
155           break;
156         }
157         if (cancelStatus.isCanceled()) {
158           processDescriptor.cancelBuild();
159         }
160       }
161
162       return rh.isTerminatedSuccessfully();
163     }
164     catch (Throwable e) {
165       LOG.info(e);
166       diagnosticSink.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR, e.getMessage()));
167     }
168     finally {
169       unregisterMessageHandler(uuid);
170     }
171     return false;
172   }
173
174   private void unregisterMessageHandler(UUID uuid) {
175     final JavacProcessDescriptor descriptor;
176     synchronized (myMessageHandlers) {
177       descriptor = myMessageHandlers.remove(uuid);
178     }
179     if (descriptor != null) {
180       descriptor.setDone();
181     }
182   }
183
184   @Nullable
185   private static String getEncodingName(List<String> options) {
186     boolean found = false;
187     for (String option : options) {
188       if (found) {
189         return option;
190       }
191       if ("-encoding".equalsIgnoreCase(option)) {
192         found = true;
193       }
194     }
195     return null;
196   }
197   
198   public void stop() {
199     myChannelRegistrar.close().awaitUninterruptibly();
200   }
201
202   private ExternalJavacProcessHandler launchExternalJavacProcess(UUID uuid, String sdkHomePath,
203                                                                         int heapSize,
204                                                                         int port,
205                                                                         File workingDir,
206                                                                         List<String> vmOptions,
207                                                                         JavaCompilingTool compilingTool) throws Exception {
208     final List<String> cmdLine = new ArrayList<String>();
209     appendParam(cmdLine, getVMExecutablePath(sdkHomePath));
210     //appendParam(cmdLine, "-XX:MaxPermSize=150m");
211     //appendParam(cmdLine, "-XX:ReservedCodeCacheSize=64m");
212     appendParam(cmdLine, "-Djava.awt.headless=true");
213     if (heapSize > 0) {
214       // if the value is zero or negative, use JVM default memory settings
215       final int xms = heapSize / 2;
216       if (xms > 32) {
217         appendParam(cmdLine, "-Xms" + xms + "m");
218       }
219       appendParam(cmdLine, "-Xmx" + heapSize + "m");
220     }
221
222     // debugging
223     //appendParam(cmdLine, "-XX:+HeapDumpOnOutOfMemoryError");
224     //appendParam(cmdLine, "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5009");
225
226     // javac's VM should use the same default locale that IDEA uses in order for javac to print messages in 'correct' language
227     final String encoding = System.getProperty("file.encoding");
228     if (encoding != null) {
229       appendParam(cmdLine, "-Dfile.encoding=" + encoding);
230     }
231     final String lang = System.getProperty("user.language");
232     if (lang != null) {
233       //noinspection HardCodedStringLiteral
234       appendParam(cmdLine, "-Duser.language=" + lang);
235     }
236     final String country = System.getProperty("user.country");
237     if (country != null) {
238       //noinspection HardCodedStringLiteral
239       appendParam(cmdLine, "-Duser.country=" + country);
240     }
241     //noinspection HardCodedStringLiteral
242     final String region = System.getProperty("user.region");
243     if (region != null) {
244       //noinspection HardCodedStringLiteral
245       appendParam(cmdLine, "-Duser.region=" + region);
246     }
247
248     appendParam(cmdLine, "-D" + ExternalJavacProcess.JPS_JAVA_COMPILING_TOOL_PROPERTY + "=" + compilingTool.getId());
249
250     // this will disable standard extensions to ensure javac is loaded from the right tools.jar
251     appendParam(cmdLine, "-Djava.ext.dirs=");
252
253     appendParam(cmdLine, "-Dlog4j.defaultInitOverride=true");
254
255     for (String option : vmOptions) {
256       appendParam(cmdLine, option);
257     }
258
259     appendParam(cmdLine, "-classpath");
260
261     final List<File> cp = ClasspathBootstrap.getExternalJavacProcessClasspath(sdkHomePath, compilingTool);
262     final StringBuilder classpath = new StringBuilder();
263     for (File file : cp) {
264       if (classpath.length() > 0) {
265         classpath.append(File.pathSeparator);
266       }
267       classpath.append(file.getPath());
268     }
269     appendParam(cmdLine, classpath.toString());
270
271     appendParam(cmdLine, ExternalJavacProcess.class.getName());
272     appendParam(cmdLine, uuid.toString());
273     appendParam(cmdLine, "127.0.0.1");
274     appendParam(cmdLine, Integer.toString(port));
275
276     workingDir.mkdirs();
277
278     appendParam(cmdLine, FileUtil.toSystemIndependentName(workingDir.getPath()));
279
280     final ProcessBuilder builder = new ProcessBuilder(cmdLine);
281     builder.directory(workingDir);
282
283     final Process process = builder.start();
284     return createProcessHandler(process, StringUtil.join(cmdLine, " "));
285   }
286
287   protected ExternalJavacProcessHandler createProcessHandler(@NotNull Process process, @NotNull String commandLine) {
288     return new ExternalJavacProcessHandler(process, commandLine);
289   }
290
291   private static void appendParam(List<String> cmdLine, String param) {
292     if (SystemInfo.isWindows) {
293       if (param.contains("\"")) {
294         param = StringUtil.replace(param, "\"", "\\\"");
295       }
296       else if (param.length() == 0) {
297         param = "\"\"";
298       }
299     }
300     cmdLine.add(param);
301   }
302
303   private static String getVMExecutablePath(String sdkHome) {
304     return sdkHome + "/bin/java";
305   }
306
307   protected static class ExternalJavacProcessHandler extends BaseOSProcessHandler {
308     private volatile int myExitCode;
309
310     protected ExternalJavacProcessHandler(@NotNull Process process, @NotNull String commandLine) {
311       super(process, commandLine, null);
312       addProcessListener(new ProcessAdapter() {
313         @Override
314         public void processTerminated(ProcessEvent event) {
315           myExitCode = event.getExitCode();
316         }
317       });
318     }
319
320     public int getExitCode() {
321       return myExitCode;
322     }
323   }
324
325   @ChannelHandler.Sharable
326   private class CompilationRequestsHandler extends SimpleChannelInboundHandler<JavacRemoteProto.Message> {
327     @Override
328     public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
329       JavacProcessDescriptor descriptor = ctx.attr(SESSION_DESCRIPTOR).get();
330       if (descriptor != null) {
331         descriptor.setDone();
332       }
333       super.channelUnregistered(ctx);
334     }
335
336     @Override
337     public void channelRead0(final ChannelHandlerContext context, JavacRemoteProto.Message message) throws Exception {
338       JavacProcessDescriptor descriptor = context.attr(SESSION_DESCRIPTOR).get();
339   
340       UUID sessionId;
341       if (descriptor == null) {
342         // this is the first message for this session, so fill session data with missing info
343         sessionId = JavacProtoUtil.fromProtoUUID(message.getSessionId());
344   
345         descriptor = myMessageHandlers.get(sessionId);
346         if (descriptor != null) {
347           descriptor.channel = context.channel();
348           context.attr(SESSION_DESCRIPTOR).set(descriptor);
349         }
350       }
351       else {
352         sessionId = descriptor.sessionId;
353       }
354   
355       final ExternalJavacMessageHandler handler = descriptor != null? descriptor.handler : null;
356
357       final JavacRemoteProto.Message.Type messageType = message.getMessageType();
358
359       JavacRemoteProto.Message reply = null;
360       try {
361         if (messageType == JavacRemoteProto.Message.Type.RESPONSE) {
362           final JavacRemoteProto.Message.Response response = message.getResponse();
363           final JavacRemoteProto.Message.Response.Type responseType = response.getResponseType();
364           if (handler != null) {
365             if (responseType == JavacRemoteProto.Message.Response.Type.REQUEST_ACK) {
366               final JavacRemoteProto.Message.Request request = descriptor.request;
367               if (request != null) {
368                 reply = JavacProtoUtil.toMessage(sessionId, request);
369                 descriptor.request = null;
370               }
371             }
372             else {
373               final boolean terminateOk = handler.handleMessage(message);
374               if (terminateOk) {
375                 descriptor.setDone();
376               }
377             }
378           }
379           else {
380             reply = JavacProtoUtil.toMessage(sessionId, JavacProtoUtil.createCancelRequest());
381           }
382         }
383         else {
384           reply = JavacProtoUtil.toMessage(sessionId, JavacProtoUtil.createFailure("Unsupported message: " + messageType.name(), null));
385         }
386       }
387       finally {
388         if (reply != null) {
389           context.channel().writeAndFlush(reply);  
390         }
391       }
392     }
393   }
394
395   @ChannelHandler.Sharable
396   private static final class ChannelRegistrar extends ChannelInboundHandlerAdapter {
397     private final ChannelGroup openChannels = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
398
399     public boolean isEmpty() {
400       return openChannels.isEmpty();
401     }
402
403     public void add(@NotNull Channel serverChannel) {
404       assert serverChannel instanceof ServerChannel;
405       openChannels.add(serverChannel);
406     }
407
408     @Override
409     public void channelActive(ChannelHandlerContext context) throws Exception {
410       // we don't need to remove channel on close - ChannelGroup do it
411       openChannels.add(context.channel());
412       super.channelActive(context);
413     }
414
415     public ChannelGroupFuture close() {
416       EventLoopGroup eventLoopGroup = null;
417       for (Channel channel : openChannels) {
418         if (channel instanceof ServerChannel) {
419           eventLoopGroup = channel.eventLoop().parent();
420           break;
421         }
422       }
423
424       ChannelGroupFuture future;
425       try {
426         future = openChannels.close();
427       }
428       finally {
429         assert eventLoopGroup != null;
430         eventLoopGroup.shutdownGracefully(0, 15, TimeUnit.SECONDS);
431       }
432       return future;
433     }
434   }
435
436   private static class JavacProcessDescriptor {
437     @NotNull
438     final UUID sessionId;
439     @NotNull
440     final ExternalJavacMessageHandler handler;
441     volatile JavacRemoteProto.Message.Request request;
442     volatile Channel channel;
443     private final Semaphore myDone = new Semaphore();
444
445     public JavacProcessDescriptor(@NotNull UUID sessionId, @NotNull ExternalJavacMessageHandler handler, @NotNull JavacRemoteProto.Message.Request request) {
446       this.sessionId = sessionId;
447       this.handler = handler;
448       this.request = request;
449       myDone.down();
450     }
451
452     public void cancelBuild() {
453       if (channel != null) {
454         channel.writeAndFlush(JavacProtoUtil.toMessage(sessionId, JavacProtoUtil.createCancelRequest()));
455       }
456     }
457     
458     public void setDone() {
459       myDone.up();
460     }
461     
462     
463     public boolean waitFor(long timeout) {
464       return myDone.waitFor(timeout);
465     }
466     
467   }
468   
469 }