73c8a594e240b655db6e3ab36f00d0ed94e83687
[idea/community.git] / java / compiler / impl / src / com / intellij / compiler / server / BuildManager.java
1 /*
2  * Copyright 2000-2016 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.server;
17
18 import com.intellij.ProjectTopics;
19 import com.intellij.compiler.CompilerConfiguration;
20 import com.intellij.compiler.CompilerConfigurationImpl;
21 import com.intellij.compiler.CompilerWorkspaceConfiguration;
22 import com.intellij.compiler.impl.CompilerUtil;
23 import com.intellij.compiler.impl.javaCompiler.BackendCompiler;
24 import com.intellij.compiler.impl.javaCompiler.javac.JavacConfiguration;
25 import com.intellij.compiler.server.impl.BuildProcessClasspathManager;
26 import com.intellij.concurrency.JobScheduler;
27 import com.intellij.execution.ExecutionAdapter;
28 import com.intellij.execution.ExecutionException;
29 import com.intellij.execution.ExecutionManager;
30 import com.intellij.execution.configurations.GeneralCommandLine;
31 import com.intellij.execution.configurations.RunProfile;
32 import com.intellij.execution.process.OSProcessHandler;
33 import com.intellij.execution.process.ProcessAdapter;
34 import com.intellij.execution.process.ProcessEvent;
35 import com.intellij.execution.process.ProcessHandler;
36 import com.intellij.ide.DataManager;
37 import com.intellij.ide.PowerSaveMode;
38 import com.intellij.ide.file.BatchFileChangeListener;
39 import com.intellij.openapi.Disposable;
40 import com.intellij.openapi.actionSystem.CommonDataKeys;
41 import com.intellij.openapi.application.Application;
42 import com.intellij.openapi.application.ApplicationManager;
43 import com.intellij.openapi.application.PathManager;
44 import com.intellij.openapi.application.TransactionGuard;
45 import com.intellij.openapi.compiler.CompilationStatusListener;
46 import com.intellij.openapi.compiler.CompileContext;
47 import com.intellij.openapi.compiler.CompilerPaths;
48 import com.intellij.openapi.compiler.CompilerTopics;
49 import com.intellij.openapi.diagnostic.Logger;
50 import com.intellij.openapi.editor.EditorFactory;
51 import com.intellij.openapi.editor.event.DocumentAdapter;
52 import com.intellij.openapi.editor.event.DocumentEvent;
53 import com.intellij.openapi.fileEditor.FileDocumentManager;
54 import com.intellij.openapi.fileEditor.impl.FileDocumentManagerImpl;
55 import com.intellij.openapi.fileTypes.impl.FileTypeManagerImpl;
56 import com.intellij.openapi.module.Module;
57 import com.intellij.openapi.module.ModuleManager;
58 import com.intellij.openapi.project.Project;
59 import com.intellij.openapi.project.ProjectCoreUtil;
60 import com.intellij.openapi.project.ProjectManager;
61 import com.intellij.openapi.project.ProjectManagerAdapter;
62 import com.intellij.openapi.projectRoots.JavaSdk;
63 import com.intellij.openapi.projectRoots.JavaSdkType;
64 import com.intellij.openapi.projectRoots.JavaSdkVersion;
65 import com.intellij.openapi.projectRoots.Sdk;
66 import com.intellij.openapi.projectRoots.impl.JavaAwareProjectJdkTableImpl;
67 import com.intellij.openapi.roots.*;
68 import com.intellij.openapi.startup.StartupManager;
69 import com.intellij.openapi.util.*;
70 import com.intellij.openapi.util.io.FileUtil;
71 import com.intellij.openapi.util.registry.Registry;
72 import com.intellij.openapi.util.text.StringUtil;
73 import com.intellij.openapi.vfs.CharsetToolkit;
74 import com.intellij.openapi.vfs.LocalFileSystem;
75 import com.intellij.openapi.vfs.VirtualFile;
76 import com.intellij.openapi.vfs.VirtualFileManager;
77 import com.intellij.openapi.vfs.newvfs.BulkFileListener;
78 import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
79 import com.intellij.openapi.vfs.newvfs.impl.FileNameCache;
80 import com.intellij.openapi.wm.IdeFrame;
81 import com.intellij.util.*;
82 import com.intellij.util.concurrency.SequentialTaskExecutor;
83 import com.intellij.util.containers.IntArrayList;
84 import com.intellij.util.io.BaseOutputReader;
85 import com.intellij.util.io.storage.HeavyProcessLatch;
86 import com.intellij.util.messages.MessageBusConnection;
87 import com.intellij.util.net.NetUtils;
88 import com.intellij.util.text.DateFormatUtil;
89 import gnu.trove.THashSet;
90 import io.netty.bootstrap.ServerBootstrap;
91 import io.netty.channel.Channel;
92 import io.netty.channel.ChannelInitializer;
93 import io.netty.channel.nio.NioEventLoopGroup;
94 import io.netty.handler.codec.protobuf.ProtobufDecoder;
95 import io.netty.handler.codec.protobuf.ProtobufEncoder;
96 import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
97 import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;
98 import io.netty.util.internal.ThreadLocalRandom;
99 import org.jetbrains.annotations.NotNull;
100 import org.jetbrains.annotations.Nullable;
101 import org.jetbrains.io.ChannelRegistrar;
102 import org.jetbrains.io.NettyKt;
103 import org.jetbrains.jps.api.*;
104 import org.jetbrains.jps.cmdline.BuildMain;
105 import org.jetbrains.jps.cmdline.ClasspathBootstrap;
106 import org.jetbrains.jps.incremental.Utils;
107 import org.jetbrains.jps.model.java.JpsJavaSdkType;
108 import org.jetbrains.jps.model.java.compiler.JavaCompilers;
109 import org.jetbrains.jps.model.serialization.JpsGlobalLoader;
110
111 import javax.tools.*;
112 import java.awt.*;
113 import java.io.File;
114 import java.io.IOException;
115 import java.net.InetAddress;
116 import java.net.InetSocketAddress;
117 import java.nio.charset.Charset;
118 import java.text.SimpleDateFormat;
119 import java.util.*;
120 import java.util.List;
121 import java.util.concurrent.*;
122 import java.util.concurrent.atomic.AtomicBoolean;
123 import java.util.stream.Stream;
124
125 import static org.jetbrains.jps.api.CmdlineRemoteProto.Message.ControllerMessage.ParametersMessage.TargetTypeBuildScope;
126
127 /**
128  * @author Eugene Zhuravlev
129  *         Date: 9/6/11
130  */
131 public class BuildManager implements Disposable {
132   public static final Key<Boolean> ALLOW_AUTOMAKE = Key.create("_allow_automake_when_process_is_active_");
133   private static final Key<Integer> COMPILER_PROCESS_DEBUG_PORT = Key.create("_compiler_process_debug_port_");
134   private static final Key<String> FORCE_MODEL_LOADING_PARAMETER = Key.create(BuildParametersKeys.FORCE_MODEL_LOADING);
135   private static final Key<CharSequence> STDERR_OUTPUT = Key.create("_process_launch_errors_");
136   private static final SimpleDateFormat USAGE_STAMP_DATE_FORMAT = new SimpleDateFormat("dd.MM.yyyy");
137
138   private static final Logger LOG = Logger.getInstance("#com.intellij.compiler.server.BuildManager");
139   private static final String COMPILER_PROCESS_JDK_PROPERTY = "compiler.process.jdk";
140   public static final String SYSTEM_ROOT = "compile-server";
141   public static final String TEMP_DIR_NAME = "_temp_";
142   // do not make static in order not to access application on class load
143   private final boolean IS_UNIT_TEST_MODE;
144   private static final String IWS_EXTENSION = ".iws";
145   private static final String IPR_EXTENSION = ".ipr";
146   private static final String IDEA_PROJECT_DIR_PATTERN = "/.idea/";
147   private static final Function<String, Boolean> PATH_FILTER =
148     SystemInfo.isFileSystemCaseSensitive ?
149     (Function<String, Boolean>)s -> !(s.contains(IDEA_PROJECT_DIR_PATTERN) || s.endsWith(IWS_EXTENSION) || s.endsWith(IPR_EXTENSION)) :
150     (Function<String, Boolean>)s -> !(StringUtil.endsWithIgnoreCase(s, IWS_EXTENSION) || StringUtil.endsWithIgnoreCase(s, IPR_EXTENSION) || StringUtil.containsIgnoreCase(s, IDEA_PROJECT_DIR_PATTERN));
151
152   private final File mySystemDirectory;
153   private final ProjectManager myProjectManager;
154
155   private final Map<TaskFuture, Project> myAutomakeFutures = Collections.synchronizedMap(new HashMap<TaskFuture, Project>());
156   private final Map<String, RequestFuture> myBuildsInProgress = Collections.synchronizedMap(new HashMap<String, RequestFuture>());
157   private final Map<String, Future<Pair<RequestFuture<PreloadedProcessMessageHandler>, OSProcessHandler>>> myPreloadedBuilds =
158     Collections.synchronizedMap(new HashMap<String, Future<Pair<RequestFuture<PreloadedProcessMessageHandler>, OSProcessHandler>>>());
159   private final BuildProcessClasspathManager myClasspathManager = new BuildProcessClasspathManager();
160   private final ExecutorService myRequestsProcessor = SequentialTaskExecutor.createSequentialApplicationPoolExecutor();
161   private final Map<String, ProjectData> myProjectDataMap = Collections.synchronizedMap(new HashMap<String, ProjectData>());
162
163   private final BuildManagerPeriodicTask myAutoMakeTask = new BuildManagerPeriodicTask() {
164     @Override
165     protected int getDelay() {
166       return Registry.intValue("compiler.automake.trigger.delay");
167     }
168
169     @Override
170     protected void runTask() {
171       runAutoMake();
172     }
173   };
174
175   private final BuildManagerPeriodicTask myDocumentSaveTask = new BuildManagerPeriodicTask() {
176     @Override
177     protected int getDelay() {
178       return Registry.intValue("compiler.document.save.trigger.delay");
179     }
180
181     @Override
182     public void runTask() {
183       if (shouldSaveDocuments()) {
184         TransactionGuard.getInstance().submitTransactionAndWait(() ->
185           ((FileDocumentManagerImpl)FileDocumentManager.getInstance()).saveAllDocuments(false));
186       }
187     }
188
189     private boolean shouldSaveDocuments() {
190       final Project contextProject = getCurrentContextProject();
191       return contextProject != null && canStartAutoMake(contextProject);
192     }
193   };
194
195   private final Runnable myGCTask = () -> {
196     // todo: make customizable in UI?
197     final int unusedThresholdDays = Registry.intValue("compiler.build.data.unused.threshold", -1);
198     if (unusedThresholdDays <= 0) {
199       return;
200     }
201     final File buildSystemDir = getBuildSystemDirectory();
202     final File[] dirs = buildSystemDir.listFiles(pathname -> pathname.isDirectory() && !TEMP_DIR_NAME.equals(pathname.getName()));
203     if (dirs != null) {
204       final Date now = new Date();
205       for (File buildDataProjectDir : dirs) {
206         final File usageFile = getUsageFile(buildDataProjectDir);
207         if (usageFile.exists()) {
208           final Pair<Date, File> usageData = readUsageFile(usageFile);
209           if (usageData != null) {
210             final File projectFile = usageData.second;
211             if (projectFile != null && !projectFile.exists() || DateFormatUtil.getDifferenceInDays(usageData.first, now) > unusedThresholdDays) {
212               LOG.info("Clearing project build data because the project does not exist or was not opened for more than " + unusedThresholdDays + " days: " + buildDataProjectDir.getPath());
213               FileUtil.delete(buildDataProjectDir);
214             }
215           }
216         }
217         else {
218           updateUsageFile(null, buildDataProjectDir); // set usage stamp to start countdown
219         }
220       }
221     }
222   };
223
224   private final ChannelRegistrar myChannelRegistrar = new ChannelRegistrar();
225
226   private final BuildMessageDispatcher myMessageDispatcher = new BuildMessageDispatcher();
227   private volatile int myListenPort = -1;
228   @NotNull
229   private final Charset mySystemCharset = CharsetToolkit.getDefaultSystemCharset();
230   private volatile boolean myBuildProcessDebuggingEnabled;
231
232   public BuildManager(final ProjectManager projectManager) {
233     final Application application = ApplicationManager.getApplication();
234     IS_UNIT_TEST_MODE = application.isUnitTestMode();
235     myProjectManager = projectManager;
236     final String systemPath = PathManager.getSystemPath();
237     File system = new File(systemPath);
238     try {
239       system = system.getCanonicalFile();
240     }
241     catch (IOException e) {
242       LOG.info(e);
243     }
244     mySystemDirectory = system;
245
246     projectManager.addProjectManagerListener(new ProjectWatcher());
247
248     final MessageBusConnection conn = application.getMessageBus().connect();
249     conn.subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener.Adapter() {
250       @Override
251       public void after(@NotNull List<? extends VFileEvent> events) {
252         if (!IS_UNIT_TEST_MODE && shouldTriggerMake(events)) {
253           scheduleAutoMake();
254         }
255       }
256
257       private boolean shouldTriggerMake(List<? extends VFileEvent> events) {
258         if (PowerSaveMode.isEnabled()) {
259           return false;
260         }
261
262         Project project = null;
263         ProjectFileIndex fileIndex = null;
264
265         for (VFileEvent event : events) {
266           final VirtualFile eventFile = event.getFile();
267           if (eventFile == null) {
268             continue;
269           }
270           if (!eventFile.isValid()) {
271             return true; // should be deleted
272           }
273
274           if (project == null) {
275             // lazy init
276             project = getCurrentContextProject();
277             if (project == null) {
278               return false;
279             }
280             fileIndex = ProjectRootManager.getInstance(project).getFileIndex();
281           }
282
283           if (fileIndex.isInContent(eventFile)) {
284             if (ProjectCoreUtil.isProjectOrWorkspaceFile(eventFile) || GeneratedSourcesFilter.isGeneratedSourceByAnyFilter(eventFile, project)) {
285               // changes in project files or generated stuff should not trigger auto-make
286               continue;
287             }
288             return true;
289           }
290         }
291         return false;
292       }
293
294     });
295
296     conn.subscribe(BatchFileChangeListener.TOPIC, new BatchFileChangeListener.Adapter() {
297       @Override
298       public void batchChangeStarted(Project project) {
299         cancelAutoMakeTasks(project);
300       }
301     });
302
303     EditorFactory.getInstance().getEventMulticaster().addDocumentListener(new DocumentAdapter() {
304       @Override
305       public void documentChanged(DocumentEvent e) {
306         scheduleProjectSave();
307       }
308     });
309
310     ShutDownTracker.getInstance().registerShutdownTask(() -> stopListening());
311
312     ScheduledFuture<?> future = JobScheduler.getScheduler().scheduleWithFixedDelay((Runnable)() -> runCommand(myGCTask), 3, 180, TimeUnit.MINUTES);
313     Disposer.register(this, () -> future.cancel(false));
314   }
315
316   private List<Project> getOpenProjects() {
317     final Project[] projects = myProjectManager.getOpenProjects();
318     if (projects.length == 0) {
319       return Collections.emptyList();
320     }
321     final List<Project> projectList = new SmartList<Project>();
322     for (Project project : projects) {
323       if (isValidProject(project)) {
324         projectList.add(project);
325       }
326     }
327     return projectList;
328   }
329
330   private static boolean isValidProject(@Nullable Project project) {
331     return project != null && !project.isDisposed() && !project.isDefault() && project.isInitialized();
332   }
333
334   public static BuildManager getInstance() {
335     return ApplicationManager.getApplication().getComponent(BuildManager.class);
336   }
337
338   public void notifyFilesChanged(final Collection<File> paths) {
339     doNotify(paths, false);
340   }
341
342   public void notifyFilesDeleted(Collection<File> paths) {
343     doNotify(paths, true);
344   }
345
346   public void runCommand(@NotNull Runnable command) {
347     myRequestsProcessor.submit(command);
348   }
349
350   private void doNotify(final Collection<File> paths, final boolean notifyDeletion) {
351     // ensure events processed in the order they arrived
352     runCommand(() -> {
353       final List<String> filtered = new ArrayList<String>(paths.size());
354       for (File file : paths) {
355         final String path = FileUtil.toSystemIndependentName(file.getPath());
356         if (PATH_FILTER.fun(path)) {
357           filtered.add(path);
358         }
359       }
360       if (filtered.isEmpty()) {
361         return;
362       }
363       synchronized (myProjectDataMap) {
364         //if (IS_UNIT_TEST_MODE) {
365         //  if (notifyDeletion) {
366         //    LOG.info("Registering deleted paths: " + filtered);
367         //  }
368         //  else {
369         //    LOG.info("Registering changed paths: " + filtered);
370         //  }
371         //}
372         for (Map.Entry<String, ProjectData> entry : myProjectDataMap.entrySet()) {
373           final ProjectData data = entry.getValue();
374           if (notifyDeletion) {
375             data.addDeleted(filtered);
376           }
377           else {
378             data.addChanged(filtered);
379           }
380           final RequestFuture future = myBuildsInProgress.get(entry.getKey());
381           if (future != null && !future.isCancelled() && !future.isDone()) {
382             final UUID sessionId = future.getRequestID();
383             final Channel channel = myMessageDispatcher.getConnectedChannel(sessionId);
384             if (channel != null) {
385               final CmdlineRemoteProto.Message.ControllerMessage message =
386                 CmdlineRemoteProto.Message.ControllerMessage.newBuilder().setType(
387                   CmdlineRemoteProto.Message.ControllerMessage.Type.FS_EVENT).setFsEvent(data.createNextEvent()).build();
388               channel.writeAndFlush(CmdlineProtoUtil.toMessage(sessionId, message));
389             }
390           }
391         }
392       }
393     });
394   }
395
396   public static void forceModelLoading(CompileContext context) {
397     context.getCompileScope().putUserData(FORCE_MODEL_LOADING_PARAMETER, Boolean.TRUE.toString());
398   }
399
400   public void clearState(Project project) {
401     final String projectPath = getProjectPath(project);
402
403     cancelPreloadedBuilds(projectPath);
404
405     synchronized (myProjectDataMap) {
406       final ProjectData data = myProjectDataMap.get(projectPath);
407       if (data != null) {
408         data.dropChanges();
409       }
410     }
411     scheduleAutoMake();
412   }
413
414   public boolean isProjectWatched(Project project) {
415     return myProjectDataMap.containsKey(getProjectPath(project));
416   }
417
418   @Nullable
419   public List<String> getFilesChangedSinceLastCompilation(Project project) {
420     String projectPath = getProjectPath(project);
421     synchronized (myProjectDataMap) {
422       ProjectData data = myProjectDataMap.get(projectPath);
423       if (data != null && !data.myNeedRescan) {
424         return convertToStringPaths(data.myChanged);
425       }
426       return null;
427     }
428   }
429
430   private static List<String> convertToStringPaths(final Collection<InternedPath> interned) {
431     final ArrayList<String> list = new ArrayList<String>(interned.size());
432     for (InternedPath path : interned) {
433       list.add(path.getValue());
434     }
435     return list;
436   }
437
438   @Nullable
439   private static String getProjectPath(final Project project) {
440     final String url = project.getPresentableUrl();
441     if (url == null) {
442       return null;
443     }
444     return VirtualFileManager.extractPath(url);
445   }
446
447   public void scheduleAutoMake() {
448     if (!IS_UNIT_TEST_MODE && !PowerSaveMode.isEnabled()) {
449       myAutoMakeTask.schedule();
450     }
451   }
452
453   private void scheduleProjectSave() {
454     if (!IS_UNIT_TEST_MODE && !PowerSaveMode.isEnabled()) {
455       myDocumentSaveTask.schedule();
456     }
457   }
458
459   private void runAutoMake() {
460     final Project project = getCurrentContextProject();
461     if (project == null || !canStartAutoMake(project)) {
462       return;
463     }
464     final List<TargetTypeBuildScope> scopes = CmdlineProtoUtil.createAllModulesScopes(false);
465     final AutoMakeMessageHandler handler = new AutoMakeMessageHandler(project);
466     final TaskFuture future = scheduleBuild(
467       project, false, true, false, scopes, Collections.<String>emptyList(), Collections.<String, String>emptyMap(), handler
468     );
469     if (future != null) {
470       myAutomakeFutures.put(future, project);
471       try {
472         future.waitFor();
473       }
474       finally {
475         myAutomakeFutures.remove(future);
476         if (handler.unprocessedFSChangesDetected()) {
477           scheduleAutoMake();
478         }
479       }
480     }
481   }
482
483   private static boolean canStartAutoMake(@NotNull Project project) {
484     if (project.isDisposed()) {
485       return false;
486     }
487     final CompilerWorkspaceConfiguration config = CompilerWorkspaceConfiguration.getInstance(project);
488     if (!config.MAKE_PROJECT_ON_SAVE) {
489       return false;
490     }
491     if (!config.allowAutoMakeWhileRunningApplication() && hasRunningProcess(project)) {
492       return false;
493     }
494     return true;
495   }
496
497   @Nullable
498   private Project getCurrentContextProject() {
499     return getContextProject(null);
500   }
501
502   @Nullable
503   private Project getContextProject(@Nullable Window window) {
504     final List<Project> openProjects = getOpenProjects();
505     if (openProjects.isEmpty()) {
506       return null;
507     }
508     if (openProjects.size() == 1) {
509       return openProjects.get(0);
510     }
511
512     if (window == null) {
513       window = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow();
514       if (window == null) {
515         return null;
516       }
517     }
518
519     Component comp = window;
520     while (true) {
521       final Container _parent = comp.getParent();
522       if (_parent == null) {
523         break;
524       }
525       comp = _parent;
526     }
527
528     Project project = null;
529     if (comp instanceof IdeFrame) {
530       project = ((IdeFrame)comp).getProject();
531     }
532     if (project == null) {
533       project = CommonDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext(comp));
534     }
535
536     return isValidProject(project)? project : null;
537   }
538
539   private static boolean hasRunningProcess(Project project) {
540     for (ProcessHandler handler : ExecutionManager.getInstance(project).getRunningProcesses()) {
541       if (!handler.isProcessTerminated() && !ALLOW_AUTOMAKE.get(handler, Boolean.FALSE)) { // active process
542         return true;
543       }
544     }
545     return false;
546   }
547
548   public Collection<TaskFuture> cancelAutoMakeTasks(Project project) {
549     final Collection<TaskFuture> futures = new SmartList<TaskFuture>();
550     synchronized (myAutomakeFutures) {
551       for (Map.Entry<TaskFuture, Project> entry : myAutomakeFutures.entrySet()) {
552         if (entry.getValue().equals(project)) {
553           final TaskFuture future = entry.getKey();
554           future.cancel(false);
555           futures.add(future);
556         }
557       }
558     }
559     return futures;
560   }
561
562   private void cancelAllPreloadedBuilds() {
563     String[] paths = ArrayUtil.toStringArray(myPreloadedBuilds.keySet());
564     for (String path : paths) {
565       cancelPreloadedBuilds(path);
566     }
567   }
568
569   private void cancelPreloadedBuilds(final String projectPath) {
570     runCommand(() -> {
571       Pair<RequestFuture<PreloadedProcessMessageHandler>, OSProcessHandler> pair = takePreloadedProcess(projectPath);
572       if (pair != null) {
573         final RequestFuture<PreloadedProcessMessageHandler> future = pair.first;
574         final OSProcessHandler processHandler = pair.second;
575         myMessageDispatcher.cancelSession(future.getRequestID());
576         // waiting for preloaded process from project's task queue guarantees no build is started for this project
577         // until this one gracefully exits and closes all its storages
578         getProjectData(projectPath).taskQueue.submit(() -> {
579           Throwable error = null;
580           try {
581             while (!processHandler.waitFor()) {
582               LOG.info("processHandler.waitFor() returned false for session " + future.getRequestID() + ", continue waiting");
583             }
584           }
585           catch (Throwable e) {
586             error = e;
587           }
588           finally {
589             notifySessionTerminationIfNeeded(future.getRequestID(), error);
590           }
591         });
592       }
593     });
594   }
595
596   @Nullable
597   private Pair<RequestFuture<PreloadedProcessMessageHandler>, OSProcessHandler> takePreloadedProcess(String projectPath) {
598     Pair<RequestFuture<PreloadedProcessMessageHandler>, OSProcessHandler> result;
599     final Future<Pair<RequestFuture<PreloadedProcessMessageHandler>, OSProcessHandler>> preloadProgress = myPreloadedBuilds.remove(projectPath);
600     try {
601       result = preloadProgress != null ? preloadProgress.get() : null;
602     }
603     catch (Throwable e) {
604       LOG.info(e);
605       result = null;
606     }
607     return result != null && !result.first.isDone()? result : null;
608   }
609
610   @Nullable
611   public TaskFuture scheduleBuild(
612     final Project project, final boolean isRebuild, final boolean isMake,
613     final boolean onlyCheckUpToDate, final List<TargetTypeBuildScope> scopes,
614     final Collection<String> paths,
615     final Map<String, String> userData, final DefaultMessageHandler messageHandler) {
616
617     final String projectPath = getProjectPath(project);
618     final boolean isAutomake = messageHandler instanceof AutoMakeMessageHandler;
619     final BuilderMessageHandler handler = new NotifyingMessageHandler(project, messageHandler, isAutomake);
620     try {
621       ensureListening();
622     }
623     catch (Exception e) {
624       final UUID sessionId = UUID.randomUUID(); // the actual session did not start, use random UUID
625       handler.handleFailure(sessionId, CmdlineProtoUtil.createFailure(e.getMessage(), null));
626       handler.sessionTerminated(sessionId);
627       return null;
628     }
629
630     final DelegateFuture _future = new DelegateFuture();
631     // by using the same queue that processes events we ensure that
632     // the build will be aware of all events that have happened before this request
633     runCommand(() -> {
634       final Pair<RequestFuture<PreloadedProcessMessageHandler>, OSProcessHandler> preloaded = takePreloadedProcess(projectPath);
635       final RequestFuture<PreloadedProcessMessageHandler> preloadedFuture = preloaded != null? preloaded.first : null;
636       final boolean usingPreloadedProcess = preloadedFuture != null;
637
638       final UUID sessionId;
639       if (usingPreloadedProcess) {
640         LOG.info("Using preloaded build process to compile " + projectPath);
641         sessionId = preloadedFuture.getRequestID();
642         preloadedFuture.getMessageHandler().setDelegateHandler(handler);
643       }
644       else {
645         sessionId = UUID.randomUUID();
646       }
647
648       final RequestFuture<? extends BuilderMessageHandler> future = usingPreloadedProcess? preloadedFuture : new RequestFuture<BuilderMessageHandler>(handler, sessionId, new CancelBuildSessionAction<BuilderMessageHandler>());
649       // futures we need to wait for: either just "future" or both "future" and "buildFuture" below
650       TaskFuture[] delegatesToWait = {future};
651
652       if (!usingPreloadedProcess && (future.isCancelled() || project.isDisposed())) {
653         // in case of preloaded process the process was already running, so the handler will be notified upon process termination
654         handler.sessionTerminated(sessionId);
655         ((BasicFuture)future).setDone();
656       }
657       else {
658         final CmdlineRemoteProto.Message.ControllerMessage.GlobalSettings globals =
659           CmdlineRemoteProto.Message.ControllerMessage.GlobalSettings.newBuilder().setGlobalOptionsPath(PathManager.getOptionsPath())
660             .build();
661         CmdlineRemoteProto.Message.ControllerMessage.FSEvent currentFSChanges;
662         final ExecutorService projectTaskQueue;
663         final boolean needRescan;
664         synchronized (myProjectDataMap) {
665           final ProjectData data = getProjectData(projectPath);
666           if (isRebuild) {
667             data.dropChanges();
668           }
669           if (IS_UNIT_TEST_MODE) {
670             LOG.info("Scheduling build for " +
671                      projectPath +
672                      "; CHANGED: " +
673                      new HashSet<String>(convertToStringPaths(data.myChanged)) +
674                      "; DELETED: " +
675                      new HashSet<String>(convertToStringPaths(data.myDeleted)));
676           }
677           needRescan = data.getAndResetRescanFlag();
678           currentFSChanges = needRescan ? null : data.createNextEvent();
679           projectTaskQueue = data.taskQueue;
680         }
681
682         final CmdlineRemoteProto.Message.ControllerMessage params;
683         if (isRebuild) {
684           params = CmdlineProtoUtil.createBuildRequest(projectPath, scopes, Collections.<String>emptyList(), userData, globals, null);
685         }
686         else if (onlyCheckUpToDate) {
687           params = CmdlineProtoUtil.createUpToDateCheckRequest(projectPath, scopes, paths, userData, globals, currentFSChanges);
688         }
689         else {
690           params = CmdlineProtoUtil.createBuildRequest(projectPath, scopes, isMake ? Collections.<String>emptyList() : paths, userData, globals, currentFSChanges);
691         }
692         if (!usingPreloadedProcess) {
693           myMessageDispatcher.registerBuildMessageHandler(future, params);
694         }
695
696         try {
697           Future<?> buildFuture = projectTaskQueue.submit(() -> {
698             Throwable execFailure = null;
699             try {
700               if (project.isDisposed()) {
701                 if (usingPreloadedProcess) {
702                   future.cancel(false);
703                 }
704                 else {
705                   return;
706                 }
707               }
708               myBuildsInProgress.put(projectPath, future);
709               final OSProcessHandler processHandler;
710               CharSequence errorsOnLaunch;
711               if (usingPreloadedProcess) {
712                 final boolean paramsSent = myMessageDispatcher.sendBuildParameters(future.getRequestID(), params);
713                 if (!paramsSent) {
714                   myMessageDispatcher.cancelSession(future.getRequestID());
715                 }
716                 processHandler = preloaded.second;
717                 errorsOnLaunch = STDERR_OUTPUT.get(processHandler);
718               }
719               else {
720                 if (isAutomake && needRescan) {
721                   // if project state was cleared because of roots changed or this is the first compilation after project opening,
722                   // ensure project model is saved on disk, so that automake sees the latest model state.
723                   // For ordinary make all project, app settings and unsaved docs are always saved before build starts.
724                   try {
725                     TransactionGuard.getInstance().submitTransactionAndWait(project::save);
726                   }
727                   catch (Throwable e) {
728                     LOG.info(e);
729                   }
730                 }
731
732                 processHandler = launchBuildProcess(project, myListenPort, sessionId, false);
733                 errorsOnLaunch = new StringBuffer();
734                 processHandler.addProcessListener(new StdOutputCollector((StringBuffer)errorsOnLaunch));
735                 processHandler.startNotify();
736               }
737
738               Integer debugPort = processHandler.getUserData(COMPILER_PROCESS_DEBUG_PORT);
739               if (debugPort != null) {
740                 String message = "Make: waiting for debugger connection on port " + debugPort;
741                 messageHandler
742                   .handleCompileMessage(sessionId, CmdlineProtoUtil.createCompileProgressMessageResponse(message).getCompileMessage());
743               }
744
745               while (!processHandler.waitFor()) {
746                 LOG.info("processHandler.waitFor() returned false for session " + sessionId + ", continue waiting");
747               }
748
749               final int exitValue = processHandler.getProcess().exitValue();
750               if (exitValue != 0) {
751                 final StringBuilder msg = new StringBuilder();
752                 msg.append("Abnormal build process termination: ");
753                 if (errorsOnLaunch != null && errorsOnLaunch.length() > 0) {
754                   msg.append("\n").append(errorsOnLaunch);
755                   if (StringUtil.contains(errorsOnLaunch, "java.lang.NoSuchMethodError")) {
756                     msg.append(
757                       "\nThe error may be caused by JARs in Java Extensions directory which conflicts with libraries used by the external build process.")
758                       .append(
759                         "\nTry adding -Djava.ext.dirs=\"\" argument to 'Build process VM options' in File | Settings | Build, Execution, Deployment | Compiler to fix the problem.");
760                   }
761                 }
762                 else {
763                   msg.append("unknown error");
764                 }
765                 handler.handleFailure(sessionId, CmdlineProtoUtil.createFailure(msg.toString(), null));
766               }
767             }
768             catch (Throwable e) {
769               execFailure = e;
770             }
771             finally {
772               myBuildsInProgress.remove(projectPath);
773               notifySessionTerminationIfNeeded(sessionId, execFailure);
774
775               if (isProcessPreloadingEnabled(project)) {
776                 runCommand(() -> {
777                   if (!myPreloadedBuilds.containsKey(projectPath)) {
778                     try {
779                       final Future<Pair<RequestFuture<PreloadedProcessMessageHandler>, OSProcessHandler>> preloadResult =
780                         launchPreloadedBuildProcess(project, projectTaskQueue);
781                       myPreloadedBuilds.put(projectPath, preloadResult);
782                     }
783                     catch (Throwable e) {
784                       LOG.info("Error pre-loading build process for project " + projectPath, e);
785                     }
786                   }
787                 });
788               }
789             }
790           });
791           delegatesToWait = new TaskFuture[]{future, new TaskFutureAdapter<>(buildFuture)};
792         }
793         catch (Throwable e) {
794           handleProcessExecutionFailure(sessionId, e);
795         }
796       }
797       boolean set = _future.setDelegates(delegatesToWait);
798       assert set;
799     });
800
801     return _future;
802   }
803
804   private boolean isProcessPreloadingEnabled(Project project) {
805     // automatically disable process preloading when debugging or testing
806     if (IS_UNIT_TEST_MODE || !Registry.is("compiler.process.preload") || myBuildProcessDebuggingEnabled) {
807       return false;
808     }
809     if (project.isDisposed()) {
810       return true;
811     }
812     for (BuildProcessParametersProvider provider : project.getExtensions(BuildProcessParametersProvider.EP_NAME)) {
813       if (!provider.isProcessPreloadingEnabled()) {
814         return false;
815       }
816     }
817     return true;
818   }
819
820   private void notifySessionTerminationIfNeeded(UUID sessionId, @Nullable Throwable execFailure) {
821     if (myMessageDispatcher.getAssociatedChannel(sessionId) == null) {
822       // either the connection has never been established (process not started or execution failed), or no messages were sent from the launched process.
823       // in this case the session cannot be unregistered by the message dispatcher
824       final BuilderMessageHandler unregistered = myMessageDispatcher.unregisterBuildMessageHandler(sessionId);
825       if (unregistered != null) {
826         if (execFailure != null) {
827           unregistered.handleFailure(sessionId, CmdlineProtoUtil.createFailure(execFailure.getMessage(), execFailure));
828         }
829         unregistered.sessionTerminated(sessionId);
830       }
831     }
832   }
833
834   private void handleProcessExecutionFailure(UUID sessionId, Throwable e) {
835     final BuilderMessageHandler unregistered = myMessageDispatcher.unregisterBuildMessageHandler(sessionId);
836     if (unregistered != null) {
837       unregistered.handleFailure(sessionId, CmdlineProtoUtil.createFailure(e.getMessage(), e));
838       unregistered.sessionTerminated(sessionId);
839     }
840   }
841
842   @NotNull
843   private ProjectData getProjectData(String projectPath) {
844     synchronized (myProjectDataMap) {
845       ProjectData data = myProjectDataMap.get(projectPath);
846       if (data == null) {
847         data = new ProjectData(SequentialTaskExecutor.createSequentialApplicationPoolExecutor());
848         myProjectDataMap.put(projectPath, data);
849       }
850       return data;
851     }
852   }
853
854   private void ensureListening() throws Exception {
855     if (myListenPort < 0) {
856       synchronized (this) {
857         if (myListenPort < 0) {
858           myListenPort = startListening();
859         }
860       }
861     }
862   }
863
864   @Override
865   public void dispose() {
866     stopListening();
867   }
868
869   @NotNull
870   public static Pair<Sdk, JavaSdkVersion> getBuildProcessRuntimeSdk(Project project) {
871     final Set<Sdk> candidates = new LinkedHashSet<Sdk>();
872     final Sdk defaultSdk = ProjectRootManager.getInstance(project).getProjectSdk();
873     if (defaultSdk != null && defaultSdk.getSdkType() instanceof JavaSdkType) {
874       candidates.add(defaultSdk);
875     }
876
877     for (Module module : ModuleManager.getInstance(project).getModules()) {
878       final Sdk sdk = ModuleRootManager.getInstance(module).getSdk();
879       if (sdk != null && sdk.getSdkType() instanceof JavaSdkType) {
880         candidates.add(sdk);
881       }
882     }
883
884     // now select the latest version from the sdks that are used in the project, but not older than the internal sdk version
885     final JavaSdk javaSdkType = JavaSdk.getInstance();
886     Sdk projectJdk = null;
887     int sdkMinorVersion = 0;
888     JavaSdkVersion sdkVersion = null;
889     for (Sdk candidate : candidates) {
890       final String vs = candidate.getVersionString();
891       if (vs != null) {
892         final JavaSdkVersion candidateVersion = getSdkVersion(javaSdkType, vs);
893         if (candidateVersion != null) {
894           final int candidateMinorVersion = getMinorVersion(vs);
895           if (projectJdk == null) {
896             sdkVersion = candidateVersion;
897             sdkMinorVersion = candidateMinorVersion;
898             projectJdk = candidate;
899           }
900           else {
901             final int result = candidateVersion.compareTo(sdkVersion);
902             if (result > 0 || result == 0 && candidateMinorVersion > sdkMinorVersion) {
903               sdkVersion = candidateVersion;
904               sdkMinorVersion = candidateMinorVersion;
905               projectJdk = candidate;
906             }
907           }
908         }
909       }
910     }
911
912     final JavaSdkVersion oldestPossible = getOldestPossiblePlatformForBuildProcess(project);
913
914     if (projectJdk == null || sdkVersion == null || !sdkVersion.isAtLeast(oldestPossible)) {
915       final Sdk internalJdk = JavaAwareProjectJdkTableImpl.getInstanceEx().getInternalJdk();
916       projectJdk = internalJdk;
917       sdkVersion = javaSdkType.getVersion(internalJdk);
918     }
919     return Pair.create(projectJdk, sdkVersion);
920   }
921
922   @Nullable
923   private static JavaSdkVersion getSdkVersion(final JavaSdk javaSdkType, final String vs) {
924     JavaSdkVersion version = javaSdkType.getVersion(vs);
925     if (version == null) {
926       // Unexpected version string: e.g. early access or experimental JDK build
927       // trying to find the 'known' sdk version that would best describe the passed version string  
928       final int parsed = JpsJavaSdkType.parseVersion(vs);
929       if (parsed > 0) {
930         version = javaSdkType.getVersion(parsed + ".0");
931       }
932     }
933     return version;
934   }
935
936   @NotNull
937   private static JavaSdkVersion getOldestPossiblePlatformForBuildProcess(Project project) {
938     final BackendCompiler javaCompiler = ((CompilerConfigurationImpl)CompilerConfiguration.getInstance(project)).getDefaultCompiler();
939     final String id = javaCompiler != null? javaCompiler.getId() : JavaCompilers.JAVAC_ID;
940     if (id == JavaCompilers.ECLIPSE_ID || id == JavaCompilers.ECLIPSE_EMBEDDED_ID) {
941       return JavaSdkVersion.JDK_1_7; // because bundled ecj is compiled against 1.7
942     }
943     return JavaSdkVersion.JDK_1_6;
944   }
945
946   private Future<Pair<RequestFuture<PreloadedProcessMessageHandler>, OSProcessHandler>> launchPreloadedBuildProcess(final Project project, ExecutorService projectTaskQueue) throws Exception {
947     ensureListening();
948
949     // launching build process from projectTaskQueue ensures that no other build process for this project is currently running
950     return projectTaskQueue.submit(() -> {
951       if (project.isDisposed()) {
952         return null;
953       }
954       final RequestFuture<PreloadedProcessMessageHandler> future = new RequestFuture<PreloadedProcessMessageHandler>(new PreloadedProcessMessageHandler(), UUID.randomUUID(), new CancelBuildSessionAction<PreloadedProcessMessageHandler>());
955       try {
956         myMessageDispatcher.registerBuildMessageHandler(future, null);
957         final OSProcessHandler processHandler = launchBuildProcess(project, myListenPort, future.getRequestID(), true);
958         final StringBuffer errors = new StringBuffer();
959         processHandler.addProcessListener(new StdOutputCollector(errors));
960         STDERR_OUTPUT.set(processHandler, errors);
961
962         processHandler.startNotify();
963         return Pair.create(future, processHandler);
964       }
965       catch (Throwable e) {
966         handleProcessExecutionFailure(future.getRequestID(), e);
967         throw e instanceof Exception? (Exception)e : new RuntimeException(e);
968       }
969     });
970   }
971
972   private OSProcessHandler launchBuildProcess(Project project, final int port, final UUID sessionId, boolean requestProjectPreload) throws ExecutionException {
973     final String compilerPath;
974     final String vmExecutablePath;
975     JavaSdkVersion sdkVersion = null;
976
977     final String forcedCompiledJdkHome = Registry.stringValue(COMPILER_PROCESS_JDK_PROPERTY);
978
979     if (StringUtil.isEmptyOrSpaces(forcedCompiledJdkHome)) {
980       // choosing sdk with which the build process should be run
981       final Pair<Sdk, JavaSdkVersion> pair = getBuildProcessRuntimeSdk(project);
982       final Sdk projectJdk = pair.first;
983       sdkVersion = pair.second;
984
985       final Sdk internalJdk = JavaAwareProjectJdkTableImpl.getInstanceEx().getInternalJdk();
986       // validate tools.jar presence
987       final JavaSdkType projectJdkType = (JavaSdkType)projectJdk.getSdkType();
988       if (FileUtil.pathsEqual(projectJdk.getHomePath(), internalJdk.getHomePath())) {
989         // important: because internal JDK can be either JDK or JRE,
990         // this is the most universal way to obtain tools.jar path in this particular case
991         final JavaCompiler systemCompiler = ToolProvider.getSystemJavaCompiler();
992         if (systemCompiler == null) {
993           throw new ExecutionException("No system java compiler is provided by the JRE. Make sure tools.jar is present in IntelliJ IDEA classpath.");
994         }
995         compilerPath = ClasspathBootstrap.getResourcePath(systemCompiler.getClass());
996       }
997       else {
998         compilerPath = projectJdkType.getToolsPath(projectJdk);
999         if (compilerPath == null) {
1000           throw new ExecutionException("Cannot determine path to 'tools.jar' library for " + projectJdk.getName() + " (" + projectJdk.getHomePath() + ")");
1001         }
1002       }
1003
1004       vmExecutablePath = projectJdkType.getVMExecutablePath(projectJdk);
1005     }
1006     else {
1007       compilerPath = new File(forcedCompiledJdkHome, "lib/tools.jar").getAbsolutePath();
1008       vmExecutablePath = new File(forcedCompiledJdkHome, "bin/java").getAbsolutePath();
1009     }
1010
1011     final CompilerConfiguration projectConfig = CompilerConfiguration.getInstance(project);
1012     final CompilerWorkspaceConfiguration config = CompilerWorkspaceConfiguration.getInstance(project);
1013     final GeneralCommandLine cmdLine = new GeneralCommandLine();
1014     cmdLine.setExePath(vmExecutablePath);
1015     //cmdLine.addParameter("-XX:MaxPermSize=150m");
1016     //cmdLine.addParameter("-XX:ReservedCodeCacheSize=64m");
1017
1018     boolean isProfilingMode = false;
1019     String userDefinedHeapSize = null;
1020     final List<String> userAdditionalOptionsList = new SmartList<String>();
1021     final String userAdditionalVMOptions = config.COMPILER_PROCESS_ADDITIONAL_VM_OPTIONS;
1022     final boolean userLocalOptionsActive = !StringUtil.isEmptyOrSpaces(userAdditionalVMOptions);
1023     final String additionalOptions = userLocalOptionsActive ? userAdditionalVMOptions : projectConfig.getBuildProcessVMOptions();
1024     if (!StringUtil.isEmptyOrSpaces(additionalOptions)) {
1025       final StringTokenizer tokenizer = new StringTokenizer(additionalOptions, " ", false);
1026       while (tokenizer.hasMoreTokens()) {
1027         final String option = tokenizer.nextToken();
1028         if (StringUtil.startsWithIgnoreCase(option, "-Xmx")) {
1029           if (userLocalOptionsActive) {
1030             userDefinedHeapSize = option;
1031           }
1032         }
1033         else {
1034           if ("-Dprofiling.mode=true".equals(option)) {
1035             isProfilingMode = true;
1036           }
1037           userAdditionalOptionsList.add(option);
1038         }
1039       }
1040     }
1041
1042     if (userDefinedHeapSize != null) {
1043       cmdLine.addParameter(userDefinedHeapSize);
1044     }
1045     else {
1046       final int heapSize = projectConfig.getBuildProcessHeapSize(JavacConfiguration.getOptions(project, JavacConfiguration.class).MAXIMUM_HEAP_SIZE);
1047       cmdLine.addParameter("-Xmx" + heapSize + "m");
1048     }
1049
1050     if (SystemInfo.isMac && sdkVersion != null && JavaSdkVersion.JDK_1_6.equals(sdkVersion) && Registry.is("compiler.process.32bit.vm.on.mac")) {
1051       // unfortunately -d32 is supported on jdk 1.6 only
1052       cmdLine.addParameter("-d32");
1053     }
1054
1055     cmdLine.addParameter("-Djava.awt.headless=true");
1056     if (sdkVersion != null && sdkVersion.ordinal() < JavaSdkVersion.JDK_1_9.ordinal()) {
1057       //-Djava.endorsed.dirs is not supported in JDK 9+, may result in abnormal process termination
1058       cmdLine.addParameter("-Djava.endorsed.dirs=\"\""); // turn off all jre customizations for predictable behaviour
1059     }
1060     if (IS_UNIT_TEST_MODE) {
1061       cmdLine.addParameter("-Dtest.mode=true");
1062     }
1063     cmdLine.addParameter("-Djdt.compiler.useSingleThread=true"); // always run eclipse compiler in single-threaded mode
1064
1065     if (requestProjectPreload) {
1066       cmdLine.addParameter("-Dpreload.project.path=" + FileUtil.toCanonicalPath(getProjectPath(project)));
1067       cmdLine.addParameter("-Dpreload.config.path=" + FileUtil.toCanonicalPath(PathManager.getOptionsPath()));
1068     }
1069
1070     final String shouldGenerateIndex = System.getProperty(GlobalOptions.GENERATE_CLASSPATH_INDEX_OPTION);
1071     if (shouldGenerateIndex != null) {
1072       cmdLine.addParameter("-D"+ GlobalOptions.GENERATE_CLASSPATH_INDEX_OPTION +"=" + shouldGenerateIndex);
1073     }
1074     cmdLine.addParameter("-D"+ GlobalOptions.COMPILE_PARALLEL_OPTION +"=" + Boolean.toString(config.PARALLEL_COMPILATION));
1075     cmdLine.addParameter("-D"+ GlobalOptions.REBUILD_ON_DEPENDENCY_CHANGE_OPTION + "=" + Boolean.toString(config.REBUILD_ON_DEPENDENCY_CHANGE));
1076
1077     if (Boolean.TRUE.equals(Boolean.valueOf(System.getProperty("java.net.preferIPv4Stack", "false")))) {
1078       cmdLine.addParameter("-Djava.net.preferIPv4Stack=true");
1079     }
1080
1081     // this will make netty initialization faster on some systems
1082     cmdLine.addParameter("-Dio.netty.initialSeedUniquifier=" + ThreadLocalRandom.getInitialSeedUniquifier());
1083
1084     for (String option : userAdditionalOptionsList) {
1085       cmdLine.addParameter(option);
1086     }
1087     if (isProfilingMode) {
1088       cmdLine.addParameter("-agentlib:yjpagent=disablealloc,delay=10000,sessionname=ExternalBuild");
1089     }
1090
1091     // debugging
1092     int debugPort = -1;
1093     if (myBuildProcessDebuggingEnabled) {
1094       debugPort = Registry.intValue("compiler.process.debug.port");
1095       if (debugPort <= 0) {
1096         try {
1097           debugPort = NetUtils.findAvailableSocketPort();
1098         }
1099         catch (IOException e) {
1100           throw new ExecutionException("Cannot find free port to debug build process", e);
1101         }
1102       }
1103       if (debugPort > 0) {
1104         cmdLine.addParameter("-XX:+HeapDumpOnOutOfMemoryError");
1105         cmdLine.addParameter("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=" + debugPort);
1106       }
1107     }
1108
1109     if (!Registry.is("compiler.process.use.memory.temp.cache")) {
1110       cmdLine.addParameter("-D"+ GlobalOptions.USE_MEMORY_TEMP_CACHE_OPTION + "=false");
1111     }
1112
1113     // javac's VM should use the same default locale that IDEA uses in order for javac to print messages in 'correct' language
1114     cmdLine.setCharset(mySystemCharset);
1115     cmdLine.addParameter("-D" + CharsetToolkit.FILE_ENCODING_PROPERTY + "=" + mySystemCharset.name());
1116     cmdLine.addParameter("-D" + JpsGlobalLoader.FILE_TYPES_COMPONENT_NAME_KEY + "=" + FileTypeManagerImpl.getFileTypeComponentName());
1117     String[] propertiesToPass = {"user.language", "user.country", "user.region", PathManager.PROPERTY_PATHS_SELECTOR, "idea.case.sensitive.fs"};
1118     for (String name : propertiesToPass) {
1119       final String value = System.getProperty(name);
1120       if (value != null) {
1121         cmdLine.addParameter("-D" + name + "=" + value);
1122       }
1123     }
1124     cmdLine.addParameter("-D" + PathManager.PROPERTY_HOME_PATH + "=" + PathManager.getHomePath());
1125     cmdLine.addParameter("-D" + PathManager.PROPERTY_CONFIG_PATH + "=" + PathManager.getConfigPath());
1126     cmdLine.addParameter("-D" + PathManager.PROPERTY_PLUGINS_PATH + "=" + PathManager.getPluginsPath());
1127
1128     cmdLine.addParameter("-D" + GlobalOptions.LOG_DIR_OPTION + "=" + FileUtil.toSystemIndependentName(getBuildLogDirectory().getAbsolutePath()));
1129     cmdLine.addParameter("-D" + GlobalOptions.FALLBACK_JDK_HOME + "=" + FileUtil.toSystemIndependentName(SystemProperties.getJavaHome()));
1130     cmdLine.addParameter("-D" + GlobalOptions.FALLBACK_JDK_VERSION + "=" + SystemProperties.getJavaVersion());
1131
1132     final File workDirectory = getBuildSystemDirectory();
1133     //noinspection ResultOfMethodCallIgnored
1134     workDirectory.mkdirs();
1135     cmdLine.addParameter("-Djava.io.tmpdir=" + FileUtil.toSystemIndependentName(workDirectory.getPath()) + "/" + TEMP_DIR_NAME);
1136
1137     for (BuildProcessParametersProvider provider : project.getExtensions(BuildProcessParametersProvider.EP_NAME)) {
1138       final List<String> args = provider.getVMArguments();
1139       cmdLine.addParameters(args);
1140     }
1141
1142     @SuppressWarnings("UnnecessaryFullyQualifiedName")
1143     final Class<?> launcherClass = org.jetbrains.jps.cmdline.Launcher.class;
1144
1145     final List<String> launcherCp = new ArrayList<String>();
1146     launcherCp.add(ClasspathBootstrap.getResourcePath(launcherClass));
1147     launcherCp.add(compilerPath);
1148     ClasspathBootstrap.appendJavaCompilerClasspath(launcherCp);
1149     launcherCp.addAll(BuildProcessClasspathManager.getLauncherClasspath(project));
1150     cmdLine.addParameter("-classpath");
1151     cmdLine.addParameter(classpathToString(launcherCp));
1152
1153     cmdLine.addParameter(launcherClass.getName());
1154
1155     final List<String> cp = ClasspathBootstrap.getBuildProcessApplicationClasspath(true);
1156     cp.addAll(myClasspathManager.getBuildProcessPluginsClasspath(project));
1157     if (isProfilingMode) {
1158       cp.add(new File(workDirectory, "yjp-controller-api-redist.jar").getPath());
1159     }
1160     cmdLine.addParameter(classpathToString(cp));
1161
1162     cmdLine.addParameter(BuildMain.class.getName());
1163     cmdLine.addParameter(Boolean.valueOf(System.getProperty("java.net.preferIPv6Addresses", "false"))? "::1" : "127.0.0.1");
1164     cmdLine.addParameter(Integer.toString(port));
1165     cmdLine.addParameter(sessionId.toString());
1166
1167     cmdLine.addParameter(FileUtil.toSystemIndependentName(workDirectory.getPath()));
1168
1169     cmdLine.setWorkDirectory(workDirectory);
1170
1171     try {
1172       ApplicationManager.getApplication().getMessageBus().syncPublisher(BuildManagerListener.TOPIC).beforeBuildProcessStarted(project, sessionId);
1173     }
1174     catch (Throwable e) {
1175       LOG.error(e);
1176     }
1177
1178     final OSProcessHandler processHandler = new OSProcessHandler(cmdLine) {
1179       @Override
1180       protected boolean shouldDestroyProcessRecursively() {
1181         return true;
1182       }
1183
1184       @NotNull
1185       @Override
1186       protected BaseOutputReader.Options readerOptions() {
1187         return BaseOutputReader.Options.BLOCKING;
1188       }
1189     };
1190     processHandler.addProcessListener(new ProcessAdapter() {
1191       @Override
1192       public void onTextAvailable(ProcessEvent event, Key outputType) {
1193         // re-translate builder's output to idea.log
1194         final String text = event.getText();
1195         if (!StringUtil.isEmptyOrSpaces(text)) {
1196           LOG.info("BUILDER_PROCESS [" + outputType.toString() + "]: " + text.trim());
1197         }
1198       }
1199     });
1200     if (debugPort > 0) {
1201       processHandler.putUserData(COMPILER_PROCESS_DEBUG_PORT, debugPort);
1202     }
1203
1204     return processHandler;
1205   }
1206
1207   public File getBuildSystemDirectory() {
1208     return new File(mySystemDirectory, SYSTEM_ROOT);
1209   }
1210
1211   public File getBuildLogDirectory() {
1212     return new File(PathManager.getLogPath(), "build-log");
1213   }
1214
1215   @Nullable
1216   public File getProjectSystemDirectory(Project project) {
1217     final String projectPath = getProjectPath(project);
1218     return projectPath != null? Utils.getDataStorageRoot(getBuildSystemDirectory(), projectPath) : null;
1219   }
1220
1221   private static File getUsageFile(@NotNull File projectSystemDir) {
1222     return new File(projectSystemDir, "ustamp");
1223   }
1224
1225   private static void updateUsageFile(@Nullable Project project, @NotNull File projectSystemDir) {
1226     final File usageFile = getUsageFile(projectSystemDir);
1227     StringBuilder content = new StringBuilder();
1228     try {
1229       synchronized (USAGE_STAMP_DATE_FORMAT) {
1230         content.append(USAGE_STAMP_DATE_FORMAT.format(System.currentTimeMillis()));
1231       }
1232       if (project != null && !project.isDisposed()) {
1233         final String projectFilePath = project.getProjectFilePath();
1234         if (!StringUtil.isEmptyOrSpaces(projectFilePath)) {
1235           content.append("\n").append(FileUtil.toCanonicalPath(projectFilePath));
1236         }
1237       }
1238       FileUtil.writeToFile(usageFile, content.toString());
1239     }
1240     catch (Throwable e) {
1241       LOG.info(e);
1242     }
1243   }
1244
1245   @Nullable
1246   private static Pair<Date, File> readUsageFile(File usageFile) {
1247     try {
1248       final List<String> lines = FileUtil.loadLines(usageFile, CharsetToolkit.UTF8_CHARSET.name());
1249       if (!lines.isEmpty()) {
1250         final String dateString = lines.get(0);
1251         final Date date;
1252         synchronized (USAGE_STAMP_DATE_FORMAT) {
1253           date = USAGE_STAMP_DATE_FORMAT.parse(dateString);
1254         }
1255         final File projectFile = lines.size() > 1? new File(lines.get(1)) : null;
1256         return Pair.create(date, projectFile);
1257       }
1258     }
1259     catch (Throwable e) {
1260       LOG.info(e);
1261     }
1262     return null;
1263   }
1264
1265   private static int getMinorVersion(String vs) {
1266     final int dashIndex = vs.lastIndexOf('_');
1267     if (dashIndex >= 0) {
1268       StringBuilder builder = new StringBuilder();
1269       for (int idx = dashIndex + 1; idx < vs.length(); idx++) {
1270         final char ch = vs.charAt(idx);
1271         if (Character.isDigit(ch)) {
1272           builder.append(ch);
1273         }
1274         else {
1275           break;
1276         }
1277       }
1278       if (builder.length() > 0) {
1279         try {
1280           return Integer.parseInt(builder.toString());
1281         }
1282         catch (NumberFormatException ignored) {
1283         }
1284       }
1285     }
1286     return 0;
1287   }
1288
1289   @NotNull
1290   private Future<?> stopListening() {
1291     myListenPort = -1;
1292     return myChannelRegistrar.close(true);
1293   }
1294
1295   private int startListening() throws Exception {
1296     final ServerBootstrap bootstrap = NettyKt.serverBootstrap(new NioEventLoopGroup(1, ConcurrencyUtil.newNamedThreadFactory("External compiler")));
1297     bootstrap.childHandler(new ChannelInitializer() {
1298       @Override
1299       protected void initChannel(@NotNull Channel channel) throws Exception {
1300         channel.pipeline().addLast(myChannelRegistrar,
1301                                    new ProtobufVarint32FrameDecoder(),
1302                                    new ProtobufDecoder(CmdlineRemoteProto.Message.getDefaultInstance()),
1303                                    new ProtobufVarint32LengthFieldPrepender(),
1304                                    new ProtobufEncoder(),
1305                                    myMessageDispatcher);
1306       }
1307     });
1308     Channel serverChannel = bootstrap.bind(InetAddress.getLoopbackAddress(), 0).syncUninterruptibly().channel();
1309     myChannelRegistrar.add(serverChannel);
1310     return ((InetSocketAddress)serverChannel.localAddress()).getPort();
1311   }
1312
1313   private static String classpathToString(List<String> cp) {
1314     StringBuilder builder = new StringBuilder();
1315     for (String file : cp) {
1316       if (builder.length() > 0) {
1317         builder.append(File.pathSeparator);
1318       }
1319       builder.append(FileUtil.toCanonicalPath(file));
1320     }
1321     return builder.toString();
1322   }
1323
1324   public boolean isBuildProcessDebuggingEnabled() {
1325     return myBuildProcessDebuggingEnabled;
1326   }
1327
1328   public void setBuildProcessDebuggingEnabled(boolean buildProcessDebuggingEnabled) {
1329     myBuildProcessDebuggingEnabled = buildProcessDebuggingEnabled;
1330     if (myBuildProcessDebuggingEnabled) {
1331       cancelAllPreloadedBuilds();
1332     }
1333   }
1334
1335   private abstract static class BuildManagerPeriodicTask implements Runnable {
1336     private final Alarm myAlarm = new Alarm(Alarm.ThreadToUse.SHARED_THREAD);
1337     private final AtomicBoolean myInProgress = new AtomicBoolean(false);
1338     private final Runnable myTaskRunnable = () -> {
1339       try {
1340         runTask();
1341       }
1342       finally {
1343         myInProgress.set(false);
1344       }
1345     };
1346
1347     public final void schedule() {
1348       myAlarm.cancelAllRequests();
1349       final int delay = Math.max(100, getDelay());
1350       myAlarm.addRequest(this, delay);
1351     }
1352
1353     protected abstract int getDelay();
1354
1355     protected abstract void runTask();
1356
1357     @Override
1358     public final void run() {
1359       if (!HeavyProcessLatch.INSTANCE.isRunning() && !myInProgress.getAndSet(true)) {
1360         try {
1361           ApplicationManager.getApplication().executeOnPooledThread(myTaskRunnable);
1362         }
1363         catch (RejectedExecutionException ignored) {
1364           // we were shut down
1365           myInProgress.set(false);
1366         }
1367         catch (Throwable e) {
1368           myInProgress.set(false);
1369           throw new RuntimeException(e);
1370         }
1371       }
1372       else {
1373         schedule();
1374       }
1375     }
1376   }
1377
1378   private static class NotifyingMessageHandler extends DelegatingMessageHandler {
1379     private final Project myProject;
1380     private final BuilderMessageHandler myDelegateHandler;
1381     private final boolean myIsAutomake;
1382
1383     public NotifyingMessageHandler(@NotNull Project project, @NotNull BuilderMessageHandler delegateHandler, final boolean isAutomake) {
1384       myProject = project;
1385       myDelegateHandler = delegateHandler;
1386       myIsAutomake = isAutomake;
1387     }
1388
1389     @Override
1390     protected BuilderMessageHandler getDelegateHandler() {
1391       return myDelegateHandler;
1392     }
1393
1394     @Override
1395     public void buildStarted(UUID sessionId) {
1396       super.buildStarted(sessionId);
1397       try {
1398         ApplicationManager
1399           .getApplication().getMessageBus().syncPublisher(BuildManagerListener.TOPIC).buildStarted(myProject, sessionId, myIsAutomake);
1400       }
1401       catch (Throwable e) {
1402         LOG.error(e);
1403       }
1404     }
1405
1406     @Override
1407     public void sessionTerminated(UUID sessionId) {
1408       try {
1409         super.sessionTerminated(sessionId);
1410       }
1411       finally {
1412         try {
1413           ApplicationManager.getApplication().getMessageBus().syncPublisher(BuildManagerListener.TOPIC).buildFinished(myProject, sessionId, myIsAutomake);
1414         }
1415         catch (Throwable e) {
1416           LOG.error(e);
1417         }
1418       }
1419     }
1420   }
1421
1422   private static final class StdOutputCollector extends ProcessAdapter {
1423     private final Appendable myOutput;
1424     private int myStoredLength;
1425     StdOutputCollector(@NotNull Appendable outputSink) {
1426       myOutput = outputSink;
1427     }
1428
1429     @Override
1430     public void onTextAvailable(ProcessEvent event, Key outputType) {
1431       String text;
1432
1433       synchronized (this) {
1434         if (myStoredLength > 16384) {
1435           return;
1436         }
1437         text = event.getText();
1438         if (StringUtil.isEmptyOrSpaces(text)) {
1439           return;
1440         }
1441         myStoredLength += text.length();
1442       }
1443
1444       try {
1445         myOutput.append(text);
1446       }
1447       catch (IOException ignored) {
1448       }
1449     }
1450   }
1451
1452   private class ProjectWatcher extends ProjectManagerAdapter {
1453     private final Map<Project, MessageBusConnection> myConnections = new HashMap<Project, MessageBusConnection>();
1454
1455     @Override
1456     public void projectOpened(final Project project) {
1457       if (ApplicationManager.getApplication().isUnitTestMode()) return;
1458       final MessageBusConnection conn = project.getMessageBus().connect();
1459       myConnections.put(project, conn);
1460       conn.subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootAdapter() {
1461         @Override
1462         public void rootsChanged(final ModuleRootEvent event) {
1463           final Object source = event.getSource();
1464           if (source instanceof Project) {
1465             clearState((Project)source);
1466           }
1467         }
1468       });
1469       conn.subscribe(ExecutionManager.EXECUTION_TOPIC, new ExecutionAdapter() {
1470         @Override
1471         public void processTerminated(@NotNull RunProfile runProfile, @NotNull ProcessHandler handler) {
1472           scheduleAutoMake();
1473         }
1474       });
1475       conn.subscribe(CompilerTopics.COMPILATION_STATUS, new CompilationStatusListener() {
1476         private final Set<String> myRootsToRefresh = new THashSet<String>(FileUtil.PATH_HASHING_STRATEGY);
1477
1478         @Override
1479         public void automakeCompilationFinished(int errors, int warnings, CompileContext compileContext) {
1480           if (!compileContext.getProgressIndicator().isCanceled()) {
1481             refreshSources(compileContext);
1482           }
1483         }
1484
1485         @Override
1486         public void compilationFinished(boolean aborted, int errors, int warnings, CompileContext compileContext) {
1487           refreshSources(compileContext);
1488         }
1489
1490         private void refreshSources(CompileContext compileContext) {
1491           if (project.isDisposed()) {
1492             return;
1493           }
1494           final Set<String> candidates = new THashSet<String>(FileUtil.PATH_HASHING_STRATEGY);
1495           synchronized (myRootsToRefresh) {
1496             candidates.addAll(myRootsToRefresh);
1497             myRootsToRefresh.clear();
1498           }
1499           if (compileContext.isAnnotationProcessorsEnabled()) {
1500             // annotation processors may have re-generated code
1501             final CompilerConfiguration config = CompilerConfiguration.getInstance(project);
1502             for (Module module : compileContext.getCompileScope().getAffectedModules()) {
1503               if (config.getAnnotationProcessingConfiguration(module).isEnabled()) {
1504                 final String path = CompilerPaths.getAnnotationProcessorsGenerationPath(module);
1505                 if (path != null) {
1506                   candidates.add(path);
1507                 }
1508               }
1509             }
1510           }
1511
1512           if (!candidates.isEmpty()) {
1513             ApplicationManager.getApplication().executeOnPooledThread(() -> {
1514               if (project.isDisposed()) {
1515                 return;
1516               }
1517               final List<File> rootFiles = new ArrayList<File>(candidates.size());
1518               for (String root : candidates) {
1519                 rootFiles.add(new File(root));
1520               }
1521               // this will ensure that we'll be able to obtain VirtualFile for existing roots
1522               CompilerUtil.refreshOutputDirectories(rootFiles, false);
1523
1524               final LocalFileSystem lfs = LocalFileSystem.getInstance();
1525               final Set<VirtualFile> filesToRefresh = new HashSet<VirtualFile>();
1526               ApplicationManager.getApplication().runReadAction(() -> {
1527                 if (project.isDisposed()) {
1528                   return;
1529                 }
1530                 final ProjectFileIndex fileIndex = ProjectRootManager.getInstance(project).getFileIndex();
1531                 for (File root : rootFiles) {
1532                   final VirtualFile rootFile = lfs.findFileByIoFile(root);
1533                   if (rootFile != null && fileIndex.isInSourceContent(rootFile)) {
1534                     filesToRefresh.add(rootFile);
1535                   }
1536                 }
1537                 if (!filesToRefresh.isEmpty()) {
1538                   lfs.refreshFiles(filesToRefresh, true, true, null);
1539                 }
1540               });
1541             });
1542           }
1543         }
1544
1545         @Override
1546         public void fileGenerated(String outputRoot, String relativePath) {
1547           synchronized (myRootsToRefresh) {
1548             myRootsToRefresh.add(outputRoot);
1549           }
1550         }
1551       });
1552       final String projectPath = getProjectPath(project);
1553       Disposer.register(project, new Disposable() {
1554         @Override
1555         public void dispose() {
1556           cancelPreloadedBuilds(projectPath);
1557           myProjectDataMap.remove(projectPath);
1558         }
1559       });
1560       StartupManager.getInstance(project).registerPostStartupActivity(() -> {
1561         runCommand(() -> {
1562           final File projectSystemDir = getProjectSystemDirectory(project);
1563           if (projectSystemDir != null) {
1564             updateUsageFile(project, projectSystemDir);
1565           }
1566         });
1567         scheduleAutoMake(); // run automake after project opened
1568       });
1569     }
1570
1571     @Override
1572     public boolean canCloseProject(Project project) {
1573       cancelAutoMakeTasks(project);
1574       return super.canCloseProject(project);
1575     }
1576
1577     @Override
1578     public void projectClosing(Project project) {
1579       cancelPreloadedBuilds(getProjectPath(project));
1580       for (TaskFuture future : cancelAutoMakeTasks(project)) {
1581         future.waitFor(500, TimeUnit.MILLISECONDS);
1582       }
1583     }
1584
1585     @Override
1586     public void projectClosed(Project project) {
1587       myProjectDataMap.remove(getProjectPath(project));
1588       final MessageBusConnection conn = myConnections.remove(project);
1589       if (conn != null) {
1590         conn.disconnect();
1591       }
1592     }
1593   }
1594
1595   private static class ProjectData {
1596     @NotNull
1597     final ExecutorService taskQueue;
1598     private final Set<InternedPath> myChanged = new THashSet<InternedPath>();
1599     private final Set<InternedPath> myDeleted = new THashSet<InternedPath>();
1600     private long myNextEventOrdinal;
1601     private boolean myNeedRescan = true;
1602
1603     private ProjectData(@NotNull ExecutorService taskQueue) {
1604       this.taskQueue = taskQueue;
1605     }
1606
1607     public void addChanged(Collection<String> paths) {
1608       if (!myNeedRescan) {
1609         for (String path : paths) {
1610           final InternedPath _path = InternedPath.create(path);
1611           myDeleted.remove(_path);
1612           myChanged.add(_path);
1613         }
1614       }
1615     }
1616
1617     public void addDeleted(Collection<String> paths) {
1618       if (!myNeedRescan) {
1619         for (String path : paths) {
1620           final InternedPath _path = InternedPath.create(path);
1621           myChanged.remove(_path);
1622           myDeleted.add(_path);
1623         }
1624       }
1625     }
1626
1627     public CmdlineRemoteProto.Message.ControllerMessage.FSEvent createNextEvent() {
1628       final CmdlineRemoteProto.Message.ControllerMessage.FSEvent.Builder builder =
1629         CmdlineRemoteProto.Message.ControllerMessage.FSEvent.newBuilder();
1630       builder.setOrdinal(++myNextEventOrdinal);
1631
1632       for (InternedPath path : myChanged) {
1633         builder.addChangedPaths(path.getValue());
1634       }
1635       myChanged.clear();
1636
1637       for (InternedPath path : myDeleted) {
1638         builder.addDeletedPaths(path.getValue());
1639       }
1640       myDeleted.clear();
1641
1642       return builder.build();
1643     }
1644
1645     public boolean getAndResetRescanFlag() {
1646       final boolean rescan = myNeedRescan;
1647       myNeedRescan = false;
1648       return rescan;
1649     }
1650
1651     public void dropChanges() {
1652       myNeedRescan = true;
1653       myNextEventOrdinal = 0L;
1654       myChanged.clear();
1655       myDeleted.clear();
1656     }
1657   }
1658
1659   private abstract static class InternedPath {
1660     protected final int[] myPath;
1661
1662     /**
1663      * @param path assuming system-independent path with forward slashes
1664      */
1665     protected InternedPath(String path) {
1666       final IntArrayList list = new IntArrayList();
1667       final StringTokenizer tokenizer = new StringTokenizer(path, "/", false);
1668       while(tokenizer.hasMoreTokens()) {
1669         final String element = tokenizer.nextToken();
1670         list.add(FileNameCache.storeName(element));
1671       }
1672       myPath = list.toArray();
1673     }
1674
1675     public abstract String getValue();
1676
1677     @Override
1678     public boolean equals(Object o) {
1679       if (this == o) return true;
1680       if (o == null || getClass() != o.getClass()) return false;
1681
1682       InternedPath path = (InternedPath)o;
1683
1684       if (!Arrays.equals(myPath, path.myPath)) return false;
1685
1686       return true;
1687     }
1688
1689     @Override
1690     public int hashCode() {
1691       return Arrays.hashCode(myPath);
1692     }
1693
1694     public static InternedPath create(String path) {
1695       return path.startsWith("/")? new XInternedPath(path) : new WinInternedPath(path);
1696     }
1697   }
1698
1699   private static class WinInternedPath extends InternedPath {
1700     private WinInternedPath(String path) {
1701       super(path);
1702     }
1703
1704     @Override
1705     public String getValue() {
1706       if (myPath.length == 1) {
1707         final String name = FileNameCache.getVFileName(myPath[0]).toString();
1708         // handle case of windows drive letter
1709         return name.length() == 2 && name.endsWith(":")? name + "/" : name;
1710       }
1711
1712       final StringBuilder buf = new StringBuilder();
1713       for (int element : myPath) {
1714         if (buf.length() > 0) {
1715           buf.append("/");
1716         }
1717         buf.append(FileNameCache.getVFileName(element));
1718       }
1719       return buf.toString();
1720     }
1721   }
1722
1723   private static class XInternedPath extends InternedPath {
1724     private XInternedPath(String path) {
1725       super(path);
1726     }
1727
1728     @Override
1729     public String getValue() {
1730       if (myPath.length > 0) {
1731         final StringBuilder buf = new StringBuilder();
1732         for (int element : myPath) {
1733           buf.append("/").append(FileNameCache.getVFileName(element));
1734         }
1735         return buf.toString();
1736       }
1737       return "/";
1738     }
1739   }
1740
1741   private static final class DelegateFuture implements TaskFuture {
1742     @Nullable
1743     private TaskFuture[] myDelegates;
1744     private Boolean myRequestedCancelState;
1745
1746     @NotNull
1747     private synchronized TaskFuture[] getDelegates() {
1748       TaskFuture[] delegates = myDelegates;
1749       while (delegates == null) {
1750         try {
1751           wait();
1752         }
1753         catch (InterruptedException ignored) {
1754         }
1755         delegates = myDelegates;
1756       }
1757       return delegates;
1758     }
1759
1760     private synchronized boolean setDelegates(@NotNull TaskFuture... delegates) {
1761       if (myDelegates == null) {
1762         try {
1763           myDelegates = delegates;
1764           if (myRequestedCancelState != null) {
1765             for (TaskFuture delegate : delegates) {
1766               delegate.cancel(myRequestedCancelState);
1767             }
1768           }
1769         }
1770         finally {
1771           notifyAll();
1772         }
1773         return true;
1774       }
1775       return false;
1776     }
1777
1778     @Override
1779     public synchronized boolean cancel(boolean mayInterruptIfRunning) {
1780       Future[] delegates = myDelegates;
1781       if (delegates == null) {
1782         myRequestedCancelState = mayInterruptIfRunning;
1783         return true;
1784       }
1785       Stream.of(delegates).forEach(delegate -> delegate.cancel(mayInterruptIfRunning));
1786       return isDone();
1787     }
1788
1789     @Override
1790     public void waitFor() {
1791       Stream.of(getDelegates()).forEach(TaskFuture::waitFor);
1792     }
1793
1794     @Override
1795     public boolean waitFor(long timeout, TimeUnit unit) {
1796       Stream.of(getDelegates()).forEach(delegate -> delegate.waitFor(timeout, unit));
1797       return isDone();
1798     }
1799
1800     @Override
1801     public boolean isCancelled() {
1802       final Future[] delegates;
1803       synchronized (this) {
1804         delegates = myDelegates;
1805         if (delegates == null) {
1806           return myRequestedCancelState != null;
1807         }
1808       }
1809       return Stream.of(delegates).anyMatch(Future::isCancelled);
1810     }
1811
1812     @Override
1813     public boolean isDone() {
1814       final Future[] delegates;
1815       synchronized (this) {
1816         delegates = myDelegates;
1817         if (delegates == null) {
1818           return false;
1819         }
1820       }
1821       return Stream.of(delegates).allMatch(Future::isDone);
1822     }
1823
1824     @Override
1825     public Object get() throws InterruptedException, java.util.concurrent.ExecutionException {
1826       for (Future delegate : getDelegates()) {
1827         delegate.get();
1828       }
1829       return null;
1830     }
1831
1832     @Override
1833     public Object get(long timeout, @NotNull TimeUnit unit) throws InterruptedException, java.util.concurrent.ExecutionException, TimeoutException {
1834       for (Future delegate : getDelegates()) {
1835         delegate.get(timeout, unit);
1836       }
1837       return null;
1838     }
1839   }
1840
1841   private class CancelBuildSessionAction<T extends BuilderMessageHandler> implements RequestFuture.CancelAction<T> {
1842     @Override
1843     public void cancel(RequestFuture<T> future) throws Exception {
1844       myMessageDispatcher.cancelSession(future.getRequestID());
1845     }
1846   }
1847 }