/*******************************************************************************
* Copyright (c) 2011 Wind River Systems, Inc. and others. All rights reserved.
* This program and the accompanying materials are made available under the terms
* of the Eclipse Public License v1.0 which accompanies this distribution, and is
* available at http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Wind River Systems - initial API and implementation
*******************************************************************************/
package org.eclipse.tm.te.ui.terminals.process;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.eclipse.cdt.utils.pty.PTY;
import org.eclipse.cdt.utils.spawner.ProcessFactory;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.osgi.service.environment.Constants;
import org.eclipse.osgi.util.NLS;
import org.eclipse.tm.internal.terminal.provisional.api.ISettingsPage;
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.te.runtime.services.interfaces.constants.ILineSeparatorConstants;
import org.eclipse.tm.te.ui.terminals.process.activator.UIPlugin;
import org.eclipse.tm.te.ui.terminals.process.nls.Messages;
import org.eclipse.tm.te.ui.terminals.streams.AbstractStreamsConnector;
/**
* Process connector implementation.
*/
@SuppressWarnings("restriction")
public class ProcessConnector extends AbstractStreamsConnector {
// Reference to the process settings
private final ProcessSettings settings;
// Reference to the PTY instance.
private PTY pty;
// Reference to the launched process instance.
private Process process;
// Reference to the process monitor
private ProcessMonitor monitor;
// The terminal width and height. Initially unknown.
private int width = -1;
private int height = -1;
/**
* Constructor.
*/
public ProcessConnector() {
this(new ProcessSettings());
}
/**
* Constructor.
*
* @param settings The process settings. Must not be <code>null</code>
*/
public ProcessConnector(ProcessSettings settings) {
super();
Assert.isNotNull(settings);
this.settings = settings;
}
/**
* Returns the process object or <code>null</code> if the
* connector is connector.
*
* @return The process object or <code>null</code>.
*/
public Process getProcess() {
return process;
}
/* (non-Javadoc)
* @see org.eclipse.tm.internal.terminal.provisional.api.provider.TerminalConnectorImpl#connect(org.eclipse.tm.internal.terminal.provisional.api.ITerminalControl)
*/
@Override
public void connect(ITerminalControl control) {
Assert.isNotNull(control);
super.connect(control);
pty = null;
width = -1;
height = -1;
try {
// Try to determine process and PTY instance from the process settings
process = settings.getProcess();
pty = settings.getPTY();
// No process -> create PTY on supported platforms and execute
// process image.
if (process == null) {
if (PTY.isSupported()) {
try {
pty = new PTY(false);
} catch (IOException e) {
// PTY not supported on windows
}
}
// Build up the command
StringBuilder command = new StringBuilder(settings.getImage());
String arguments = settings.getArguments();
if (arguments != null && !"".equals(arguments.trim())) { //$NON-NLS-1$
// Append to the command now
command.append(" "); //$NON-NLS-1$
command.append(arguments.trim());
}
if (pty != null) {
// A PTY is available -> can use the ProcessFactory.
// Tokenize the command (ProcessFactory takes an array)
StreamTokenizer st = new StreamTokenizer(new StringReader(command.toString()));
st.resetSyntax();
st.whitespaceChars(0, 32);
st.whitespaceChars(0xa0, 0xa0);
st.wordChars(33, 255);
st.quoteChar('"');
st.quoteChar('\'');
List<String> argv = new ArrayList<String>();
int ttype = st.nextToken();
while (ttype != StreamTokenizer.TT_EOF) {
argv.add(st.sval);
ttype = st.nextToken();
}
// Execute the process
process = ProcessFactory.getFactory().exec(argv.toArray(new String[argv.size()]), getProcessEnvironment(), null, pty);
} else {
// No PTY -> just execute via the standard Java Runtime implementation.
process = Runtime.getRuntime().exec(command.toString());
}
}
String lineSeparator = settings.getLineSeparator();
if (lineSeparator == null) {
lineSeparator = System.getProperty("line.separator"); //$NON-NLS-1$
if ("\r".equals(lineSeparator)) { //$NON-NLS-1$
lineSeparator = ILineSeparatorConstants.LINE_SEPARATOR_CR;
}
else if ("\n".equals(lineSeparator)) { //$NON-NLS-1$
lineSeparator = ILineSeparatorConstants.LINE_SEPARATOR_LF;
}
else {
lineSeparator = ILineSeparatorConstants.LINE_SEPARATOR_CRLF;
}
}
// connect the streams
connectStreams(control, process.getOutputStream(), process.getInputStream(), (pty == null ? process.getErrorStream() : null), settings.isLocalEcho(), lineSeparator);
// Set the terminal control state to CONNECTED
control.setState(TerminalState.CONNECTED);
// Create the process monitor
monitor = new ProcessMonitor(this);
monitor.startMonitoring();
} catch (Exception e) {
IStatus status = new Status(IStatus.ERROR, UIPlugin.getUniqueIdentifier(),
NLS.bind(Messages.ProcessConnector_error_creatingProcess, e.getLocalizedMessage()), e);
UIPlugin.getDefault().getLog().log(status);
}
}
/* (non-Javadoc)
* @see org.eclipse.tm.internal.terminal.provisional.api.provider.TerminalConnectorImpl#isLocalEcho()
*/
@Override
public boolean isLocalEcho() {
return settings.isLocalEcho();
}
/* (non-Javadoc)
* @see org.eclipse.tm.internal.terminal.provisional.api.provider.TerminalConnectorImpl#doDisconnect()
*/
@Override
public void doDisconnect() {
// Dispose the process
if (process != null) { process.destroy(); process = null; }
// Dispose the streams
super.doDisconnect();
// Set the terminal control state to CLOSED.
fControl.setState(TerminalState.CLOSED);
}
// ***** Process Connector settings handling *****
/* (non-Javadoc)
* @see org.eclipse.tm.internal.terminal.provisional.api.provider.TerminalConnectorImpl#makeSettingsPage()
*/
@Override
public ISettingsPage makeSettingsPage() {
return new ProcessSettingsPage(settings);
}
/* (non-Javadoc)
* @see org.eclipse.tm.internal.terminal.provisional.api.provider.TerminalConnectorImpl#getSettingsSummary()
*/
@Override
public String getSettingsSummary() {
return settings.getImage() != null ? settings.getImage() : ""; //$NON-NLS-1$
}
/* (non-Javadoc)
* @see org.eclipse.tm.internal.terminal.provisional.api.provider.TerminalConnectorImpl#load(org.eclipse.tm.internal.terminal.provisional.api.ISettingsStore)
*/
@Override
public void load(ISettingsStore store) {
settings.load(store);
}
/* (non-Javadoc)
* @see org.eclipse.tm.internal.terminal.provisional.api.provider.TerminalConnectorImpl#save(org.eclipse.tm.internal.terminal.provisional.api.ISettingsStore)
*/
@Override
public void save(ISettingsStore store) {
settings.save(store);
}
/* (non-Javadoc)
* @see org.eclipse.tm.internal.terminal.provisional.api.provider.TerminalConnectorImpl#setTerminalSize(int, int)
*/
@Override
public void setTerminalSize(int newWidth, int newHeight) {
if (width != newWidth || height != newHeight) {
width = newWidth;
height = newHeight;
if (pty != null) {
pty.setTerminalSize(newWidth, newHeight);
}
}
}
// ***** Process Environment Handling *****
// Reference to the monitor to lock if determining the native environment
private final static Object ENV_GET_MONITOR = new Object();
// Reference to the native environment once retrieved
private static Map<String, String> nativeEnvironment = null;
// Reference to the native environment with the case of the variable names preserved
private static Map<String, String> nativeEnvironmentCasePreserved = null;
/**
* Returns the specific environment to set for the process to be launched.
*
* @return The process environment.
*/
private static String[] getProcessEnvironment() {
Map<String, String> env = getNativeEnvironment();
env.put("TERM", "ansi"); //$NON-NLS-1$ //$NON-NLS-2$
Iterator<Map.Entry<String, String>> iter = env.entrySet().iterator();
List<String> strings = new ArrayList<String>(env.size());
StringBuffer buffer = null;
while (iter.hasNext()) {
Map.Entry<String, String>entry = iter.next();
buffer = new StringBuffer(entry.getKey());
buffer.append('=').append(entry.getValue());
strings.add(buffer.toString());
}
return strings.toArray(new String[strings.size()]);
}
/**
* Determine the native environment, but returns all environment variable
* names in upper case.
*
* @return The native environment with upper case variable names, or an empty map.
*/
private static Map<String, String> getNativeEnvironment() {
synchronized (ENV_GET_MONITOR) {
if (nativeEnvironment == null) {
Map<String, String> casePreserved = getNativeEnvironmentCasePreserved();
if (Platform.getOS().equals(org.eclipse.osgi.service.environment.Constants.OS_WIN32)) {
nativeEnvironment = new HashMap<String, String>();
Iterator<Map.Entry<String, String>> entries = casePreserved.entrySet().iterator();
while (entries.hasNext()) {
Map.Entry<String, String> entry = entries.next();
nativeEnvironment.put(entry.getKey().toUpperCase(), entry.getValue());
}
} else {
nativeEnvironment = new HashMap<String, String>(casePreserved);
}
}
return new HashMap<String, String>(nativeEnvironment);
}
}
/**
* Determine the native environment.
*
* @return The native environment, or an empty map.
*/
private static Map<String, String> getNativeEnvironmentCasePreserved() {
synchronized (ENV_GET_MONITOR) {
if (nativeEnvironmentCasePreserved == null) {
nativeEnvironmentCasePreserved= new HashMap<String, String>();
cacheNativeEnvironment(nativeEnvironmentCasePreserved);
}
return new HashMap<String, String>(nativeEnvironmentCasePreserved);
}
}
/**
* Query the native environment and store it to the specified cache.
*
* @param cache The environment cache. Must not be <code>null</code>.
*/
private static void cacheNativeEnvironment(Map<String, String> cache) {
Assert.isNotNull(cache);
try {
String nativeCommand = null;
boolean isWin9xME = false; // see bug 50567
String fileName = null;
if (Platform.getOS().equals(Constants.OS_WIN32)) {
String osName = System.getProperty("os.name"); //$NON-NLS-1$
isWin9xME = osName != null && (osName.startsWith("Windows 9") || osName.startsWith("Windows ME")); //$NON-NLS-1$ //$NON-NLS-2$
if (isWin9xME) {
// Win 95, 98, and ME
// SET might not return therefore we pipe into a file
IPath stateLocation = UIPlugin.getDefault().getStateLocation();
fileName = stateLocation.toOSString() + File.separator + "env.txt"; //$NON-NLS-1$
nativeCommand = "command.com /C set > " + fileName; //$NON-NLS-1$
} else {
// Win NT, 2K, XP
nativeCommand = "cmd.exe /C set"; //$NON-NLS-1$
}
} else if (!Platform.getOS().equals(Constants.OS_UNKNOWN)) {
nativeCommand = "env"; //$NON-NLS-1$
}
if (nativeCommand == null) { return; }
Process process = Runtime.getRuntime().exec(nativeCommand);
if (isWin9xME) {
// read piped data on Win 95, 98, and ME
Properties p = new Properties();
File file = new File(fileName);
InputStream stream = new BufferedInputStream(new FileInputStream(file));
p.load(stream);
stream.close();
if (!file.delete()) {
file.deleteOnExit(); // if delete() fails try again on VM close
}
for (Enumeration<Object> enumeration = p.keys(); enumeration.hasMoreElements();) {
// Win32's environment variables are case insensitive. Put everything
// to upper case so that (for example) the "PATH" variable will match
// "pAtH" correctly on Windows.
String key = (String)enumeration.nextElement();
cache.put(key, (String)p.get(key));
}
} else {
// read process directly on other platforms
// we need to parse out matching '{' and '}' for function declarations in .bash environments
// pattern is [function name]=() { and we must find the '}' on its own line with no trailing ';'
InputStream stream = process.getInputStream();
InputStreamReader isreader = new InputStreamReader(stream);
BufferedReader reader = new BufferedReader(isreader);
String line = reader.readLine();
String key = null;
String value = null;
while (line != null) {
int func = line.indexOf("=()"); //$NON-NLS-1$
if (func > 0) {
key = line.substring(0, func);
// scan until we find the closing '}' with no following chars
value = line.substring(func + 1);
while (line != null && !line.equals("}")) { //$NON-NLS-1$
line = reader.readLine();
if (line != null) {
value += line;
}
}
line = reader.readLine();
} else {
int separator = line.indexOf('=');
if (separator > 0) {
key = line.substring(0, separator);
value = line.substring(separator + 1);
line = reader.readLine();
if (line != null) {
// this line has a '=' read ahead to check next line for '=', might be broken on more
// than one line
separator = line.indexOf('=');
while (separator < 0) {
value += line.trim();
line = reader.readLine();
if (line == null) {
// if next line read is the end of the file quit the loop
break;
}
separator = line.indexOf('=');
}
}
}
}
if (key != null) {
cache.put(key, value);
key = null;
value = null;
} else {
line = reader.readLine();
}
}
reader.close();
}
} catch (IOException e) {
// Native environment-fetching code failed.
// This can easily happen and is not useful to log.
}
}
}