/*
* Copyright 2009 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.plugins.clojure.repl;
import clojure.lang.AFn;
import com.intellij.execution.CantRunException;
import com.intellij.execution.configurations.CommandLineBuilder;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.configurations.JavaParameters;
import com.intellij.execution.process.ProcessAdapter;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.process.ProcessOutputTypes;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.projectRoots.JavaSdkType;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.SdkType;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.ModuleSourceOrderEntry;
import com.intellij.openapi.roots.OrderEntry;
import com.intellij.openapi.roots.OrderRootType;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.encoding.EncodingManager;
import com.intellij.util.Alarm;
import com.intellij.util.PathUtil;
import org.jetbrains.plugins.clojure.config.ClojureConfigUtil;
import org.jetbrains.plugins.clojure.utils.ClojureUtils;
import java.io.*;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.*;
/**
* @author Kurt Christensen, ilyas
* @author <a href="mailto:ianp@ianp.org">Ian Phillips</a>
*/
public class ClojureReplProcessHandler extends ProcessHandler {
private static final Logger LOG = Logger.getInstance(ClojureReplProcessHandler.class.getName());
private static ExecutorService ourThreadExecutorsService = null;
private final Process myProcess;
private final ProcessWaitFor myWaitFor;
private final String myExecPath;
private final Module myModule;
private static final String CLOJURE_SDK = PathUtil.getJarPathForClass(AFn.class);
public static Future<?> executeOnPooledThread(Runnable task) {
final Application application = ApplicationManager.getApplication();
if (application != null) {
return application.executeOnPooledThread(task);
} else {
if (ourThreadExecutorsService == null) {
ourThreadExecutorsService = new ThreadPoolExecutor(
10,
Integer.MAX_VALUE,
60L,
TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactory() {
@SuppressWarnings({"HardCodedStringLiteral"})
public Thread newThread(Runnable r) {
return new Thread(r, "OSProcessHandler pooled thread");
}
}
);
}
return ourThreadExecutorsService.submit(task);
}
}
public ClojureReplProcessHandler(String path, Module module)
throws IOException, ConfigurationException, CantRunException {
myExecPath = path;
myModule = module;
if (notConfigured()) {
throw new ConfigurationException("Can't create Clojure REPL process");
} else {
// Only a single command line per-project is supported. We may need more flexibility
// in the future (e.g., different Clojure paths with different args)
final JavaParameters params = new JavaParameters();
params.configureByModule(myModule, JavaParameters.JDK_AND_CLASSES_AND_TESTS);
// To avoid NCDFE while starting REPL
final boolean sdkConfigured = ClojureConfigUtil.isClojureConfigured(myModule);
if (!sdkConfigured) {
final String jarPath = ClojureConfigUtil.CLOJURE_SDK;
assert jarPath != null;
params.getClassPath().add(jarPath);
}
Set<VirtualFile> cpVFiles = new HashSet<VirtualFile>();
ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(myModule);
OrderEntry[] entries = moduleRootManager.getOrderEntries();
for (OrderEntry orderEntry : entries) {
// Add module sources to classpath
if (orderEntry instanceof ModuleSourceOrderEntry) {
cpVFiles.addAll(Arrays.asList(orderEntry.getFiles(OrderRootType.SOURCES)));
}
}
for (VirtualFile file : cpVFiles) {
params.getClassPath().add(file.getPath());
}
params.setMainClass(ClojureUtils.CLOJURE_MAIN);
params.setWorkingDirectory(path);
final GeneralCommandLine line = CommandLineBuilder.createFromJavaParameters(params, PlatformDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext()), true);
final Sdk sdk = params.getJdk();
assert sdk != null;
final SdkType type = (SdkType) sdk.getSdkType();
final String executablePath = ((JavaSdkType) type).getVMExecutablePath(sdk);
final ArrayList<String> env = new ArrayList<String>();
final ArrayList<String> cmd = new ArrayList<String>();
cmd.add(executablePath);
cmd.addAll(line.getParametersList().getList());
if (!sdkConfigured) {
ClojureConfigUtil.warningDefaultClojureJar(myModule);
}
myProcess = Runtime.getRuntime().exec(cmd.toArray(new String[cmd.size()]),
env.toArray(new String[env.size()]), new File(myExecPath));
myWaitFor = new ProcessWaitFor(myProcess);
}
}
private boolean notConfigured() {
return false;
}
private static class ProcessWaitFor {
private final com.intellij.util.concurrency.Semaphore myWaitSemaphore = new com.intellij.util.concurrency.Semaphore();
private final Future<?> myWaitForThreadFuture;
private int myExitCode;
public void detach() {
myWaitForThreadFuture.cancel(true);
myWaitSemaphore.up();
}
public ProcessWaitFor(final Process process) {
myWaitSemaphore.down();
final Runnable action = new Runnable() {
public void run() {
try {
myExitCode = process.waitFor();
}
catch (InterruptedException e) {
// Do nothing, by design
}
myWaitSemaphore.up();
}
};
myWaitForThreadFuture = executeOnPooledThread(action);
}
public int waitFor() {
myWaitSemaphore.waitFor();
return myExitCode;
}
}
public void startNotify() {
final ReadProcessThread stdoutThread = new ReadProcessThread(createProcessOutReader()) {
protected void textAvailable(String s) {
notifyTextAvailable(s, ProcessOutputTypes.STDOUT);
}
};
final ReadProcessThread stderrThread = new ReadProcessThread(createProcessErrReader()) {
protected void textAvailable(String s) {
notifyTextAvailable(s, ProcessOutputTypes.STDERR);
}
};
//notifyTextAvailable(myCommandLine + '\n', ProcessOutputTypes.SYSTEM);
addProcessListener(new ProcessAdapter() {
public void startNotified(final ProcessEvent event) {
try {
final Future<?> stdOutReadingFuture = executeOnPooledThread(stdoutThread);
final Future<?> stdErrReadingFuture = executeOnPooledThread(stderrThread);
final Runnable action = new Runnable() {
public void run() {
int exitCode = 0;
try {
exitCode = myWaitFor.waitFor();
// tell threads that no more attempts to read process' output should be made
stderrThread.setProcessTerminated(true);
stdoutThread.setProcessTerminated(true);
stdErrReadingFuture.get();
stdOutReadingFuture.get();
}
catch (InterruptedException e) {
// Do nothing
}
catch (ExecutionException e) {
// Do nothing
}
onOSProcessTerminated(exitCode);
}
};
executeOnPooledThread(action);
}
finally {
removeProcessListener(this);
}
}
});
super.startNotify();
}
protected void onOSProcessTerminated(final int exitCode) {
notifyProcessTerminated(exitCode);
}
protected Reader createProcessOutReader() {
return new BufferedReader(new InputStreamReader(myProcess.getInputStream(), getCharset()));
}
protected Reader createProcessErrReader() {
return new BufferedReader(new InputStreamReader(myProcess.getErrorStream(), getCharset()));
}
protected void destroyProcessImpl() {
try {
closeStreams();
}
finally {
myProcess.destroy();
}
}
protected void detachProcessImpl() {
final Runnable runnable = new Runnable() {
public void run() {
closeStreams();
myWaitFor.detach();
notifyProcessDetached();
}
};
executeOnPooledThread(runnable);
}
private void closeStreams() {
try {
myProcess.getOutputStream().close();
}
catch (IOException e) {
LOG.error(e);
}
}
public boolean detachIsDefault() {
return false;
}
public OutputStream getProcessInput() {
return myProcess.getOutputStream();
}
public Charset getCharset() {
return EncodingManager.getInstance().getDefaultCharset();
}
private static abstract class ReadProcessThread implements Runnable {
private static final int NOTIFY_TEXT_DELAY = 300;
private final Reader myReader;
private final StringBuffer myBuffer = new StringBuffer();
private final Alarm myAlarm;
private boolean myIsClosed = false;
private boolean myIsProcessTerminated = false;
public ReadProcessThread(final Reader reader) {
myReader = reader;
myAlarm = new Alarm(Alarm.ThreadToUse.SHARED_THREAD);
}
public synchronized boolean isProcessTerminated() {
return myIsProcessTerminated;
}
public synchronized void setProcessTerminated(boolean isProcessTerminated) {
myIsProcessTerminated = isProcessTerminated;
}
public void run() {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
try {
myAlarm.addRequest(new Runnable() {
public void run() {
if (!isClosed()) {
myAlarm.addRequest(this, NOTIFY_TEXT_DELAY);
checkTextAvailable();
}
}
}, NOTIFY_TEXT_DELAY);
try {
while (!isClosed()) {
final int c = readNextByte();
if (c == -1) {
break;
}
synchronized (myBuffer) {
myBuffer.append((char) c);
}
if (c == '\n') { // not by '\r' because of possible '\n'
checkTextAvailable();
}
}
}
catch (Exception e) {
LOG.error(e);
e.printStackTrace();
}
close();
}
finally {
Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
}
}
private int readNextByte() {
try {
while (!myReader.ready()) {
if (isProcessTerminated()) {
return -1;
}
try {
Thread.sleep(1L);
}
catch (InterruptedException ignore) {
}
}
return myReader.read();
}
catch (IOException e) {
return -1; // When process terminated Process.getInputStream()'s underlaying stream becomes closed on Linux.
}
}
private void checkTextAvailable() {
synchronized (myBuffer) {
if (myBuffer.length() == 0) return;
// warning! Since myBuffer is reused, do not use myBuffer.toString() to fetch the string
// because the created string will get StringBuffer's internal char array as a buffer which is possibly too large.
final String s = myBuffer.substring(0, myBuffer.length());
myBuffer.setLength(0);
textAvailable(s);
}
}
private void close() {
synchronized (this) {
if (isClosed()) {
return;
}
myIsClosed = true;
}
try {
myReader.close();
}
catch (IOException e1) {
// supressed
}
checkTextAvailable();
}
protected abstract void textAvailable(final String s);
private synchronized boolean isClosed() {
return myIsClosed;
}
}
}