package org.rascalmpl.eclipse.repl; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.Arrays; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.debug.core.DebugEvent; import org.eclipse.debug.core.DebugException; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.IDebugEventSetListener; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; import org.eclipse.debug.core.IStreamListener; import org.eclipse.debug.core.model.IProcess; import org.eclipse.debug.core.model.IStreamMonitor; import org.eclipse.debug.core.model.IStreamsProxy; import org.eclipse.debug.ui.IDebugUIConstants; import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; import org.eclipse.swt.widgets.Display; import org.eclipse.tm.internal.terminal.emulator.VT100TerminalControl; import org.eclipse.tm.internal.terminal.provisional.api.ISettingsStore; import org.eclipse.tm.internal.terminal.provisional.api.ITerminalControl; import org.eclipse.tm.internal.terminal.provisional.api.TerminalState; import org.eclipse.tm.internal.terminal.provisional.api.provider.TerminalConnectorImpl; import org.rascalmpl.eclipse.Activator; import org.rascalmpl.shell.EclipseTerminalConnection; import org.rascalmpl.shell.RascalShell; @SuppressWarnings("restriction") public class JavaTerminalConnector extends TerminalConnectorImpl { @Override public OutputStream getTerminalToRemoteStream() { return stdInUI; } private OutputStream stdInUI; private String file; private ILaunchConfiguration config; private String mode = "run"; private ILaunch launch; private IDebugEventSetListener detectTerminated; private int port; private ServerSocket server; private int currentWidth; private int currentHeight; @Override public boolean isLocalEcho() { return false; } @Override public void load(ISettingsStore store) { String label = store.get("launchConfiguration"); if (label != null) { for (ILaunchConfiguration config : JavaLauncherDelegate.getJavaLaunchConfigs()) { if (config.getName().equals(label)) { this.config = config; } } } if (this.config == null) { throw new RuntimeException("unable to load configuration " + label); } mode = store.get("mode"); } @Override public void connect(ITerminalControl control) { assert this.config != null; super.connect(control); configure((VT100TerminalControl)control); control.setState(TerminalState.CONNECTING); try { ILaunchConfigurationWorkingCopy workingCopy = config.getWorkingCopy(); // this is necessary to enable the test for ATTR_CAPTURE_IN_FILE: workingCopy.setAttribute(IDebugUIConstants.ATTR_CAPTURE_IN_FILE, System.getProperty("os.name").startsWith("Windows") ? "nul" : "/dev/null"); // this makes sure the terminal does not echo the characters to the normal console as well: workingCopy.setAttribute(IDebugUIConstants.ATTR_CAPTURE_IN_CONSOLE, false); if (System.getProperty("os.name").toLowerCase().contains("mac")) { server = startREPLWindowSizeSocket(); String vmArgs = workingCopy.getAttribute(IJavaLaunchConfigurationConstants.ATTR_VM_ARGUMENTS, ""); vmArgs += " -D" + RascalShell.ECLIPSE_TERMINAL_CONNECTION_REPL_KEY + "=" + port; workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_VM_ARGUMENTS, vmArgs); } else { server = null; } launch = workingCopy.launch(mode, new NullProgressMonitor(), true /*build first*/, true /*do register for debug*/); if (launch.getProcesses().length == 1) { final IProcess currentProcess = launch.getProcesses()[0]; final IStreamsProxy proxy = currentProcess.getStreamsProxy(); stdInUI = new OutputStream() { //private final CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder(); @Override public void write(int b) throws IOException { // todo handle multi byte utf8 stuff (anything not ASCII breaks here) proxy.write(new String(new byte[]{(byte)b}, StandardCharsets.UTF_8)); } @Override public void write(byte[] b, int off, int len) throws IOException { proxy.write(new String(b, off, len, StandardCharsets.UTF_8)); } @Override public void write(byte[] b) throws IOException { write(b, 0, b.length); } }; proxy.getOutputStreamMonitor().addListener(new IStreamListener() { private boolean firstPrint = true; @Override public void streamAppended(String text, IStreamMonitor monitor) { try { if (firstPrint) { Display.getDefault().asyncExec(new Runnable() { public void run() { setFocus(); }; }); firstPrint = false; } control.getRemoteToTerminalOutputStream().write(text.getBytes(StandardCharsets.UTF_8)); } catch (IOException e) { } } }); proxy.getErrorStreamMonitor().addListener(new IStreamListener() { @Override public void streamAppended(String text, IStreamMonitor monitor) { try { control.getRemoteToTerminalOutputStream().write(text.getBytes(StandardCharsets.UTF_8)); } catch (IOException e) { } } }); detectTerminated = new IDebugEventSetListener() { @Override public void handleDebugEvents(DebugEvent[] events) { for (int i = 0; i < events.length; i++) { if (events[i].getSource() == currentProcess && events[i].getKind() == DebugEvent.TERMINATE) { control.setState(TerminalState.CLOSED); DebugPlugin.getDefault().removeDebugEventListener(detectTerminated); if (server != null) { try { server.close(); } catch (IOException e) { } } break; } } } }; DebugPlugin.getDefault().addDebugEventListener(detectTerminated); control.setState(TerminalState.CONNECTED); setFocus(); } else { control.setState(TerminalState.CLOSED); } } catch (Throwable e1) { Activator.log(e1.getMessage(), e1); control.setState(TerminalState.CLOSED); } } private ServerSocket startREPLWindowSizeSocket() { try { final ServerSocket result = new ServerSocket(0, 1, InetAddress.getLoopbackAddress()); port = result.getLocalPort(); Thread runner = new Thread() { public void run() { Socket sock; // only one connection possible try { if ((sock = result.accept()) != null) { DataOutputStream send = new DataOutputStream(sock.getOutputStream()); DataInputStream recv = new DataInputStream(sock.getInputStream()); byte[] clientHeader = new byte[EclipseTerminalConnection.HEADER.length]; int off = 0; while (off < clientHeader.length) { off += recv.read(clientHeader, off, clientHeader.length - off); } if (!Arrays.equals(clientHeader, EclipseTerminalConnection.HEADER)) { throw new RuntimeException("Incorrect client"); } send.write(EclipseTerminalConnection.HEADER); while (true) { switch (recv.readByte()) { case EclipseTerminalConnection.GET_HEIGHT: send.writeInt(currentHeight); break; case EclipseTerminalConnection.GET_WIDTH: send.writeInt(currentWidth); break; } } } } catch (IOException e) { return; } } }; runner.setName("REPL Companion Runner"); runner.setDaemon(true); runner.start(); return result; } catch (IOException e) { throw new RuntimeException(e); } } public ITerminalControl getControl() { return fControl; } private void configure(VT100TerminalControl control) { control.setConnectOnEnterIfClosed(false); control.setVT100LineWrapping(false); control.setBufferLineLimit(10_000); try { control.setEncoding(StandardCharsets.UTF_8.name()); } catch (UnsupportedEncodingException e) { throw new RuntimeException("UTF8 not available???", e); } control.getTerminalText().setCrAfterNewLine(true); ((VT100TerminalControl) control).addMouseListener(new RascalLinkMouseListener());; } @Override protected void doDisconnect() { super.doDisconnect(); if (launch != null) { try { launch.terminate(); if (server != null) { server.close(); } } catch (DebugException | IOException e) { Activator.log(e.getMessage(), e); } } } public void setFocus() { ((VT100TerminalControl) fControl).setFocus(); } @Override public String getSettingsSummary() { return file != null ? "Running Java program " + file : "no file associated"; } @Override public void setTerminalSize(int newWidth, int newHeight) { super.setTerminalSize(newWidth, newHeight); currentWidth = newWidth; currentHeight = newHeight; } }