// Copyright 2006-2012 AdvancedTools. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.advancedtools.cpp.communicator; import com.advancedtools.cpp.CppSupportLoader; import com.advancedtools.cpp.facade.EnvironmentFacade; import com.advancedtools.cpp.commands.StringCommand; import com.intellij.ProjectTopics; import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; import com.intellij.ide.IdeEventQueue; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.components.ProjectComponent; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.PerformInBackgroundOption; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ModuleRootEvent; import com.intellij.openapi.roots.ModuleRootListener; import com.intellij.openapi.startup.StartupManager; import com.intellij.openapi.util.ModificationTracker; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.wm.WindowManager; import com.intellij.util.messages.MessageBusConnection; import org.jetbrains.annotations.NonNls; import javax.swing.*; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.io.PrintWriter; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.LinkedBlockingQueue; /** * @author maxim */ public class Communicator implements ProjectComponent { // In log4.xml add // <category name="cppsupport.communicator"> // <priority value="DEBUG"/> // <appender-ref ref="CONSOLE-ALL"/> // </category> static final Logger LOG = Logger.getInstance("cppsupport.communicator"); public static final boolean isDebugEnabled = LOG.isDebugEnabled(); private static Communicator instance; private Project myProject; private Runnable myIdleListener; private ModuleRootListener myRootListener; private MessageBusConnection myConnection; private long myServerRestartCount; private volatile int myServerInternalRestartCount; @NonNls public static final String DSP_EXTENSION = "dsp"; @NonNls public static final String MAK_EXTENSION = "mak"; @NonNls public static final String DSW_EXTENSION = "dsw"; @NonNls public static final String VCPROJ_EXTENSION = "vcproj"; @NonNls public static final String SLN_EXTENSION = "sln"; @NonNls public static final String MAKEFILE_FILE_NAME = "makefile"; @NonNls static final String PATH_DELIM = ";"; private ModificationTracker myServerRestartTracker = new ModificationTracker() { public long getModificationCount() { return myServerRestartCount; } }; private ModificationTracker myModificationTracker = new ModificationTracker() { public long getModificationCount() { return modificationCount; } }; private long modificationCount; @NonNls private static final String QUIT_COMMAND_NAME = "quit"; @NonNls static final String IDLE_COMMAND_NAME = "idle"; private boolean myListenerAdded; public static final char DELIMITER = '|'; public static final String DELIMITER_STRING = "|"; private static final int CAPACITY = 25; static enum ServerState { FAILED_OR_NOT_STARTED, STARTING, STARTED } private volatile ServerState serverState; public Communicator(Project project) { myProject = project; if (ApplicationManager.getApplication().isUnitTestMode()) instance = this; } public synchronized long incModificationCount() { return ++modificationCount; } public synchronized long getModificationCount() { return modificationCount; } void cancelCommand(CommunicatorCommand command) { if (command != null && command.isCancellable()) { sendCommand(new CancelCommand()); if (isDebugEnabled) { LOG.debug("cancelled:" + command.getCommand()); } } } public static void debug(String s) { if (isDebugEnabled) LOG.debug(s); } public Project getProject() { return myProject; } public static int findDelimiter(String str, int startFrom) { boolean escaped = false; for(int i = startFrom;i < str.length(); ++i) { char ch = str.charAt(i); if (escaped) { escaped = false; continue; } if (ch == '\\') escaped = true; else if (ch == Communicator.DELIMITER) { return i + 1; } } return str.length(); } public void communicatorStarted() { serverState = ServerState.STARTED; } void startErrorStreamReader() { new Thread(new ServerErrorThread(), "Cpp Communicator Error Stream Read Thread").start(); } public boolean isServerUpAndRunning() { return serverState == ServerState.STARTED; } static class RestartCommand extends CommunicatorCommand { private final String unexpected; public RestartCommand(String unexpected) { this.unexpected = unexpected; } public void doExecute() { } public void commandOutputString(String str) { } public String getCommand() { return null; } } public void projectOpened() { if (!myProject.isDefault()) { final Runnable initRunnable = new Runnable() { public void run() { new CommunicatorThread(); myIdleListener = new Runnable() { long lastUpdate = -1; public void run() { final long currentModificationCount = getModificationCount(); if (lastUpdate != currentModificationCount) { lastUpdate = currentModificationCount; sendCommand(new StringCommand(IDLE_COMMAND_NAME + " -n "+lastUpdate) { public void doExecuteOnCancel() { super.doExecuteOnCancel(); lastUpdate = -1; } }); } } }; SwingUtilities.invokeLater(new Runnable() { public void run() { IdeEventQueue.getInstance().addIdleListener( myIdleListener, 2000 ); myListenerAdded = true; myRootListener = new ModuleRootListener() { public void beforeRootsChange(ModuleRootEvent moduleRootEvent) { } public void rootsChanged(ModuleRootEvent moduleRootEvent) { if (EnvironmentFacade.getInstance().isRealRootChangedEvent(moduleRootEvent)) { restartServer(); } } }; myConnection = myProject.getMessageBus().connect(); myConnection.subscribe(ProjectTopics.PROJECT_ROOTS, myRootListener); } }); sendCommand(new StringCommand("gc")); } }; if (ApplicationManager.getApplication().isUnitTestMode()) { initRunnable.run(); try { Thread.sleep(500); // we need to give some time for starting thread and get write lock } catch (InterruptedException e) {} } else { StartupManager.getInstance(myProject).registerStartupActivity(initRunnable); } } } static class QuitCommand extends StringCommand { public QuitCommand() { super(QUIT_COMMAND_NAME); } } public void sendCommand(final CommunicatorCommand command) { if (command instanceof QuitCommand) commandsToRestart.offer(command); command.setRestartTimestamp(myServerInternalRestartCount); if (meaningfulCommand(command)) ++nonCancellableCommandsCount; int startingFactor = serverState == ServerState.STARTING ? 2 : 1; if (nonCancellableCommandsCount < startingFactor * CAPACITY || !command.isCancellable()) { commandsToWrite.offer(command); } else { // do restart the server because command length is big commandsToRestart.offer(new RestartCommand("Write command limit exceeded")); command.doExecuteOnCancel(); } } public void projectClosed() { if (!myProject.isDefault()) { if (myListenerAdded) IdeEventQueue.getInstance().removeIdleListener(myIdleListener); myConnection.disconnect(); myRootListener = null; myConnection = null; sendCommand(new QuitCommand()); SwingUtilities.invokeLater(new Runnable() { public void run() { myExecutableState.doDestroyProcess(); } }); } } @NonNls public String getComponentName() { return "CppSupport.Communicator"; } public void initComponent() {} public void disposeComponent() {} public void restartServer() { myServerRestartCount++; incModificationCount(); sendCommand(new StringCommand(QUIT_COMMAND_NAME)); serverState = ServerState.FAILED_OR_NOT_STARTED; commandsToRestart.add(new RestartCommand(null) { public void doExecute() { SwingUtilities.invokeLater(new Runnable() { public void run() { if (!myProject.isDisposed()) DaemonCodeAnalyzer.getInstance(myProject).restart(); } }); } }); } public void onFileCreated(VirtualFile file) { StringBuilder builder = new StringBuilder(); if (isHeaderFile(file)) { addHeaderFileCommand(buildContainingDirPath(file), builder); } else { addSourceFileCommand(file, builder); } if (builder.length() > 0) { sendCommand(new StringCommand(builder.toString())); } incModificationCount(); } public ModificationTracker getServerRestartTracker() { return myServerRestartTracker; } public ModificationTracker getModificationTracker() { return myModificationTracker; } private static String buildContainingDirPath(VirtualFile fileOrDir) { return fileOrDir.getParent().getPath().replace('/', File.separatorChar); } public static boolean isCFile(VirtualFile file) { final String extension = file.getExtension(); return "c".equals(extension); } public static boolean isHeaderFile(VirtualFile file) { final String extension = file.getExtension(); return CppSupportLoader.H_EXTENSION.equals(extension) || CppSupportLoader.INC_EXTENSION.equals(extension) || CppSupportLoader.isKnownEmptyExtensionFile(file.getName()) || ( CppSupportLoader.HPP_EXTENSION.equals(extension) || CppSupportLoader.INL_EXTENSION.equals(extension) || CppSupportLoader.HXX_EXTENSION.equals(extension) || CppSupportLoader.HI_EXTENSION.equals(extension) || CppSupportLoader.TCC_EXTENSION.equals(extension) ); } private static void addHeaderFileCommand(String path, StringBuilder builder) { builder.append("user-include-path ").append(BuildingCommandHelper.quote(path)).append("\n"); } public static void addSourceFileCommand(VirtualFile fileOrDir, StringBuilder builder) { String path = fileOrDir.getPath().replace('/', File.separatorChar); builder.append("module ").append(BuildingCommandHelper.quote(path)).append(" " + configName(fileOrDir)).append("\n"); } private static String configName(VirtualFile fileOrDir) { return BuildingCommandHelper.configNameByMode(isCFile(fileOrDir)); } public void onFileRemoved(VirtualFile file) { String path = file.getPath().replace('/', File.separatorChar); StringBuilder builder = new StringBuilder(); if (!isHeaderFile(file)) { builder.append("module-remove ").append(BuildingCommandHelper.quote(path)).append(" " + configName(file)); } if (builder.length() > 0) { sendCommand(new StringCommand(builder.toString())); } incModificationCount(); } public static Communicator getInstance(Project project) { if (instance != null) return instance; return project.getComponent(Communicator.class); } private final BlockingQueue<CommunicatorCommand> commandsToRestart = new ArrayBlockingQueue<CommunicatorCommand>(CAPACITY); private final BlockingQueue<CommunicatorCommand> commandsToRead = new LinkedBlockingQueue<CommunicatorCommand>(); private final BlockingQueue<CommunicatorCommand> commandsToWrite = new LinkedBlockingQueue<CommunicatorCommand>(); private volatile int nonCancellableCommandsCount; private ServerExecutableState myExecutableState = new ServerExecutableState(); class CommunicatorThread implements Runnable { CommunicatorThread() { new Thread(new ServerAliveThread(), "Cpp Communicator Support Thread").start(); new Thread(null, this, "Cpp Communicator Thread").start(); new Thread(new ServerReadThread(), "Cpp Communicator Read Thread").start(); } public void run() { try { while(true) { if (myProject.isDisposed()) { return; } final CommunicatorCommand pendingCommand = commandsToWrite.poll(Long.MAX_VALUE, TimeUnit.MILLISECONDS); if (meaningfulCommand(pendingCommand)) --nonCancellableCommandsCount; if (pendingCommand.getRestartTimestamp() != myServerInternalRestartCount) { pendingCommand.doExecuteOnCancel(); continue; } if (!myExecutableState.isServerProcessAlive()) { pendingCommand.doExecuteOnCancel(); if (myExecutableState.isServerProcessStarted()) { commandsToRestart.clear(); commandsToRestart.add(new RestartCommand("Unexpected server exit")); } continue; } if (!commandsToRead.offer(pendingCommand)) { // we offer command to read first because next write should not // block during flush due to limited i / o buffer! if (pendingCommand.isCancellable()) { commandsToRestart.add(new RestartCommand("Unexpected read buffer overflow")); pendingCommand.doExecuteOnCancel(); } else { commandsToRead.put(pendingCommand); } } try { String command = pendingCommand.getCommand(); if (isDebugEnabled) LOG.debug("Sending to server: " + command); myExecutableState.writeStringToInputStream(command +"\n"); if (pendingCommand instanceof QuitCommand) return; } catch (IOException e) { pendingCommand.doExecuteOnCancel(); LOG.debug(e); commandsToRestart.add(new RestartCommand(e.getMessage())); } } } catch (InterruptedException e) { return; } } } class ServerErrorThread implements Runnable { public void run() { try { while(true) { if (myProject.isDisposed()) { return; } final String str = myExecutableState.readLineFromErrorStream(); if (str == null) break; LOG.error(str); } } catch (IOException ex) { serverState = ServerState.FAILED_OR_NOT_STARTED; if(filterExceptionDueToServerExit(ex)) return; ex.printStackTrace(); LOG.error(ex); } } } class ServerReadThread implements Runnable { @NonNls private static final String OK_COMMAND_PREFIX = "<OK:"; @NonNls private static final String CANCELLED_COMMAND_PREFIX = "<Cancelled:"; @NonNls private static final String OK_CANCEL_COMMAND_PREFIX = "<OK:cancel"; public void run() { try { Out: while(true) { if (myProject.isDisposed()) { return; } CommunicatorCommand communicatorCommand = commandsToRead.poll(Long.MAX_VALUE, TimeUnit.MILLISECONDS); if (communicatorCommand instanceof QuitCommand) return; if (communicatorCommand.getRestartTimestamp() != myServerInternalRestartCount) { communicatorCommand.doExecuteOnCancel(); continue; } String commandText = communicatorCommand.getCommand(); int i = commandText.lastIndexOf('\n') + 1; int i2 = commandText.indexOf(' ',i); String commandId = null; if (i2 == -1) i2 = commandText.length(); else { final String str = "-n "; int i3 = commandText.indexOf(str, i2); if (i3 != -1) { i3 += str.length(); int index = commandText.indexOf(' ', i3); commandId = ":" + commandText.substring(i3, index == -1 ? commandText.length():index); } } String lastCommandName = commandText.substring(i, i2); String confirmationText = OK_COMMAND_PREFIX + lastCommandName; if (commandId != null) { confirmationText += commandId; lastCommandName += commandId; } else { confirmationText += ":"; lastCommandName += ":"; } if (isDebugEnabled) LOG.debug("waiting for confirmation string:"+confirmationText); boolean wasOutputToStatus = false; try { final long started = System.currentTimeMillis(); while(true) { String read = myExecutableState.readLineFromOutputStream(); if (isDebugEnabled) LOG.debug("Read from server:" + read); if (read == null) { if (!(communicatorCommand instanceof QuitCommand)) { communicatorCommand.doExecuteOnCancel(); } break; } if (read.indexOf(CANCELLED_COMMAND_PREFIX) != -1) { if (read.indexOf(lastCommandName) != -1) { communicatorCommand.doExecuteOnCancel(); break; } } else if (read.indexOf(confirmationText) != -1) { if (isDebugEnabled) { LOG.debug("Done:" + confirmationText + " " + (System.currentTimeMillis() - started)); } notifySink(communicatorCommand, read); break; } else if (read.startsWith(OK_COMMAND_PREFIX, 0)) { if (read.startsWith(OK_CANCEL_COMMAND_PREFIX)) { break; } if (isDebugEnabled) { LOG.debug("1:" + read + " " + (System.currentTimeMillis() - started)); } communicatorCommand.commandFinishedString( read ); } else { wasOutputToStatus |= doDispatch(communicatorCommand, read); } } } catch(IOException ex) { communicatorCommand.doExecuteOnCancel(); serverState = ServerState.FAILED_OR_NOT_STARTED; if (filterExceptionDueToServerExit(ex)) { continue; } ex.printStackTrace(); LOG.error(ex); continue; } finally { if (wasOutputToStatus) { setMessage(""); } } } } catch (InterruptedException e) { return; } } void notifySink(final CommunicatorCommand myCommand, final String confirmationText) { myCommand.commandFinishedString( confirmationText ); if (myCommand.doInvokeInDispatchThread()) { SwingUtilities.invokeLater(new Runnable() { public void run() { if (!myProject.isDisposed()) myCommand.doExecute(); } }); } else { myCommand.doExecute(); } } private boolean doDispatch(final CommunicatorCommand myCommand, final String str) { if (str == null) return false; // ? final String prefix = "Status:"; final String prefix2 = "MESSAGE:|"; boolean shouldClearMessage; if ((shouldClearMessage = str.startsWith(prefix)) || str.startsWith(prefix2) ) { final String message = str.substring( str.startsWith(prefix) ? prefix.length() : prefix2.length() ); setMessage(message); return shouldClearMessage; } if (str.length() == 0 && !myCommand.acceptsEmptyResult()) return false; try { myCommand.commandOutputString( str ); } catch(Exception e) { e.printStackTrace(); LOG.debug(e); } return false; } private void setMessage(final String message) { SwingUtilities.invokeLater(new Runnable() { public void run() { if (myProject.isDisposed()) return; WindowManager.getInstance().getStatusBar(myProject).setInfo(message); if (message.indexOf("Failure:") != -1) { serverState = ServerState.FAILED_OR_NOT_STARTED; LOG.error(new ServerExecutionException(BuildingCommandHelper.unquote(message))); } } }); } } private static boolean filterExceptionDueToServerExit(IOException ex) { final String localizedMessage = ex.getLocalizedMessage(); return localizedMessage != null && localizedMessage.indexOf("Stream closed") >= 0; } static final long MIN_TIME = 5 * 60 * 1000; class ServerAliveThread implements Runnable { final Runnable startServerAction = new Runnable() { public void run() { myExecutableState.startProcess(Communicator.this); } }; final Runnable restartServerAction = new Runnable() { public void run() { myExecutableState.restartProcess(Communicator.this); } }; public void run() { try { int restartCount = 0; long startedTime = System.currentTimeMillis(); doServerStartup(startServerAction); while(true) { if (myProject.isDisposed()) { return; } final CommunicatorCommand pendingCommand = commandsToRestart.poll(Long.MAX_VALUE, TimeUnit.MILLISECONDS); if (pendingCommand instanceof QuitCommand) return; if (serverState == ServerState.STARTING) { continue; } ++myServerInternalRestartCount; nonCancellableCommandsCount = 0; if (isDebugEnabled) LOG.debug("restarting server"); if (pendingCommand instanceof RestartCommand && ((RestartCommand)pendingCommand).unexpected != null ) { LOG.error(new ServerExecutionException(((RestartCommand)pendingCommand).unexpected)); } final long now = System.currentTimeMillis(); if (now - startedTime < MIN_TIME) ++restartCount; else restartCount = 0; startedTime = now; doServerStartup(restartServerAction); commandsToRestart.clear(); pendingCommand.doExecute(); Thread.sleep(5000 * restartCount); } } catch(InterruptedException ex) {} } private void doServerStartup(final Runnable startServerAction) { if (ApplicationManager.getApplication().isUnitTestMode()) return; serverState = ServerState.STARTING; ApplicationManager.getApplication().invokeAndWait(new Runnable() { public void run() { ProgressManager.getInstance().runProcessWithProgressAsynchronously( myProject, "analyzing c / c++ sources", startServerAction, null, null, new PerformInBackgroundOption() { public boolean shouldStartInBackground() { return true; } public void processSentToBackground() { } } ); } }, ModalityState.NON_MODAL); } } static class ServerExecutionException extends RuntimeException { public ServerExecutionException(String message) { super(message); } public void printStackTrace() { printStackTrace(System.out); } public void printStackTrace(PrintStream s) { s.println(getMessage()); } public void printStackTrace(PrintWriter s) { s.println(getMessage()); } } private class CancelCommand extends StringCommand { public CancelCommand() { super("cancel"); } } private boolean meaningfulCommand(CommunicatorCommand pendingCommand) { return pendingCommand.isCancellable() && !(pendingCommand instanceof CancelCommand); } }