package com.bitbakery.plugin.arc.repl; /* * Copyright (c) Kurt Christensen, 2009 * * Licensed under the Artistic 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.opensource.org/licenses/artistic-license-2.0.php * * 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.. */ import static com.intellij.openapi.util.SystemInfo.*; import com.bitbakery.plugin.arc.config.ArcSettings; 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.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.encoding.EncodingManager; import com.intellij.util.Alarm; import com.intellij.util.text.StringTokenizer; import java.io.*; import java.nio.charset.Charset; import java.util.concurrent.*; /** * Ripped off from OSProcessHandler, minus the command line noise */ public class ArcProcessHandler extends ProcessHandler { private static final Logger LOG = Logger.getInstance(ArcProcessHandler.class.getName()); private static ExecutorService ourThreadExecutorsService = null; private final Process myProcess; private final ProcessWaitFor myWaitFor; 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 ArcProcessHandler() throws IOException, ConfigurationException { /* TODO - This is broken for some stupid reason... if (notConfigured()) { ShowSettingsUtil.getInstance().showSettingsDialog(project, ArcConfigurable.class); } */ if (notConfigured()) { // TODO - Put me in a resource bundle, please! throw new ConfigurationException("Can't create Arc REPL process"); } else { String scheme = ArcSettings.getInstance().mzSchemeHome; if (!StringUtil.isEmptyOrSpaces(scheme)) { scheme += isMac ? "/bin/" : "/"; } scheme += isWindows ? "MzScheme.exe" : "mzscheme"; String[] myCommandLine = new String[]{scheme, "-m", "-f", ArcSettings.getInstance().arcInitializationFile}; // For now, the command-line is hard-coded. We may need more flexibility // in the future (e.g., different Arc paths with different args) myProcess = Runtime.getRuntime().exec(myCommandLine, null, new File(ArcSettings.getInstance().arcHome)); myWaitFor = new ProcessWaitFor(myProcess); } addProcessListener(new ProcessAdapter() { public void onTextAvailable(ProcessEvent event, Key outputType) { System.out.println(scrub(event.getText())); } private String scrub(String output) { // TODO - Holy... please clean this! StringTokenizer t = new StringTokenizer(output, "=>"); if (t.hasMoreTokens()) { t.nextToken(); if (t.hasMoreTokens()) { return t.nextToken().trim(); } } return output; } }); } private boolean notConfigured() { return StringUtil.isEmptyOrSpaces(ArcSettings.getInstance().arcHome) || StringUtil.isEmptyOrSpaces(ArcSettings.getInstance().mzSchemeHome) || StringUtil.isEmptyOrSpaces(ArcSettings.getInstance().arcInitializationFile); } 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 { // if(Thread.currentThread() != this) { // join(0); // } //} //catch (InterruptedException e) { //} // must close after the thread finished its execution, cause otherwise // the thread will try to read from the closed (and nulled) stream try { myReader.close(); } catch (IOException e1) { // supressed } checkTextAvailable(); } protected abstract void textAvailable(final String s); private synchronized boolean isClosed() { return myIsClosed; } } }