// Copyright © 2010, Esko Luontola <www.orfjackal.net> // This software is released under the Apache License 2.0. // The license text is at http://www.apache.org/licenses/LICENSE-2.0 package net.orfjackal.sbt.plugin; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.components.AbstractProjectComponent; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.progress.*; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.DumbAwareRunnable; import com.intellij.openapi.project.Project; import com.intellij.openapi.startup.StartupManager; import com.intellij.openapi.ui.MessageType; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.io.StreamUtil; import com.intellij.openapi.vfs.*; import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowAnchor; import com.intellij.openapi.wm.ToolWindowManager; import com.intellij.util.concurrency.SwingWorker; import net.orfjackal.sbt.plugin.settings.*; import net.orfjackal.sbt.runner.*; import javax.swing.*; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Scanner; public class SbtRunnerComponent extends AbstractProjectComponent implements DumbAware { private static final Logger logger = Logger.getInstance(SbtRunnerComponent.class.getName()); private static final boolean DEBUG = false; private static final String SBT_CONSOLE_TOOL_WINDOW_ID = "SBT Console"; private SbtRunner sbt; private SbtConsole console; private Project project; private final SbtProjectSettingsComponent projectSettings; private final SbtApplicationSettingsComponent applicationSettings; public static SbtRunnerComponent getInstance(Project project) { return project.getComponent(SbtRunnerComponent.class); } protected SbtRunnerComponent(Project project, SbtProjectSettingsComponent projectSettings, SbtApplicationSettingsComponent applicationSettings) { super(project); this.project = project; this.projectSettings = projectSettings; this.applicationSettings = applicationSettings; } public CompletionSignal executeInBackground(final String action) { final CompletionSignal signal = new CompletionSignal(); signal.begin(); queue(new Task.Backgroundable(myProject, MessageBundle.message("sbt.tasks.executing"), false) { public void run(ProgressIndicator indicator) { try { logger.debug("Begin executing: " + action); if (executeAndWait(action)) { signal.success(); logger.debug("Done executing: " + action); } else { logger.debug("Error executing: " + action); } } catch (IOException e) { logger.error("Failed to execute action \"" + action + "\". Maybe SBT failed to start?", e); } finally { signal.finished(); } } }); return signal; } @Override public void projectOpened() { final StartupManager manager = StartupManager.getInstance(myProject); manager.registerPostStartupActivity(new DumbAwareRunnable() { public void run() { console = createConsole(project); registerToolWindow(); } }); } @Override public void disposeComponent() { unregisterToolWindow(); destroyProcess(); } private SbtConsole createConsole(Project project) { return new SbtConsole(MessageBundle.message("sbt.tasks.action"), project, this); } private void registerToolWindow() { ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(myProject); if (toolWindowManager != null) { ToolWindow toolWindow = toolWindowManager.registerToolWindow(SBT_CONSOLE_TOOL_WINDOW_ID, false, ToolWindowAnchor.BOTTOM, myProject, true); SbtRunnerComponent sbtRunnerComponent = SbtRunnerComponent.getInstance(myProject); sbtRunnerComponent.getConsole().ensureAttachedToToolWindow(toolWindow, false); } } private void unregisterToolWindow() { ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(myProject); if (toolWindowManager != null && toolWindowManager.getToolWindow(SBT_CONSOLE_TOOL_WINDOW_ID) != null) { toolWindowManager.unregisterToolWindow(SBT_CONSOLE_TOOL_WINDOW_ID); } } private void queue(final Task.Backgroundable task) { if (ApplicationManager.getApplication().isDispatchThread()) { task.queue(); } else { ApplicationManager.getApplication().invokeAndWait(new Runnable() { public void run() { task.queue(); } }, ModalityState.NON_MODAL); } } /** * @param action the SBT action to run * @return false if an error was detected, true otherwise * @throws IOException */ public boolean executeAndWait(String action) throws IOException { saveAllDocuments(); if (!startIfNotStartedSafe(true)) { return false; } boolean success; try { success = sbt.execute(action); // TODO: update target folders (?) // org.jetbrains.idea.maven.project.MavenProjectsManager#updateProjectFolders // org.jetbrains.idea.maven.execution.MavenRunner#runBatch // org.jetbrains.idea.maven.execution.MavenRunner#updateTargetFolders } catch (IOException e) { destroyProcess(); throw e; } VirtualFileManager.getInstance().refreshWithoutFileWatcher(true); return success; } private static void saveAllDocuments() { ApplicationManager.getApplication().invokeAndWait(new Runnable() { public void run() { FileDocumentManager.getInstance().saveAllDocuments(); } }, ModalityState.NON_MODAL); } public final SbtConsole getConsole() { return console; } public final String getFormattedCommand() { return sbt.getFormattedCommand(); } public final boolean startIfNotStartedSafe(boolean wait) { try { startIfNotStarted(wait); return true; } catch (Throwable e) { String toolWindowId = MessageBundle.message("sbt.console.id"); ToolWindowManager.getInstance(project).notifyByBalloon(toolWindowId, MessageType.ERROR, "Unable to start SBT. " + e.getMessage()); logger.info("Failed to start SBT", e); return false; } } private void startIfNotStarted(final boolean wait) throws IOException { if (!isSbtAlive()) { sbt = new SbtRunner(projectSettings.getJavaCommand(applicationSettings), projectDir(), launcherJar(), vmParameters()); printToMessageWindow(); if (DEBUG) { printToLogFile(); } sbt.start(wait, new Runnable() { public void run() { try { // See https://github.com/orfjackal/idea-sbt-plugin/issues/49 sbt.execute("eval {System.setProperty(\"jline.terminal\" , \"none\"); \"<modified system property 'jline.terminal' for Scala console compatibility>\"}"); } catch (Exception e) { // ignore } } }); } } public final boolean isSbtAlive() { return sbt != null && sbt.isAlive(); } private File projectDir() { VirtualFile baseDir = myProject.getBaseDir(); assert baseDir != null; return new File(baseDir.getPath()); } private File launcherJar() { String pathname = projectSettings.effectiveSbtLauncherJarPath(applicationSettings); if (pathname != null && pathname.length() != 0) { return new File(pathname); } try { return unpackBundledLauncher(); } catch (Exception e) { // ignore } return new File("no-launcher.jar"); } private File unpackBundledLauncher() throws IOException { String launcherName = "sbt-launch.jar"; File launcherTemp = new File(new File(PathManager.getSystemPath(), "sbt"), launcherName); if (!launcherTemp.exists()) { InputStream resource = SbtRunnerComponent.class.getClassLoader().getResourceAsStream("sbt-launch.jar"); byte[] bytes = StreamUtil.loadFromStream(resource); FileUtil.writeToFile(launcherTemp, bytes); } return launcherTemp; } private String[] vmParameters() { String[] split = projectSettings.effectiveSbtLauncherVmParameters(applicationSettings).split("\\s"); if (split.length == 1 && split[0].trim().equals("")) return new String[0]; else return split; } private void printToMessageWindow() { // org.jetbrains.idea.maven.execution.MavenExecutor#myConsole SbtProcessHandler process = new SbtProcessHandler(this, sbt.subscribeToOutput()); console.attachToProcess(process, this); process.startNotify(); } private void printToLogFile() { final OutputReader output = sbt.subscribeToOutput(); Thread t = new Thread(new Runnable() { public void run() { Scanner scanner = new Scanner(output); while (scanner.hasNextLine()) { logger.info(scanner.nextLine()); } } }); t.setDaemon(true); t.start(); } public void destroyProcess() { if (sbt != null) { sbt.destroy(); sbt = null; } } }