blob: a1cc98c1a43a13c3b0268e8ffce25defa10f1e16 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2009, 2020 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.internal.r.console.ui.launching;
import java.lang.reflect.InvocationTargetException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RMIClientSocketFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.ibm.icu.text.DateFormat;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.CellLabelProvider;
import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
import org.eclipse.jface.viewers.ColumnWeightData;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.TreeViewerColumn;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
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.Shell;
import org.eclipse.ui.dialogs.SelectionStatusDialog;
import org.eclipse.ui.statushandlers.StatusManager;
import org.eclipse.statet.jcommons.rmi.RMIAddress;
import org.eclipse.statet.ecommons.ui.SharedUIResources;
import org.eclipse.statet.ecommons.ui.dialogs.DialogUtils;
import org.eclipse.statet.ecommons.ui.util.LayoutUtils;
import org.eclipse.statet.ecommons.ui.viewers.ViewerUtils.TreeComposite;
import org.eclipse.statet.internal.r.console.ui.Messages;
import org.eclipse.statet.internal.r.console.ui.RConsoleUIPlugin;
import org.eclipse.statet.rj.server.RjsComConfig;
import org.eclipse.statet.rj.server.Server;
import org.eclipse.statet.rj.server.ServerInfo;
public class RRemoteConsoleSelectionDialog extends SelectionStatusDialog {
public static abstract class SpecialAddress {
private final RMIAddress rmiAddress;
private final String privateHost;
public SpecialAddress(final RMIAddress rmiAddress, final String privateHost) {
this.rmiAddress= rmiAddress;
this.privateHost= privateHost;
}
public abstract RMIClientSocketFactory getSocketFactory(IProgressMonitor monitor) throws CoreException;
}
private static final String SETTINGS_DIALOG_ID= "RRemoteConsoleSelection"; //$NON-NLS-1$
private static final String SETTINGS_HOST_HISTORY_KEY= "hosts.history"; //$NON-NLS-1$
private static final Pattern ADDRESS_MULTI_PATTERN= Pattern.compile("\\/?\\s*[\\,\\;]+\\s*"); //$NON-NLS-1$
private static final Pattern ADDRESS_WITH_PORT_PATTERN= Pattern.compile("(.*):(\\d{1,5})"); //$NON-NLS-1$
private static class RemoteR {
final String hostName;
final String hostIP;
final String address;
final ServerInfo info;
RemoteR(final String hostName, final String hostIP, final String address, final ServerInfo info) {
this.hostName= hostName;
this.hostIP= hostIP;
this.address= address;
this.info= info;
}
String createSummary() {
final StringBuilder sb= new StringBuilder(100);
sb.append("Address: ").append(this.address).append('\n');
sb.append('\n');
sb.append("Host-Name: ").append(this.hostName).append('\n');
sb.append("Host-IP: ").append(this.hostIP).append('\n');
sb.append("Date: ").append((this.info.getTimestamp() != 0) ?
DateFormat.getDateInstance().format(this.info.getTimestamp()) : "<unknown>").append('\n');
sb.append("Directory: ").append(this.info.getDirectory()).append('\n');
sb.append("Status: ");
switch (this.info.getState()) {
case Server.S_NOT_STARTED:
sb.append("New – Ready to connect and start R");
break;
case Server.S_CONNECTED:
case Server.S_CONNECTED_STALE:
sb.append("Running – Connected (username is ");
sb.append((this.info.getUsername(ServerInfo.USER_CONSOLE) != null) ?
this.info.getUsername(ServerInfo.USER_CONSOLE) : "<unknown>").append(')');
break;
case Server.S_LOST:
sb.append("Running – Connection lost / Ready to reconnect");
break;
case Server.S_DISCONNECTED:
sb.append("Running – Disconnected / Ready to reconnect");
break;
case Server.S_STOPPED:
sb.append("Stopped");
break;
default:
sb.append("Unknown");
break;
}
return sb.toString();
}
}
private static class RemoteRContentProvider implements ITreeContentProvider {
private final HashMap<String, RemoteR[]> mapping= new HashMap<>();
@Override
public void inputChanged(final Viewer viewer, final Object oldInput, final Object newInput) {
}
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public Object[] getElements(final Object inputElement) {
final List<RemoteR> all= (List<RemoteR>) inputElement;
this.mapping.clear();
final Map mapping= this.mapping;
for (final RemoteR r : all) {
final String username= r.info.getUsername(ServerInfo.USER_OWNER).toLowerCase();
List<RemoteR> list= (List<RemoteR>) mapping.get(username);
if (list == null) {
list= new ArrayList<>();
mapping.put(username, list);
}
list.add(r);
}
final Set<Map.Entry> entrySet= mapping.entrySet();
for (final Entry cat : entrySet) {
final List<RemoteR> list= (List<RemoteR>) cat.getValue();
cat.setValue(list.toArray(new RemoteR[list.size()]));
}
return this.mapping.keySet().toArray();
}
@Override
public Object getParent(final Object element) {
if (element instanceof RemoteR) {
return ((RemoteR) element).info.getUsername(ServerInfo.USER_OWNER);
}
return null;
}
@Override
public boolean hasChildren(final Object element) {
return (element instanceof String);
}
@Override
public Object[] getChildren(final Object parentElement) {
if (parentElement instanceof String) {
return this.mapping.get(parentElement);
}
return null;
}
@Override
public void dispose() {
}
}
private static abstract class RemoteRLabelProvider extends CellLabelProvider {
@Override
public void update(final ViewerCell cell) {
final Object element= cell.getElement();
String text= null;
if (element instanceof RemoteR) {
text= getText((RemoteR) element);
}
cell.setText(text);
}
public abstract String getText(RemoteR r);
@Override
public Font getToolTipFont(final Object element) {
if (element instanceof RemoteR) {
return JFaceResources.getTextFont();
}
return null;
}
@Override
public String getToolTipText(final Object element) {
if (element instanceof RemoteR) {
return ((RemoteR) element).createSummary();
}
return null;
}
}
private Combo hostAddressControl;
private TreeViewer rServerViewer;
private List<RemoteR> rServerList;
private final boolean filterOnlyRunning;
private String username;
private final List<String> historyAddress= new ArrayList<>(DialogUtils.HISTORY_MAX + 10);
private final List<String> additionalAddress= new ArrayList<>(8);
private final Map<String, SpecialAddress> specialAddress= new HashMap<>(8);
private String initialAddress;
public RRemoteConsoleSelectionDialog(final Shell parentShell, final boolean onlyRunning) {
super(parentShell);
setTitle(Messages.RRemoteConsoleSelectionDialog_title);
setMessage(Messages.RRemoteConsoleSelectionDialog_message);
setStatusLineAboveButtons(true);
setDialogBoundsSettings(getDialogSettings(), Dialog.DIALOG_PERSISTSIZE);
this.username= System.getProperty("user.name"); //$NON-NLS-1$
this.filterOnlyRunning= onlyRunning;
}
public void setUser(final String username) {
if (username != null && username.length() > 0) {
this.username= username;
}
}
public void setInitialAddress(final String address) {
this.additionalAddress.remove(address);
this.additionalAddress.add(0, address);
this.initialAddress= address;
}
public void addAdditionalAddress(final String label, final SpecialAddress factory) {
this.additionalAddress.add(label);
if (factory != null) {
this.specialAddress.put(label, factory);
}
}
public void clearAdditionaAddress(final boolean specialOnly) {
if (specialOnly) {
for (final Entry<String, ?> entry : this.specialAddress.entrySet()) {
this.additionalAddress.remove(entry.getKey());
}
}
else {
this.additionalAddress.clear();
}
this.specialAddress.clear();
}
protected IDialogSettings getDialogSettings() {
return DialogUtils.getDialogSettings(RConsoleUIPlugin.getInstance(), SETTINGS_DIALOG_ID);
}
@Override
protected Control createContents(final Composite parent) {
// PlatformUI.getWorkbench().getHelpSystem().setHelp(parent, "org.eclipse.statet.r.ui.remote_engine_selection_dialog"); //$NON-NLS-1$
return super.createContents(parent);
}
@Override
protected Control createDialogArea(final Composite parent) {
// page group
final Composite area= (Composite) super.createDialogArea(parent);
createMessageArea(area);
final IDialogSettings dialogSettings= getDialogSettings();
{ final Composite composite= new Composite(area, SWT.NONE);
composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
composite.setLayout(LayoutUtils.newCompositeGrid(3));
final Label label= new Label(composite, SWT.NONE);
label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false));
label.setText(Messages.RRemoteConsoleSelectionDialog_Hostname_label);
this.hostAddressControl= new Combo(composite, SWT.DROP_DOWN);
final GridData gd= new GridData(SWT.FILL, SWT.CENTER, true, false);
gd.widthHint= LayoutUtils.hintWidth(this.hostAddressControl, 50);
this.hostAddressControl.setLayoutData(gd);
final String[] history= dialogSettings.getArray(SETTINGS_HOST_HISTORY_KEY);
if (history != null) {
this.historyAddress.addAll(Arrays.asList(history));
}
this.hostAddressControl.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetDefaultSelected(final SelectionEvent e) {
update();
}
});
final Button goButton= new Button(composite, SWT.PUSH);
goButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false));
goButton.setText(Messages.RRemoteConsoleSelectionDialog_Update_label);
goButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(final SelectionEvent e) {
update();
}
});
}
{ final TreeComposite composite= new TreeComposite(area, SWT.BORDER | SWT.SINGLE | SWT.FULL_SELECTION);
final GridData gd= new GridData(SWT.FILL, SWT.FILL, true, true);
gd.heightHint= LayoutUtils.hintHeight(composite.tree, 10);
composite.setLayoutData(gd);
this.rServerViewer= composite.viewer;
composite.tree.setHeaderVisible(true);
ColumnViewerToolTipSupport.enableFor(composite.viewer);
{ final TreeViewerColumn column= new TreeViewerColumn(this.rServerViewer, SWT.NONE);
column.getColumn().setText(Messages.RRemoteConsoleSelectionDialog_Table_UserOrEngine_label);
composite.layout.setColumnData(column.getColumn(), new ColumnWeightData(1));
column.setLabelProvider(new RemoteRLabelProvider() {
@Override
public void update(final ViewerCell cell) {
final Object element= cell.getElement();
String text= null;
Image image= null;
if (element instanceof String) {
text= (String) element;
image= SharedUIResources.getImages().get(SharedUIResources.OBJ_USER_IMAGE_ID);
}
else if (element instanceof RemoteR) {
text= getText((RemoteR) element);
}
cell.setText(text);
cell.setImage(image);
}
@Override
public String getText(final RemoteR r) {
return r.info.getName();
}
});
}
{ final TreeViewerColumn column= new TreeViewerColumn(this.rServerViewer, SWT.NONE);
column.getColumn().setText(Messages.RRemoteConsoleSelectionDialog_Table_Host_label);
composite.layout.setColumnData(column.getColumn(), new ColumnWeightData(1));
column.setLabelProvider(new RemoteRLabelProvider() {
@Override
public String getText(final RemoteR r) {
return r.hostName;
}
});
}
this.rServerViewer.setContentProvider(new RemoteRContentProvider());
this.rServerViewer.getTree().addSelectionListener(new SelectionListener() {
@Override
public void widgetSelected(final SelectionEvent e) {
updateState();
}
@Override
public void widgetDefaultSelected(final SelectionEvent e) {
updateState();
if (getOkButton().isEnabled()) {
buttonPressed(IDialogConstants.OK_ID);
}
}
});
}
Dialog.applyDialogFont(area);
updateInput();
if (this.rServerList != null) {
updateStatus(new Status(IStatus.OK, RConsoleUIPlugin.BUNDLE_ID,
Messages.RRemoteConsoleSelectionDialog_info_ListRestored_message ));
}
return area;
}
private void update() {
final String input= this.hostAddressControl.getText();
this.rServerList= null;
final AtomicReference<IStatus> status= new AtomicReference<>();
if (input != null && input.length() > 0) {
try {
new ProgressMonitorDialog(getShell()).run(true, true, new IRunnableWithProgress() {
@Override
public void run(final IProgressMonitor monitor) throws InvocationTargetException {
status.set(updateRServerList(input, monitor));
}
});
}
catch (final InvocationTargetException e) {
// not used
}
catch (final InterruptedException e) {
this.rServerViewer= null;
status.compareAndSet(null, Status.CANCEL_STATUS);
}
}
if (status.get() != null) {
updateStatus(status.get());
}
getOkButton().setEnabled(false);
if (this.rServerList != null && this.rServerList.size() > 0) {
if (!this.specialAddress.containsKey(input)) {
this.historyAddress.remove(input);
this.historyAddress.add(0, input);
}
if (this.filterOnlyRunning) {
for (final Iterator<RemoteR> iter= this.rServerList.iterator(); iter.hasNext();) {
switch (iter.next().info.getState()) {
case Server.S_NOT_STARTED:
case Server.S_STOPPED:
iter.remove();
}
}
}
updateInput();
return;
}
else {
this.rServerViewer.setInput(null);
}
}
private void updateInput() {
final String selectedAddress= this.hostAddressControl.getText();
final List<String> list= new ArrayList<>(this.historyAddress.size() + this.additionalAddress.size());
list.addAll(this.historyAddress);
for (final String address : this.additionalAddress) {
if (!list.contains(address)) {
list.add(address);
}
}
this.hostAddressControl.setItems(list.toArray(new String[list.size()]));
this.rServerViewer.setInput(this.rServerList);
if (this.username != null && this.username.length() > 0) {
Display.getCurrent().asyncExec(new Runnable() {
@Override
public void run() {
if (RRemoteConsoleSelectionDialog.this.initialAddress != null) {
RRemoteConsoleSelectionDialog.this.hostAddressControl.setText(RRemoteConsoleSelectionDialog.this.initialAddress);
RRemoteConsoleSelectionDialog.this.initialAddress= null;
}
else if (selectedAddress != null && selectedAddress.length() > 0) {
RRemoteConsoleSelectionDialog.this.hostAddressControl.setText(selectedAddress);
}
else if (RRemoteConsoleSelectionDialog.this.hostAddressControl.getItemCount() > 0) {
RRemoteConsoleSelectionDialog.this.hostAddressControl.select(0);
}
RRemoteConsoleSelectionDialog.this.rServerViewer.expandToLevel(RRemoteConsoleSelectionDialog.this.username.toLowerCase(), 1);
updateState();
}
});
}
}
private void updateState() {
final IStructuredSelection selection= (IStructuredSelection) this.rServerViewer.getSelection();
getOkButton().setEnabled(selection.getFirstElement() instanceof RemoteR);
}
@Override
protected void computeResult() {
final IStructuredSelection selection= (IStructuredSelection) this.rServerViewer.getSelection();
final Object element= selection.getFirstElement();
if (element instanceof RemoteR) {
setSelectionResult(new Object[] { ((RemoteR) element).address });
}
}
@Override
public boolean close() {
final IDialogSettings dialogSettings= getDialogSettings();
dialogSettings.put(SETTINGS_HOST_HISTORY_KEY, this.historyAddress.toArray(new String[this.historyAddress.size()]));
return super.close();
}
private IStatus updateRServerList(final String combined, final IProgressMonitor monitor) {
final List<RemoteR> infos= new ArrayList<>();
final String[] addresses= ADDRESS_MULTI_PATTERN.split(combined, -1);
if (addresses.length == 0) {
return null;
}
final SubMonitor m= SubMonitor.convert(monitor, Messages.RRemoteConsoleSelectionDialog_task_Gathering_message, addresses.length*2 +2);
String failedHosts= null;
final List<IStatus> failedStatus= new ArrayList<>();
m.worked(1);
// Collect R engines for each address
for (int i= 0; i < addresses.length; i++) {
m.setWorkRemaining((addresses.length - i) * 2 + 1);
String address= addresses[i];
final SpecialAddress special= this.specialAddress.get(address);
if (special == null) {
if (address.startsWith("rmi:")) { //$NON-NLS-1$
address= address.substring(4);
}
if (address.startsWith("//")) { //$NON-NLS-1$
address= address.substring(2);
}
}
if (address.isEmpty()) {
return null;
}
if (monitor.isCanceled()) {
return Status.CANCEL_STATUS;
}
IStatus status;
if (special == null) {
final Matcher matcher= ADDRESS_WITH_PORT_PATTERN.matcher(address);
if (matcher.matches()) {
status= collectServerInfos(matcher.group(1), Integer.parseInt(matcher.group(2)), null,
infos, m.newChild(1) );
}
else {
status= collectServerInfos(address, Registry.REGISTRY_PORT, null,
infos, m.newChild(1) );
}
}
else {
address= special.rmiAddress.getAddress();
status= collectServerInfos(null, 0, special, infos, m.newChild(1));
}
switch (status.getSeverity()) {
case IStatus.CANCEL:
return status;
case IStatus.ERROR:
StatusManager.getManager().handle(status, StatusManager.LOG);
return status;
case IStatus.WARNING:
failedStatus.add(status);
failedHosts= (failedHosts == null) ? address : (failedHosts + ", " + address); //$NON-NLS-1$
continue;
default:
continue;
}
}
if (!failedStatus.isEmpty()) {
StatusManager.getManager().handle(new MultiStatus(RConsoleUIPlugin.BUNDLE_ID, 0,
failedStatus.toArray(new IStatus[failedStatus.size()]),
"Info about connection failures when browsing R engines:", null), //$NON-NLS-1$
StatusManager.LOG );
}
if (!infos.isEmpty() || failedStatus.isEmpty() ) {
this.rServerList= infos;
}
if (failedHosts != null) {
return new Status(IStatus.WARNING, RConsoleUIPlugin.BUNDLE_ID,
Messages.RRemoteConsoleSelectionDialog_error_ConnectionFailed_message+failedHosts );
}
return Status.OK_STATUS;
}
private static IStatus collectServerInfos(String address, int port,
final SpecialAddress special,
final List<RemoteR> infos, final SubMonitor m) {
try {
if (special != null) {
address= special.rmiAddress.getAddress();
port= special.rmiAddress.getPort().get();
}
m.subTask(NLS.bind(Messages.RRemoteConsoleSelectionDialog_task_Resolving_message, address));
final InetAddress inetAddress= InetAddress.getByName(address);
final String hostname= inetAddress.getHostName();
final String hostip= inetAddress.getHostAddress();
m.worked(1);
if (m.isCanceled()) {
return Status.CANCEL_STATUS;
}
m.subTask(NLS.bind(Messages.RRemoteConsoleSelectionDialog_task_Connecting_message, hostname));
final Registry registry;
if (special != null) {
final RMIClientSocketFactory socketFactory= special.getSocketFactory(m.newChild(5));
RjsComConfig.setRMIClientSocketFactory(socketFactory);
registry= LocateRegistry.getRegistry(special.privateHost, port, socketFactory );
}
else {
RjsComConfig.setRMIClientSocketFactory(null);
registry= LocateRegistry.getRegistry(address, port);
}
final String rmiBase= (port == Registry.REGISTRY_PORT) ?
"//" + address + '/' : //$NON-NLS-1$
"//" + address + ':' + port + '/'; //$NON-NLS-1$
final String[] names= registry.list();
for (final String name : names) {
try {
final Remote remote= registry.lookup(name);
if (remote instanceof Server) {
final Server server= (Server) remote;
final ServerInfo info= server.getInfo();
final String rmiAddress= rmiBase+name;
final RemoteR r= new RemoteR(hostname, hostip, rmiAddress, info);
infos.add(r);
}
}
catch (final Exception e) {
}
}
return Status.OK_STATUS;
}
catch (final RemoteException e) {
return new Status(IStatus.WARNING, RConsoleUIPlugin.BUNDLE_ID, address);
}
catch (final UnknownHostException e) {
return new Status(IStatus.ERROR, RConsoleUIPlugin.BUNDLE_ID, "Unknown host: " + e.getLocalizedMessage()); //$NON-NLS-1$
}
catch (final CoreException e) {
return e.getStatus();
}
finally {
RjsComConfig.clearRMIClientSocketFactory();
}
}
}