/**
* Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the Eclipse Public License (EPL).
* Please see the license.txt included with this distribution for details.
* Any modifications to this file must keep this entire header intact.
*/
package org.python.pydev.customizations.common;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.program.Program;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Link;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.python.pydev.core.IPythonPathNature;
import org.python.pydev.core.docutils.StringUtils;
import org.python.pydev.core.log.Log;
import org.python.pydev.debug.ui.launching.PythonRunnerConfig;
import org.python.pydev.runners.UniversalRunner;
import org.python.pydev.runners.UniversalRunner.AbstractRunner;
import com.aptana.shared_core.io.ThreadStreamReader;
import com.aptana.shared_core.string.FastStringBuffer;
import com.aptana.shared_core.structure.Tuple;
/**
* This is the window used to handle a process. Currently specific to google app engine (could be more customizable
* if needed).
*/
public abstract class ProcessWindow extends Dialog {
//Labels
private static final String SEND_TO_PROMPT_LABEL = "Send to &prompt: ";
private static final String SEND_LABEL = "&Send";
private static final String EXECUTING_COMMAND_LABEL = "E&xecuting: ";
private static final String COMMAND_TO_EXECUTE_LABEL = "Command to e&xecute: ";
private static final String CLOSE_LABEL = "C&lose";
private static final String CANCEL_LABEL = "&Cancel";
private static final String RUN_LABEL = "&Run";
//Input
protected Text output;
protected IContainer container;
protected IPythonPathNature pythonPathNature;
protected File appcfg;
protected File appEngineLocation;
//If not null, this command should be run when the interface is opened.
protected String initialCommand;
//lock and state
private Object lock = new Object();
private static final int STATE_NOT_RUNNING = 0;
private static final int STATE_RUNNING = 1;
private volatile int state = STATE_NOT_RUNNING;
//only while running
private ThreadStreamReader err;
private ThreadStreamReader std;
private OutputStream outputStream;
private ProcessHandler processHandler;
private Process process;
//UI
private Button cancelButton;
private Button okButton;
private Combo commandToExecute;
private Text sendToText;
private final int NUMBER_OF_COLUMNS = 6;
private Label commandToExecuteLabel;
/**
* This thread is responsible for reading from the process and writing to it asynchronously.
*/
class ProcessHandler extends Thread {
/**
* List of commands that still need to be sent to the process.
*/
private List<String> commandTexts = new ArrayList<String>();
/**
* Lock for accessing commandTexts.
*/
private Object commandTextsLock = new Object();
/**
* Whether we should force the process to quit
*/
private boolean forceQuit = false;
/**
* A buffer with the contents found. We analyze that buffer to know if we should
* change the command Text to have an echo char (when password is requested)
*/
private FastStringBuffer buffer = new FastStringBuffer();
/**
* Keep here until process is finished (naturally or we finish it).
*/
public void run() {
try {
try {
while (process != null && forceQuit == false) {
boolean hasExited = true;
try {
process.exitValue();
} catch (IllegalThreadStateException e) {
//that's ok, still running!
hasExited = false;
}
try {
Thread.sleep(75);
} catch (InterruptedException e1) {
//ignore
}
try {
String errContents = err.getAndClearContents();
String stdContents = std.getAndClearContents();
append(errContents);
append(stdContents);
if (hasExited) {
process = null;
} else {
synchronized (commandTextsLock) {
if (this.commandTexts.size() > 0) {
String txt = this.commandTexts.remove(0);
try {
append("\n");
outputStream.write(txt.getBytes());
outputStream.flush();
} catch (IOException e) {
Log.log(e);
}
}
}
}
} catch (Exception e) {
append(e.getMessage());
Log.log(e);
break;
}
}
} finally {
//Gotten out of the loop.
if (process != null) {
append("Forcing the process to quit.\n");
try {
process.destroy();
} catch (Exception e) {
}
process = null;
}
//liberate all.
err = null;
std = null;
outputStream = null;
processHandler = null;
append("FINISHED\n\n");
}
} finally {
onEndRun();
}
}
/**
* This function is called synchronously, but adds the contents to the output window the user
* is seeing asynchronously.
*
* It also sets the echo char by analyzing the available contents.
*
* Nothing is done if the contents is an empty string.
*/
private void append(final String contents) {
if (contents == null || contents.length() == 0) {
return;
}
buffer.append(contents);
if (buffer.length() > 2000) {
//Let it always close to 2000.
try {
buffer.delete(0, 2000 - buffer.length());
} catch (Exception e) {
}
}
final List<String> split = StringUtils.splitInLines(buffer.toString());
Display.getDefault().asyncExec(new Runnable() {
public void run() {
if (split.size() > 0) {
String last = split.get(split.size() - 1);
if (last.toLowerCase().indexOf("password for") != -1) {
ProcessWindow.this.sendToText.setEchoChar('*');
} else {
ProcessWindow.this.sendToText.setEchoChar('\0');
}
}
output.append(contents);
}
});
}
/**
* Adds a command to be executed (asynchronously)
*/
public void addCommandText(String text) {
synchronized (commandTextsLock) {
this.commandTexts.add(text);
}
}
}
/**
* We need to set the shell style to be resizable.
*/
public ProcessWindow(Shell parentShell) {
super(parentShell);
setShellStyle(getShellStyle() | SWT.RESIZE);
}
protected void configureShell(final Shell shell) {
super.configureShell(shell);
shell.setText("Manage Google App Engine");
}
/**
* Create the dialog contents
*/
@Override
protected Control createDialogArea(Composite parent) {
Composite top = (Composite) super.createDialogArea(parent);
Composite composite = new Composite(top, SWT.None);
composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
composite.setLayout(new GridLayout(NUMBER_OF_COLUMNS, false));
createLabel(composite, "Arguments to pass to: " + appcfg.getAbsolutePath());
createLabel(composite, "The command line can be changed as needed.");
Link link = new Link(composite, SWT.None);
link.setText("See <a>http://code.google.com/appengine/docs/python/tools/uploadinganapp.html</a>");
link.addSelectionListener(new SelectionListener() {
public void widgetDefaultSelected(SelectionEvent e) {
}
public void widgetSelected(SelectionEvent e) {
Program.launch("http://code.google.com/appengine/docs/python/tools/uploadinganapp.html");
}
});
GridData gridData = new GridData(GridData.FILL_HORIZONTAL);
gridData.horizontalSpan = NUMBER_OF_COLUMNS;
link.setLayoutData(gridData);
//--- Command to execute
commandToExecuteLabel = createLabel(composite, COMMAND_TO_EXECUTE_LABEL, 1);
commandToExecute = new Combo(composite, SWT.SINGLE | SWT.BORDER);
gridData = new GridData(GridData.FILL_HORIZONTAL);
gridData.horizontalSpan = NUMBER_OF_COLUMNS - 2; //1 from the label and 1 from the button
gridData.grabExcessHorizontalSpace = true;
commandToExecute.setLayoutData(gridData);
String[] availableCommands = getAvailableCommands();
commandToExecute.setItems(availableCommands);
commandToExecute.setText(availableCommands[0]);
okButton = createButton(composite, RUN_LABEL, 1, SWT.PUSH);
okButton.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent event) {
buttonPressed(IDialogConstants.OK_ID);
}
});
okButton.setData(IDialogConstants.OK_ID);
//--- main output
output = new Text(composite, SWT.MULTI | SWT.BORDER | SWT.V_SCROLL | SWT.WRAP | SWT.READ_ONLY);
gridData = new GridData(GridData.FILL_BOTH);
gridData.horizontalSpan = NUMBER_OF_COLUMNS;
gridData.grabExcessHorizontalSpace = true;
gridData.grabExcessVerticalSpace = true;
output.setLayoutData(gridData);
//--- Send any command to the shell
createLabel(composite, SEND_TO_PROMPT_LABEL, 1);
sendToText = createText(composite, NUMBER_OF_COLUMNS - 2); //1 from the label and 1 from the button
sendToText.addKeyListener(new KeyListener() {
public void keyReleased(KeyEvent e) {
if (e.character == '\r' || e.character == '\n') {
addCurrentCommand();
}
}
public void keyPressed(KeyEvent e) {
}
});
Button button = createButton(composite, SEND_LABEL, 1, SWT.PUSH);
button.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent e) {
addCurrentCommand();
}
});
return top;
}
/**
* Subclasses should override to provide the commands available to be executed.
*/
protected abstract String[] getAvailableCommands();
private Label createLabel(Composite composite, String message) {
return createLabel(composite, message, NUMBER_OF_COLUMNS);
}
private Label createLabel(Composite composite, String message, int horizontalSpan) {
Label label = new Label(composite, SWT.None);
label.setText(message);
GridData gridData = new GridData(GridData.FILL_HORIZONTAL);
gridData.horizontalSpan = horizontalSpan;
label.setLayoutData(gridData);
return label;
}
@Override
protected void constrainShellSize() {
getShell().setSize(640, 480);
super.constrainShellSize();
}
protected void createButtonsForButtonBar(Composite parent) {
cancelButton = createButton(parent, IDialogConstants.CANCEL_ID, CLOSE_LABEL, false);
}
/**
* Overridden to execute the initial command if we had one set.
*/
@Override
public void create() {
super.create();
//After creating things, execute the initial command if it was set.
this.okButton.setFocus();
if (this.initialCommand != null) {
commandToExecute.setText(this.initialCommand);
this.okPressed();
}
}
/**
* The Ok is used for Run/Cancel.
*/
@Override
protected void okPressed() {
boolean doRun = false;
synchronized (lock) {
if (state == STATE_NOT_RUNNING) {
state = STATE_RUNNING;
doRun = true;
}
}
if (doRun) {
run();
commandToExecuteLabel.setText(EXECUTING_COMMAND_LABEL);
commandToExecute.setEnabled(false);
cancelButton.setEnabled(false);
okButton.setText(CANCEL_LABEL);
} else {
//We're running... this means it meant a cancel.
cancelRun();
}
}
/**
* Requests the process to be canceled (when it's possible to do so). onEndRun() is called after
* it's successfully canceled.
*/
private void cancelRun() {
//Running: cancel it.
ProcessHandler handler = this.processHandler;
if (handler != null) {
handler.forceQuit = true;
}
}
private void onEndRun() {
synchronized (lock) {
state = STATE_NOT_RUNNING;
}
Display.getDefault().asyncExec(new Runnable() {
public void run() {
commandToExecuteLabel.setText(COMMAND_TO_EXECUTE_LABEL);
commandToExecute.setEnabled(true);
cancelButton.setEnabled(true);
okButton.setText(RUN_LABEL);
}
});
}
public boolean close() {
if (state == STATE_NOT_RUNNING) {
return super.close();
} else {
cancelRun();
return false;
}
}
@Override
protected void cancelPressed() {
if (state == STATE_NOT_RUNNING) {
super.cancelPressed();
} else {
cancelRun();
}
}
public void setParameters(IContainer container, IPythonPathNature pythonPathNature, File appcfg,
File appEngineLocation) {
this.container = container;
this.pythonPathNature = pythonPathNature;
this.appcfg = appcfg;
this.appEngineLocation = appEngineLocation;
}
private void run() {
if (processHandler != null) {
return; //Still running.
}
try {
String cmdLineArguments = commandToExecute.getText().trim();
String[] arguments = new String[0];
if (cmdLineArguments.length() > 0) {
arguments = PythonRunnerConfig.parseStringIntoList(cmdLineArguments);
}
AbstractRunner universalRunner = UniversalRunner.getRunner(pythonPathNature.getNature());
Tuple<Process, String> run = universalRunner.createProcess(appcfg.getAbsolutePath(), arguments,
appEngineLocation, new NullProgressMonitor());
process = run.o1;
if (process != null) {
std = new ThreadStreamReader(process.getInputStream());
err = new ThreadStreamReader(process.getErrorStream());
std.start();
err.start();
outputStream = process.getOutputStream();
processHandler = new ProcessHandler();
processHandler.start();
}
} catch (Exception e) {
Log.log(e);
}
}
private Button createButton(Composite composite, String label, int colSpan, int style) {
Button button = new Button(composite, style);
button.setText(label);
setButtonLayout(button, colSpan);
return button;
}
private void setButtonLayout(Button button, int colSpan) {
GridData gridData;
gridData = new GridData(GridData.FILL_HORIZONTAL);
gridData.horizontalSpan = colSpan;
gridData.grabExcessHorizontalSpace = true;
button.setLayoutData(gridData);
}
private Text createText(Composite composite, int colSpan) {
Text text = new Text(composite, SWT.SINGLE | SWT.BORDER);
GridData gridData = new GridData(GridData.FILL_HORIZONTAL);
gridData.horizontalSpan = colSpan;
text.setLayoutData(gridData);
return text;
}
private void addCurrentCommand() {
ProcessHandler p = processHandler;
if (p != null) {
String text = sendToText.getText();
sendToText.setText("");
p.addCommandText(text + "\n");
}
}
public void setInitialCommandToRun(String initialCommand) {
this.initialCommand = initialCommand;
}
}