d34ce9d92a7bee2e2545bd6aef9a0c3b17cfe82c
[idea/community.git] / java / compiler / impl / src / com / intellij / compiler / CompileServerManager.java
1 /*
2  * Copyright 2000-2011 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 com.intellij.compiler;
17
18 import com.intellij.ProjectTopics;
19 import com.intellij.application.options.PathMacrosImpl;
20 import com.intellij.execution.ExecutionException;
21 import com.intellij.execution.configurations.GeneralCommandLine;
22 import com.intellij.execution.process.OSProcessHandler;
23 import com.intellij.execution.process.ProcessAdapter;
24 import com.intellij.execution.process.ProcessEvent;
25 import com.intellij.execution.process.ProcessOutputTypes;
26 import com.intellij.openapi.application.ApplicationManager;
27 import com.intellij.openapi.application.PathMacros;
28 import com.intellij.openapi.application.PathManager;
29 import com.intellij.openapi.components.ApplicationComponent;
30 import com.intellij.openapi.diagnostic.Logger;
31 import com.intellij.openapi.project.Project;
32 import com.intellij.openapi.project.ProjectManager;
33 import com.intellij.openapi.project.ProjectManagerAdapter;
34 import com.intellij.openapi.projectRoots.JavaSdkType;
35 import com.intellij.openapi.projectRoots.ProjectJdkTable;
36 import com.intellij.openapi.projectRoots.Sdk;
37 import com.intellij.openapi.projectRoots.impl.JavaAwareProjectJdkTableImpl;
38 import com.intellij.openapi.roots.ModuleRootEvent;
39 import com.intellij.openapi.roots.ModuleRootListener;
40 import com.intellij.openapi.roots.OrderRootType;
41 import com.intellij.openapi.roots.libraries.Library;
42 import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar;
43 import com.intellij.openapi.util.Key;
44 import com.intellij.openapi.util.Ref;
45 import com.intellij.openapi.util.ShutDownTracker;
46 import com.intellij.openapi.util.io.FileUtil;
47 import com.intellij.openapi.util.registry.Registry;
48 import com.intellij.openapi.util.text.StringUtil;
49 import com.intellij.openapi.vfs.JarFileSystem;
50 import com.intellij.openapi.vfs.VirtualFile;
51 import com.intellij.openapi.vfs.encoding.EncodingManager;
52 import com.intellij.util.concurrency.Semaphore;
53 import com.intellij.util.messages.MessageBusConnection;
54 import com.intellij.util.net.NetUtils;
55 import org.jetbrains.annotations.NotNull;
56 import org.jetbrains.annotations.Nullable;
57 import org.jetbrains.jps.api.*;
58 import org.jetbrains.jps.client.CompileServerClient;
59 import org.jetbrains.jps.server.ClasspathBootstrap;
60 import org.jetbrains.jps.server.Server;
61
62 import javax.tools.*;
63 import java.io.File;
64 import java.io.FileOutputStream;
65 import java.io.IOException;
66 import java.io.InputStream;
67 import java.util.*;
68 import java.util.concurrent.Future;
69 import java.util.concurrent.RunnableFuture;
70 import java.util.concurrent.TimeUnit;
71
72 /**
73  * @author Eugene Zhuravlev
74  *         Date: 9/6/11
75  */
76 public class CompileServerManager implements ApplicationComponent{
77   private static final Logger LOG = Logger.getInstance("#com.intellij.compiler.CompileServerManager");
78   private static final String COMPILE_SERVER_SYSTEM_ROOT = "compile-server";
79   private static final String LOGGER_CONFIG = "log.xml";
80   private static final String DEFAULT_LOGGER_CONFIG = "defaultLogConfig.xml";
81   private volatile OSProcessHandler myProcessHandler;
82   private final File mySystemDirectory;
83   private volatile CompileServerClient myClient = new CompileServerClient();
84   private final SequentialTaskExecutor myTaskExecutor = new SequentialTaskExecutor(new SequentialTaskExecutor.AsyncTaskExecutor() {
85     public void submit(Runnable runnable) {
86       ApplicationManager.getApplication().executeOnPooledThread(runnable);
87     }
88   });
89   private final ProjectManager myProjectManager;
90
91   public CompileServerManager(final ProjectManager projectManager) {
92     myProjectManager = projectManager;
93     final String systemPath = PathManager.getSystemPath();
94     File system = new File(systemPath);
95     try {
96       system = system.getCanonicalFile();
97     }
98     catch (IOException e) {
99       LOG.info(e);
100     }
101     mySystemDirectory = system;
102
103     projectManager.addProjectManagerListener(new ProjectWatcher());
104
105     ShutDownTracker.getInstance().registerShutdownTask(new Runnable() {
106       @Override
107       public void run() {
108         shutdownServer(myClient, myProcessHandler);
109       }
110     });
111   }
112
113   public static CompileServerManager getInstance() {
114     return ApplicationManager.getApplication().getComponent(CompileServerManager.class);
115   }
116
117   public void notifyFilesChanged(Collection<String> paths) {
118     sendNotification(paths, false);
119   }
120
121   public void notifyFilesDeleted(Collection<String> paths) {
122     sendNotification(paths, true);
123   }
124
125   public void sendReloadRequest(final Project project) {
126     if (!project.isDefault() && project.isOpen()) {
127       myTaskExecutor.submit(new Runnable() {
128         @Override
129         public void run() {
130           try {
131             if (!project.isDisposed()) {
132               final CompileServerClient client = ensureServerRunningAndClientConnected(false);
133               if (client != null) {
134                 client.sendProjectReloadRequest(Collections.singletonList(project.getLocation()));
135               }
136             }
137           }
138           catch (Throwable e) {
139             LOG.info(e);
140           }
141         }
142       });
143     }
144   }
145
146   public void sendCancelBuildRequest(final UUID sessionId) {
147     myTaskExecutor.submit(new Runnable() {
148       @Override
149       public void run() {
150         try {
151           final CompileServerClient client = ensureServerRunningAndClientConnected(false);
152           if (client != null) {
153             client.sendCancelBuildRequest(sessionId);
154           }
155         }
156         catch (Throwable e) {
157           LOG.info(e);
158         }
159       }
160     });
161   }
162
163   private void sendNotification(final Collection<String> paths, final boolean isDeleted) {
164     if (paths.isEmpty()) {
165       return;
166     }
167     try {
168       final CompileServerClient client = ensureServerRunningAndClientConnected(false);
169       if (client != null) {
170         myTaskExecutor.submit(new Runnable() {
171           public void run() {
172             final Project[] openProjects = myProjectManager.getOpenProjects();
173             if (openProjects.length > 0) {
174               final Collection<String> changed, deleted;
175               if (isDeleted) {
176                 changed = Collections.emptyList();
177                 deleted = paths;
178               }
179               else {
180                 changed = paths;
181                 deleted = Collections.emptyList();
182               }
183               for (Project project : openProjects) {
184                 try {
185                   client.sendFSEvent(project.getLocation(), changed, deleted);
186                 }
187                 catch (Exception e) {
188                   LOG.info(e);
189                 }
190               }
191             }
192           }
193         });
194       }
195     }
196     catch (Throwable th) {
197       LOG.error(th); // should not happen
198     }
199   }
200
201   @Nullable
202   public RequestFuture submitCompilationTask(final String projectId, final boolean isRebuild, final boolean isMake, final Collection<String> modules, final Collection<String> paths, final JpsServerResponseHandler handler) {
203     final Ref<RequestFuture> futureRef = new Ref<RequestFuture>(null);
204     final RunnableFuture future = myTaskExecutor.submit(new Runnable() {
205       public void run() {
206         try {
207           final CompileServerClient client = ensureServerRunningAndClientConnected(true);
208           if (client != null) {
209             final RequestFuture requestFuture = isRebuild ?
210               client.sendRebuildRequest(projectId, handler) :
211               client.sendCompileRequest(isMake, projectId, modules, paths, handler);
212             futureRef.set(requestFuture);
213           }
214           else {
215             handler.sessionTerminated();
216           }
217         }
218         catch (Throwable e) {
219           try {
220             handler.handleFailure(ProtoUtil.createFailure(e.getMessage(), e));
221           }
222           finally {
223             handler.sessionTerminated();
224           }
225         }
226       }
227     });
228     try {
229       future.get();
230     }
231     catch (Throwable e) {
232       LOG.info(e);
233     }
234     return futureRef.get();
235   }
236
237   @Override
238   public void initComponent() {
239   }
240
241   @Override
242   public void disposeComponent() {
243     shutdownServer(myClient, myProcessHandler);
244   }
245
246   @NotNull
247   @Override
248   public String getComponentName() {
249     return "com.intellij.compiler.JpsServerManager";
250   }
251
252   // executed in one thread at a time
253   @Nullable
254   private CompileServerClient ensureServerRunningAndClientConnected(boolean forceRestart) throws Throwable {
255     final OSProcessHandler ph = myProcessHandler;
256     final CompileServerClient cl = myClient;
257     final boolean processNotRunning = ph == null || ph.isProcessTerminated() || ph.isProcessTerminating();
258     final boolean clientNotConnected = cl == null || !cl.isConnected();
259
260     if (processNotRunning || clientNotConnected) {
261       // cleanup; ensure the process is not running
262       shutdownServer(cl, ph);
263       myProcessHandler = null;
264       myClient = null;
265
266       if (!forceRestart) {
267         return null;
268       }
269
270       final int port = NetUtils.findAvailableSocketPort();
271       final Process process = launchServer(port);
272
273       final OSProcessHandler processHandler = new OSProcessHandler(process, null) {
274         protected boolean shouldDestroyProcessRecursively() {
275           return true;
276         }
277       };
278       final StringBuilder serverStartMessage = new StringBuilder();
279       final Semaphore semaphore  = new Semaphore();
280       semaphore.down();
281       processHandler.addProcessListener(new ProcessAdapter() {
282         public void onTextAvailable(ProcessEvent event, Key outputType) {
283           // re-translate server's output to idea.log
284           final String text = event.getText();
285           if (!StringUtil.isEmpty(text)) {
286             LOG.info("COMPILE_SERVER [" +outputType.toString() +"]: "+ text);
287           }
288         }
289       });
290       processHandler.addProcessListener(new ProcessAdapter() {
291         public void processTerminated(ProcessEvent event) {
292           try {
293             processHandler.removeProcessListener(this);
294           }
295           finally {
296             semaphore.up();
297           }
298         }
299
300         public void onTextAvailable(ProcessEvent event, Key outputType) {
301           if (outputType == ProcessOutputTypes.STDERR) {
302             try {
303               final String text = event.getText();
304               if (text != null) {
305                 if (text.contains(Server.SERVER_SUCCESS_START_MESSAGE) || text.contains(Server.SERVER_ERROR_START_MESSAGE)) {
306                   processHandler.removeProcessListener(this);
307                 }
308                 if (serverStartMessage.length() > 0) {
309                   serverStartMessage.append("\n");
310                 }
311                 serverStartMessage.append(text);
312               }
313             }
314             finally {
315               semaphore.up();
316             }
317           }
318         }
319       });
320       processHandler.startNotify();
321       semaphore.waitFor();
322
323       final String startupMsg = serverStartMessage.toString();
324       if (!startupMsg.contains(Server.SERVER_SUCCESS_START_MESSAGE)) {
325         throw new Exception("Server startup failed: " + startupMsg);
326       }
327
328       CompileServerClient client = new CompileServerClient();
329       boolean connected = false;
330       try {
331         connected = client.connect(NetUtils.getLocalHostString(), port);
332         if (connected) {
333           final RequestFuture setupFuture = sendSetupRequest(client);
334           setupFuture.get();
335           myProcessHandler = processHandler;
336           myClient = client;
337         }
338       }
339       finally {
340         if (!connected) {
341           shutdownServer(cl, processHandler);
342         }
343       }
344     }
345     return myClient;
346   }
347
348   private static RequestFuture sendSetupRequest(final @NotNull CompileServerClient client) throws Exception {
349     final Map<String, String> data = new HashMap<String, String>();
350
351     // need this for tests and when this macro is missing from PathMacros registry
352     data.put(PathMacrosImpl.APPLICATION_HOME_MACRO_NAME, FileUtil.toSystemIndependentName(PathManager.getHomePath()));
353
354     final PathMacros pathVars = PathMacros.getInstance();
355     for (String name : pathVars.getAllMacroNames()) {
356       final String path = pathVars.getValue(name);
357       if (path != null) {
358         data.put(name, FileUtil.toSystemIndependentName(path));
359       }
360     }
361
362     final List<GlobalLibrary> globals = new ArrayList<GlobalLibrary>();
363
364     fillSdks(globals);
365     fillGlobalLibraries(globals);
366
367     return client.sendSetupRequest(data, globals, EncodingManager.getInstance().getDefaultCharsetName());
368   }
369
370   private static void fillSdks(List<GlobalLibrary> globals) {
371     for (Sdk sdk : ProjectJdkTable.getInstance().getAllJdks()) {
372       final String name = sdk.getName();
373       final String homePath = sdk.getHomePath();
374       if (homePath == null) {
375         continue;
376       }
377       final List<String> paths = convertToLocalPaths(sdk.getRootProvider().getFiles(OrderRootType.CLASSES));
378       globals.add(new SdkLibrary(name, homePath, paths));
379     }
380   }
381
382   private static void fillGlobalLibraries(List<GlobalLibrary> globals) {
383     final Iterator<Library> iterator = LibraryTablesRegistrar.getInstance().getLibraryTable().getLibraryIterator();
384     while (iterator.hasNext()) {
385       Library library = iterator.next();
386       final String name = library.getName();
387
388       if (name != null) {
389         final List<String> paths = convertToLocalPaths(library.getFiles(OrderRootType.CLASSES));
390         globals.add(new GlobalLibrary(name, paths));
391       }
392     }
393   }
394
395   private static List<String> convertToLocalPaths(VirtualFile[] files) {
396     final List<String> paths = new ArrayList<String>();
397     for (VirtualFile file : files) {
398       if (file.isValid()) {
399         paths.add(StringUtil.trimEnd(FileUtil.toSystemIndependentName(file.getPath()), JarFileSystem.JAR_SEPARATOR));
400       }
401     }
402     return paths;
403   }
404
405   //public static void addLocaleOptions(final List<String> commandLine, final boolean launcherUsed) {
406   //  // need to specify default encoding so that javac outputs messages in 'correct' language
407   //  commandLine.add((launcherUsed? "-J" : "") + "-D" + CharsetToolkit.FILE_ENCODING_PROPERTY + "=" + CharsetToolkit.getDefaultSystemCharset().name());
408   //}
409
410   private Process launchServer(final int port) throws ExecutionException {
411     // validate tools.jar presence
412     final JavaCompiler systemCompiler = ToolProvider.getSystemJavaCompiler();
413     if (systemCompiler == null) {
414       throw new ExecutionException("No system java compiler is provided by the JRE. Make sure tools.jar is present in IntelliJ IDEA classpath.");
415     }
416
417     final Sdk projectJdk = JavaAwareProjectJdkTableImpl.getInstanceEx().getInternalJdk();
418     final GeneralCommandLine cmdLine = new GeneralCommandLine();
419     final String vmExecutablePath = ((JavaSdkType)projectJdk.getSdkType()).getVMExecutablePath(projectJdk);
420     cmdLine.setExePath(vmExecutablePath);
421     cmdLine.addParameter("-server");
422     cmdLine.addParameter("-XX:MaxPermSize=150m");
423     cmdLine.addParameter("-XX:ReservedCodeCacheSize=64m");
424     cmdLine.addParameter("-Xmx" + Registry.intValue("compiler.server.heap.size") + "m");
425     cmdLine.addParameter("-Djava.awt.headless=true");
426     //cmdLine.addParameter("-DuseJavaUtilZip");
427     final String additionalOptions = Registry.stringValue("compiler.server.vm.options");
428     if (!StringUtil.isEmpty(additionalOptions)) {
429       final StringTokenizer tokenizer = new StringTokenizer(additionalOptions, " ", false);
430       while (tokenizer.hasMoreTokens()) {
431         cmdLine.addParameter(tokenizer.nextToken());
432       }
433     }
434
435     // debugging
436     final int debugPort = Registry.intValue("compiler.server.debug.port");
437     if (debugPort > 0) {
438       cmdLine.addParameter("-XX:+HeapDumpOnOutOfMemoryError");
439       cmdLine.addParameter("-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=" + debugPort);
440     }
441
442     if (Registry.is("compiler.server.use.memory.temp.cache")) {
443       cmdLine.addParameter("-D"+ GlobalOptions.USE_MEMORY_TEMP_CACHE_OPTION + "=true");
444     }
445     if (Registry.is("compiler.server.use.external.javac.process")) {
446       cmdLine.addParameter("-D"+ GlobalOptions.USE_EXTERNAL_JAVAC_OPTION + "=true");
447     }
448     cmdLine.addParameter("-D"+ GlobalOptions.HOSTNAME_OPTION + "=" + NetUtils.getLocalHostString());
449     cmdLine.addParameter("-D"+ GlobalOptions.VM_EXE_PATH_OPTION + "=" + FileUtil.toSystemIndependentName(vmExecutablePath));
450
451     // javac's VM should use the same default locale that IDEA uses in order for javac to print messages in 'correct' language
452     final String lang = System.getProperty("user.language");
453     if (lang != null) {
454       //noinspection HardCodedStringLiteral
455       cmdLine.addParameter("-Duser.language=" + lang);
456     }
457     final String country = System.getProperty("user.country");
458     if (country != null) {
459       //noinspection HardCodedStringLiteral
460       cmdLine.addParameter("-Duser.country=" + country);
461     }
462     //noinspection HardCodedStringLiteral
463     final String region = System.getProperty("user.region");
464     if (region != null) {
465       //noinspection HardCodedStringLiteral
466       cmdLine.addParameter("-Duser.region=" + region);
467     }
468
469     cmdLine.addParameter("-classpath");
470
471     final List<File> cp = ClasspathBootstrap.getCompileServerApplicationClasspath();
472
473     cmdLine.addParameter(classpathToString(cp));
474
475     cmdLine.addParameter(org.jetbrains.jps.server.Server.class.getName());
476     cmdLine.addParameter(Integer.toString(port));
477
478     final File workDirectory = new File(mySystemDirectory, COMPILE_SERVER_SYSTEM_ROOT);
479     workDirectory.mkdirs();
480     ensureLogConfigExists(workDirectory);
481
482     cmdLine.addParameter(FileUtil.toSystemIndependentName(workDirectory.getPath()));
483
484     cmdLine.setWorkDirectory(workDirectory);
485
486
487     return cmdLine.createProcess();
488   }
489
490   private static void ensureLogConfigExists(File workDirectory) {
491     final File logConfig = new File(workDirectory, LOGGER_CONFIG);
492     if (!logConfig.exists()) {
493       FileUtil.createIfDoesntExist(logConfig);
494       try {
495         final InputStream in = Server.class.getResourceAsStream("/" + DEFAULT_LOGGER_CONFIG);
496         if (in != null) {
497           try {
498             final FileOutputStream out = new FileOutputStream(logConfig);
499             try {
500               FileUtil.copy(in, out);
501             }
502             finally {
503               out.close();
504             }
505           }
506           finally {
507             in.close();
508           }
509         }
510       }
511       catch (IOException e) {
512         LOG.error(e);
513       }
514     }
515   }
516
517   public void shutdownServer() {
518     shutdownServer(myClient, myProcessHandler);
519   }
520
521   private static void shutdownServer(final CompileServerClient client, final OSProcessHandler processHandler) {
522     try {
523       if (client != null && client.isConnected()) {
524         final Future future = client.sendShutdownRequest();
525         future.get(500, TimeUnit.MILLISECONDS);
526         client.disconnect();
527       }
528     }
529     catch (Throwable ignored) {
530       LOG.info(ignored);
531     }
532     finally {
533       if (processHandler != null) {
534         processHandler.destroyProcess();
535       }
536     }
537   }
538
539   private static String classpathToString(List<File> cp) {
540     StringBuilder builder = new StringBuilder();
541     for (File file : cp) {
542       if (builder.length() > 0) {
543         builder.append(File.pathSeparator);
544       }
545       builder.append(file.getAbsolutePath());
546     }
547     return builder.toString();
548   }
549
550   private class ProjectWatcher extends ProjectManagerAdapter {
551     private final Map<Project, MessageBusConnection> myConnections = new HashMap<Project, MessageBusConnection>();
552
553     public void projectOpened(final Project project) {
554       final MessageBusConnection conn = project.getMessageBus().connect();
555       myConnections.put(project, conn);
556       conn.subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootListener() {
557         public void beforeRootsChange(final ModuleRootEvent event) {
558           sendReloadRequest(project);
559         }
560
561         public void rootsChanged(final ModuleRootEvent event) {
562           myTaskExecutor.submit(new Runnable() {
563             public void run() {
564               try {
565                 // this will reload sdks and global libraries
566                 final CompileServerClient client = ensureServerRunningAndClientConnected(false);
567                 if (client != null) {
568                   sendSetupRequest(client);
569                 }
570               }
571               catch (Throwable e) {
572                 LOG.info(e);
573               }
574             }
575           });
576         }
577       });
578     }
579
580     public void projectClosing(Project project) {
581       sendReloadRequest(project);
582     }
583
584     public void projectClosed(Project project) {
585       final MessageBusConnection conn = myConnections.remove(project);
586       if (conn != null) {
587         conn.disconnect();
588       }
589     }
590   }
591 }