-terminate automake session when project is disposed
[idea/community.git] / jps / jps-builders / src / org / jetbrains / jps / server / ServerMessageHandler.java
1 package org.jetbrains.jps.server;
2
3 import com.intellij.openapi.diagnostic.Logger;
4 import com.intellij.openapi.util.Pair;
5 import com.intellij.openapi.util.Ref;
6 import org.jboss.netty.channel.*;
7 import org.jetbrains.annotations.NotNull;
8 import org.jetbrains.annotations.Nullable;
9 import org.jetbrains.jps.api.*;
10 import org.jetbrains.jps.incremental.MessageHandler;
11 import org.jetbrains.jps.incremental.messages.*;
12
13 import java.io.ByteArrayOutputStream;
14 import java.io.File;
15 import java.io.PrintStream;
16 import java.util.*;
17 import java.util.concurrent.ExecutionException;
18 import java.util.concurrent.ExecutorService;
19 import java.util.concurrent.RunnableFuture;
20
21 /**
22  * @author Eugene Zhuravlev
23  *         Date: 8/11/11
24  */
25 class ServerMessageHandler extends SimpleChannelHandler {
26   private static final Logger LOG = Logger.getInstance("#org.jetbrains.jps.server.ServerMessageHandler");
27
28   private final Map<String, SequentialTaskExecutor> myTaskExecutors = new HashMap<String, SequentialTaskExecutor>();
29   private final List<Pair<RunnableFuture, CompilationTask>> myBuildsInProgress = Collections.synchronizedList(new LinkedList<Pair<RunnableFuture, CompilationTask>>());
30   private final ExecutorService myBuildsExecutor;
31   private final Server myServer;
32
33   public ServerMessageHandler(ExecutorService buildsExecutor, Server server) {
34     myBuildsExecutor = buildsExecutor;
35     myServer = server;
36   }
37
38   public void messageReceived(final ChannelHandlerContext ctx, MessageEvent e) throws Exception {
39     final JpsRemoteProto.Message message = (JpsRemoteProto.Message)e.getMessage();
40     final UUID sessionId = ProtoUtil.fromProtoUUID(message.getSessionId());
41
42     JpsRemoteProto.Message reply = null;
43
44     if (message.getMessageType() != JpsRemoteProto.Message.Type.REQUEST) {
45       reply = ProtoUtil.toMessage(sessionId, ProtoUtil.createFailure("Cannot handle message " + message.toString()));
46     }
47     else if (!message.hasRequest()) {
48       reply = ProtoUtil.toMessage(sessionId, ProtoUtil.createFailure("No request in message: " + message.toString()));
49     }
50     else {
51       final JpsRemoteProto.Message.Request request = message.getRequest();
52       final JpsRemoteProto.Message.Request.Type requestType = request.getRequestType();
53       final ServerState facade = ServerState.getInstance();
54       switch (requestType) {
55         case COMPILE_REQUEST :
56           reply = startBuild(sessionId, ctx, request.getCompileRequest());
57           break;
58         case RELOAD_PROJECT_COMMAND:
59           final JpsRemoteProto.Message.Request.ReloadProjectCommand reloadProjectCommand = request.getReloadProjectCommand();
60           facade.clearProjectCache(reloadProjectCommand.getProjectIdList());
61           reply = ProtoUtil.toMessage(sessionId, ProtoUtil.createCommandCompletedEvent(null));
62           break;
63         case CANCEL_BUILD_COMMAND:
64           final JpsRemoteProto.Message.Request.CancelBuildCommand cancelCommand = request.getCancelBuildCommand();
65           final UUID targetSessionId = ProtoUtil.fromProtoUUID(cancelCommand.getTargetSessionId());
66           cancelSession(targetSessionId);
67           reply = ProtoUtil.toMessage(sessionId, ProtoUtil.createCommandCompletedEvent(null));
68           break;
69         case SETUP_COMMAND:
70           final Map<String, String> pathVars = new HashMap<String, String>();
71           final JpsRemoteProto.Message.Request.SetupCommand setupCommand = request.getSetupCommand();
72           for (JpsRemoteProto.Message.Request.SetupCommand.PathVariable variable : setupCommand.getPathVariableList()) {
73             pathVars.put(variable.getName(), variable.getValue());
74           }
75           final List<GlobalLibrary> libs = new ArrayList<GlobalLibrary>();
76           for (JpsRemoteProto.Message.Request.SetupCommand.GlobalLibrary library : setupCommand.getGlobalLibraryList()) {
77             libs.add(
78               library.hasHomePath()?
79               new SdkLibrary(library.getName(), library.getHomePath(), library.getPathList()) :
80               new GlobalLibrary(library.getName(), library.getPathList())
81             );
82           }
83           final String globalEncoding = setupCommand.isInitialized()? setupCommand.getGlobalEncoding() : null;
84           facade.setGlobals(libs, pathVars, globalEncoding);
85           reply = ProtoUtil.toMessage(sessionId, ProtoUtil.createCommandCompletedEvent(null));
86           break;
87
88         case SHUTDOWN_COMMAND :
89           // todo pay attention to policy
90           myBuildsExecutor.submit(new Runnable() {
91             public void run() {
92               final List<RunnableFuture> futures = new ArrayList<RunnableFuture>();
93
94               synchronized (myBuildsInProgress) {
95                 for (Iterator<Pair<RunnableFuture, CompilationTask>> it = myBuildsInProgress.iterator(); it.hasNext(); ) {
96                   final Pair<RunnableFuture, CompilationTask> pair = it.next();
97                   it.remove();
98                   pair.second.cancel();
99                   final RunnableFuture future = pair.first;
100                   futures.add(future);
101                   future.cancel(true);
102                 }
103               }
104
105               facade.clearCahedState();
106
107               // wait until really stopped
108               for (RunnableFuture future : futures) {
109                 try {
110                   future.get();
111                 }
112                 catch (InterruptedException ignored) {
113                 }
114                 catch (ExecutionException ignored) {
115                 }
116               }
117
118               myServer.stop();
119             }
120           });
121           break;
122         case FS_EVENT:
123           final JpsRemoteProto.Message.Request.FSEvent fsEvent = request.getFsEvent();
124           final String projectId = fsEvent.getProjectId();
125           final ProjectDescriptor pd = facade.getProjectDescriptor(projectId);
126           if (pd != null) {
127             try {
128               for (String path : fsEvent.getChangedPathsList()) {
129                 facade.notifyFileChanged(pd, new File(path));
130               }
131               for (String path : fsEvent.getDeletedPathsList()) {
132                 facade.notifyFileDeleted(pd, new File(path));
133               }
134             }
135             finally {
136               pd.release();
137             }
138           }
139           reply = ProtoUtil.toMessage(sessionId, ProtoUtil.createCommandCompletedEvent(null));
140           break;
141         default:
142           reply = ProtoUtil.toMessage(sessionId, ProtoUtil.createFailure("Unknown request: " + message));
143       }
144     }
145     if (reply != null) {
146       Channels.write(ctx.getChannel(), reply);
147     }
148   }
149
150   private void cancelSession(UUID targetSessionId) {
151     synchronized (myBuildsInProgress) {
152       for (Iterator<Pair<RunnableFuture, CompilationTask>> it = myBuildsInProgress.iterator(); it.hasNext(); ) {
153         final Pair<RunnableFuture, CompilationTask> pair = it.next();
154         final CompilationTask task = pair.second;
155         if (task.getSessionId().equals(targetSessionId)) {
156           it.remove();
157           task.cancel();
158           pair.first.cancel(true);
159           break;
160         }
161       }
162     }
163   }
164
165   @Override
166   public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
167     final Object attachment = ctx.getAttachment();
168     if (attachment instanceof UUID) {
169       cancelSession((UUID)attachment);
170     }
171     super.channelDisconnected(ctx, e);
172   }
173
174   @Nullable
175   private JpsRemoteProto.Message startBuild(UUID sessionId, final ChannelHandlerContext channelContext, JpsRemoteProto.Message.Request.CompilationRequest compileRequest) {
176     if (!compileRequest.hasProjectId()) {
177       return ProtoUtil.toMessage(sessionId, ProtoUtil.createFailure("No project specified"));
178     }
179
180     final String projectId = compileRequest.getProjectId();
181     final JpsRemoteProto.Message.Request.CompilationRequest.Type compileType = compileRequest.getCommandType();
182
183     switch (compileType) {
184       // todo
185       case MAKE:
186       case FORCED_COMPILATION:
187       case REBUILD: {
188         channelContext.setAttachment(sessionId);
189         final BuildType buildType = convertCompileType(compileType);
190         final CompilationTask task = new CompilationTask(
191           sessionId, channelContext, projectId, buildType, compileRequest.getModuleNameList(), compileRequest.getFilePathList()
192         );
193         final RunnableFuture future = getCompileTaskExecutor(projectId).submit(task);
194         myBuildsInProgress.add(new Pair<RunnableFuture, CompilationTask>(future, task));
195         return null;
196       }
197
198       default:
199         return ProtoUtil.toMessage(sessionId, ProtoUtil.createFailure("Unsupported command: '" + compileType + "'"));
200     }
201   }
202
203   @NotNull
204   private SequentialTaskExecutor getCompileTaskExecutor(String projectId) {
205     synchronized (myTaskExecutors) {
206       SequentialTaskExecutor executor = myTaskExecutors.get(projectId);
207       if (executor == null) {
208         executor = new SequentialTaskExecutor(new SequentialTaskExecutor.AsyncTaskExecutor() {
209           @Override
210           public void submit(Runnable runnable) {
211             myBuildsExecutor.submit(runnable);
212           }
213         });
214         myTaskExecutors.put(projectId, executor);
215       }
216       return executor;
217     }
218   }
219
220   public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
221     if (this == ctx.getPipeline().getLast()) {
222       LOG.error(e);
223     }
224     ctx.sendUpstream(e);
225   }
226
227   private class CompilationTask implements Runnable, CanceledStatus {
228
229     private final UUID mySessionId;
230     private final ChannelHandlerContext myChannelContext;
231     private final String myProjectPath;
232     private final BuildType myBuildType;
233     private final Collection<String> myPaths;
234     private final Set<String> myModules;
235     private volatile boolean myCanceled = false;
236
237     public CompilationTask(UUID sessionId,
238                            ChannelHandlerContext channelContext,
239                            String projectId,
240                            BuildType buildType,
241                            Collection<String> modules,
242                            Collection<String> paths) {
243       mySessionId = sessionId;
244       myChannelContext = channelContext;
245       myProjectPath = projectId;
246       myBuildType = buildType;
247       myPaths = paths;
248       myModules = new HashSet<String>(modules);
249     }
250
251     public UUID getSessionId() {
252       return mySessionId;
253     }
254
255     public boolean isCanceled() {
256       return myCanceled;
257     }
258
259     public void run() {
260       Channels.write(myChannelContext.getChannel(), ProtoUtil.toMessage(mySessionId, ProtoUtil.createBuildStartedEvent("build started")));
261       Throwable error = null;
262       final Ref<Boolean> hasErrors = new Ref<Boolean>(false);
263       final Ref<Boolean> markedFilesUptodate = new Ref<Boolean>(false);
264       try {
265         ServerState.getInstance().startBuild(myProjectPath, myBuildType, myModules, myPaths, new MessageHandler() {
266           public void processMessage(BuildMessage buildMessage) {
267             final JpsRemoteProto.Message.Response response;
268             if (buildMessage instanceof FileGeneratedEvent) {
269               final Collection<Pair<String, String>> paths = ((FileGeneratedEvent)buildMessage).getPaths();
270               response = !paths.isEmpty()? ProtoUtil.createFileGeneratedEvent(paths) : null;
271             }
272             else if (buildMessage instanceof UptoDateFilesSavedEvent) {
273               markedFilesUptodate.set(true);
274               response = null;
275             }
276             else if (buildMessage instanceof CompilerMessage) {
277               markedFilesUptodate.set(true);
278               final CompilerMessage compilerMessage = (CompilerMessage)buildMessage;
279               final String text = compilerMessage.getCompilerName() + ": " + compilerMessage.getMessageText();
280               final BuildMessage.Kind kind = compilerMessage.getKind();
281               if (kind == BuildMessage.Kind.ERROR) {
282                 hasErrors.set(true);
283               }
284               response = ProtoUtil.createCompileMessageResponse(
285                 kind, text, compilerMessage.getSourcePath(),
286                 compilerMessage.getProblemBeginOffset(), compilerMessage.getProblemEndOffset(),
287                 compilerMessage.getProblemLocationOffset(), compilerMessage.getLine(), compilerMessage.getColumn(),
288                 -1.0f);
289             }
290             else {
291               float done = -1.0f;
292               if (buildMessage instanceof ProgressMessage) {
293                 done = ((ProgressMessage)buildMessage).getDone();
294               }
295               response = ProtoUtil.createCompileProgressMessageResponse(buildMessage.getMessageText(), done);
296             }
297             if (response != null) {
298               Channels.write(myChannelContext.getChannel(), ProtoUtil.toMessage(mySessionId, response));
299             }
300           }
301         }, this);
302       }
303       catch (Throwable e) {
304         error = e;
305       }
306       finally {
307         finishBuild(error, hasErrors.get(), markedFilesUptodate.get());
308       }
309     }
310
311     private void finishBuild(@Nullable Throwable error, boolean hadBuildErrors, boolean markedUptodateFiles) {
312       JpsRemoteProto.Message lastMessage = null;
313       try {
314         if (error != null) {
315           Throwable cause = error.getCause();
316           if (cause == null) {
317             cause = error;
318           }
319           final ByteArrayOutputStream out = new ByteArrayOutputStream();
320           cause.printStackTrace(new PrintStream(out));
321
322           final StringBuilder messageText = new StringBuilder();
323           messageText.append("JPS Internal error: (").append(cause.getClass().getName()).append(") ").append(cause.getMessage());
324           final String trace = out.toString();
325           if (!trace.isEmpty()) {
326             messageText.append("\n").append(trace);
327           }
328           lastMessage = ProtoUtil.toMessage(mySessionId, ProtoUtil.createFailure(messageText.toString(), cause));
329         }
330         else {
331           JpsRemoteProto.Message.Response.BuildEvent.Status status = JpsRemoteProto.Message.Response.BuildEvent.Status.SUCCESS;
332           if (myCanceled) {
333             status = JpsRemoteProto.Message.Response.BuildEvent.Status.CANCELED;
334           }
335           else if (hadBuildErrors) {
336             status = JpsRemoteProto.Message.Response.BuildEvent.Status.ERRORS;
337           }
338           else if (!markedUptodateFiles){
339             status = JpsRemoteProto.Message.Response.BuildEvent.Status.UP_TO_DATE;
340           }
341           lastMessage = ProtoUtil.toMessage(mySessionId, ProtoUtil.createBuildCompletedEvent("build completed", status));
342         }
343       }
344       catch (Throwable e) {
345         lastMessage = ProtoUtil.toMessage(mySessionId, ProtoUtil.createFailure(e.getMessage(), e));
346       }
347       finally {
348         Channels.write(myChannelContext.getChannel(), lastMessage).addListener(new ChannelFutureListener() {
349           public void operationComplete(ChannelFuture future) throws Exception {
350             final UUID sessionId = getSessionId();
351             synchronized (myBuildsInProgress) {
352               for (Iterator<Pair<RunnableFuture, CompilationTask>> it = myBuildsInProgress.iterator(); it.hasNext(); ) {
353                 final CompilationTask task = it.next().second;
354                 if (sessionId.equals(task.getSessionId())) {
355                   it.remove();
356                   break;
357                 }
358               }
359             }
360           }
361         });
362       }
363     }
364
365     public void cancel() {
366       myCanceled = true;
367     }
368   }
369
370   private static BuildType convertCompileType(JpsRemoteProto.Message.Request.CompilationRequest.Type compileType) {
371     switch (compileType) {
372       case CLEAN: return BuildType.CLEAN;
373       case MAKE: return BuildType.MAKE;
374       case REBUILD: return BuildType.PROJECT_REBUILD;
375       case FORCED_COMPILATION: return BuildType.FORCED_COMPILATION;
376     }
377     return BuildType.MAKE; // use make by default
378   }
379 }