package com.intellij.jps.flex.build; import com.intellij.flex.FlexCommonUtils; import com.intellij.flex.model.sdk.JpsFlexSdkType; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.Function; import gnu.trove.THashMap; import org.jetbrains.jps.incremental.CompileContext; import org.jetbrains.jps.incremental.messages.BuildMessage; import org.jetbrains.jps.incremental.messages.CompilerMessage; import org.jetbrains.jps.model.JpsProject; import org.jetbrains.jps.model.library.sdk.JpsSdk; import org.jetbrains.jps.service.SharedThreadPool; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.List; import java.util.Map; public class JpsBuiltInFlexCompilerHandler { private static final Logger LOG = Logger.getInstance(JpsBuiltInFlexCompilerHandler.class.getName()); private static final String CONNECTION_SUCCESSFUL = "Connection successful"; public static final String COMPILATION_FINISHED = "Compilation finished"; private final JpsProject myProject; private String mySdkHome; private ServerSocket myServerSocket; private DataInputStream myDataInputStream; private DataOutputStream myDataOutputStream; private int commandNumber = 1; private Map<String, Listener> myActiveListeners = new THashMap<>(); public interface Listener { void textAvailable(String text); void compilationFinished(); } JpsBuiltInFlexCompilerHandler(final JpsProject project) { myProject = project; } public synchronized boolean canBeUsedForSdk(final String sdkHome) { return mySdkHome == null || mySdkHome.equals(sdkHome); } public synchronized void startCompilerIfNeeded(final JpsSdk<?> sdk, final CompileContext context, final String compilerName) throws IOException { if (!Comparing.equal(sdk.getHomePath(), mySdkHome)) { stopCompilerProcess(); } if (myServerSocket == null) { try { //context.processMessage(new ProgressMessage("Starting Flex compiler")); myServerSocket = new ServerSocket(0); myServerSocket.setSoTimeout(10000); final int port = myServerSocket.getLocalPort(); startCompilerProcess(sdk, port, context, compilerName); final Socket socket = myServerSocket.accept(); myDataInputStream = new DataInputStream(socket.getInputStream()); myDataOutputStream = new DataOutputStream(socket.getOutputStream()); mySdkHome = sdk.getHomePath(); scheduleInputReading(); } catch (IOException e) { stopCompilerProcess(); throw e; } } } private void startCompilerProcess(final JpsSdk<?> sdk, final int port, final CompileContext context, final String compilerName) throws IOException { final StringBuilder classpath = new StringBuilder(); classpath.append(FlexCommonUtils.getPathToBundledJar("idea-flex-compiler-fix.jar")); classpath.append(File.pathSeparatorChar); classpath.append(FlexCommonUtils.getPathToBundledJar("flex-compiler.jar")); if (sdk.getSdkType() == JpsFlexSdkType.INSTANCE) { classpath.append(File.pathSeparator).append(FileUtil.toSystemDependentName(sdk.getHomePath() + "/lib/flex-compiler-oem.jar")); } final List<String> commandLine = FlexCommonUtils.getCommandLineForSdkTool(myProject, sdk, classpath.toString(), "com.intellij.flex.compiler.FlexCompiler"); commandLine.add(String.valueOf(port)); final ProcessBuilder processBuilder = new ProcessBuilder(commandLine); processBuilder.redirectErrorStream(true); processBuilder.directory(new File(FlexCommonUtils.getFlexCompilerWorkDirPath(myProject))); final String plainCommand = StringUtil.join(processBuilder.command(), s -> s.contains(" ") ? "\"" + s + "\"" : s, " "); context.processMessage(new CompilerMessage(compilerName, BuildMessage.Kind.INFO, "Starting Flex compiler:\n" + plainCommand)); final Process process = processBuilder.start(); readInputStreamUntilConnected(process, context, compilerName); } private void readInputStreamUntilConnected(final Process process, final CompileContext context, final String compilerName) { SharedThreadPool.getInstance().executeOnPooledThread(() -> { final InputStreamReader reader = FlexCommonUtils.createInputStreamReader(process.getInputStream()); try { char[] buf = new char[1024]; int read; while ((read = reader.read(buf, 0, buf.length)) >= 0) { final String output = new String(buf, 0, read); if (output.startsWith(CONNECTION_SUCCESSFUL)) { break; } else { closeSocket(); context.processMessage(new CompilerMessage(compilerName, BuildMessage.Kind.ERROR, output)); } } } catch (IOException e) { closeSocket(); context.processMessage( new CompilerMessage(compilerName, BuildMessage.Kind.ERROR, "Failed to start Flex compiler: " + e.toString())); } finally { try { reader.close(); } catch (IOException e) {/*ignore*/} } }); } private void scheduleInputReading() { SharedThreadPool.getInstance().executeOnPooledThread(() -> { final StringBuilder buffer = new StringBuilder(); while (true) { final DataInputStream dataInputStream = myDataInputStream; if (dataInputStream != null) { try { buffer.append(dataInputStream.readUTF()); int index; while ((index = buffer.indexOf("\n")) > -1) { final String line = buffer.substring(0, index); buffer.delete(0, index + 1); handleInputLine(line); } } catch (IOException e) { if (dataInputStream == myDataInputStream) { stopCompilerProcess(); } break; } } else { break; } } }); } private synchronized void handleInputLine(final String line) { LOG.debug("RECEIVED: [" + line + "]"); final int colonPos = line.indexOf(":"); if (colonPos <= 0) { LOG.error("Incorrect command: [" + line + "]"); return; } final String prefix = line.substring(0, colonPos + 1); final Listener listener = myActiveListeners.get(prefix); if (listener == null) { LOG.warn("No active listener for input line: [" + line + "]"); // could be message from cancelled compilation } else { final String text = line.substring(colonPos + 1); if (text.startsWith(COMPILATION_FINISHED)) { listener.compilationFinished(); myActiveListeners.remove(prefix); } else { listener.textAvailable(text); } } } public synchronized void sendCompilationCommand(final String command, final Listener listener) { if (myDataOutputStream == null) { listener.textAvailable("Error: Compiler process is not started."); listener.compilationFinished(); return; } try { final String prefix = String.valueOf(commandNumber++) + ":"; final String commandToSend = prefix + command + "\n"; LOG.debug("SENDING: [" + commandToSend + "]"); myDataOutputStream.writeUTF(commandToSend); myActiveListeners.put(prefix, listener); } catch (IOException e) { listener.textAvailable("Error: Can't start compilation: " + e.toString()); listener.compilationFinished(); } } private synchronized void cancelAllCompilations(final boolean reportError) { for (final Listener listener : myActiveListeners.values()) { if (reportError) { listener.textAvailable("Error: Compilation terminated"); } listener.compilationFinished(); } myActiveListeners.clear(); } public synchronized void stopCompilerProcess() { cancelAllCompilations(true); closeSocket(); } private synchronized void closeSocket() { // compiler process exits when socket closes, so it's enough just to close streams if (myDataInputStream != null) { try { myDataInputStream.close(); } catch (IOException ignored) {/**/} } if (myDataOutputStream != null) { try { myDataOutputStream.close(); } catch (IOException ignored) {/**/} } if (myServerSocket != null) { try { myServerSocket.close(); } catch (IOException ignored) {/**/} } myServerSocket = null; myDataInputStream = null; myDataOutputStream = null; } public synchronized void removeListener(final Listener listener) { String toRemove = null; for (final Map.Entry<String, Listener> entry : myActiveListeners.entrySet()) { if (entry.getValue() == listener) { toRemove = entry.getKey(); break; } } if (toRemove != null) { myActiveListeners.remove(toRemove); } } public synchronized int getActiveCompilationsNumber() { return myActiveListeners.size(); } }