blob: dae40eb8c3ef35ee86944cac3e0e59c214bfd398 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2017, 2019 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.apps.ui.launching;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import java.util.Objects;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.variables.IStringVariable;
import org.eclipse.osgi.util.NLS;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IViewReference;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.browser.IWebBrowser;
import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
import org.eclipse.ui.statushandlers.StatusManager;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import org.eclipse.statet.jcommons.collections.CopyOnWriteIdentityListSet;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.jcommons.status.ErrorStatus;
import org.eclipse.statet.jcommons.status.InfoStatus;
import org.eclipse.statet.jcommons.status.ProgressMonitor;
import org.eclipse.statet.jcommons.status.Status;
import org.eclipse.statet.jcommons.status.StatusException;
import org.eclipse.statet.jcommons.ts.core.RunnableStatus;
import org.eclipse.statet.jcommons.ts.core.Tool;
import org.eclipse.statet.jcommons.ts.core.ToolRunnable;
import org.eclipse.statet.ecommons.runtime.core.util.StatusUtils;
import org.eclipse.statet.ecommons.ui.mpbv.BrowserSession;
import org.eclipse.statet.ecommons.ui.util.UIAccess;
import org.eclipse.statet.ecommons.variables.core.StaticVariable;
import org.eclipse.statet.ecommons.variables.core.VariableText2;
import org.eclipse.statet.ecommons.variables.core.VariableUtils;
import org.eclipse.statet.internal.r.apps.ui.Messages;
import org.eclipse.statet.internal.r.apps.ui.RAppUIPlugin;
import org.eclipse.statet.internal.r.apps.ui.variables.AppVarView;
import org.eclipse.statet.internal.r.apps.ui.viewer.AppBrowserSession;
import org.eclipse.statet.internal.r.apps.ui.viewer.AppBrowserView;
import org.eclipse.statet.nico.core.runtime.Queue;
import org.eclipse.statet.nico.core.runtime.ToolController;
import org.eclipse.statet.nico.ui.NicoUI;
import org.eclipse.statet.nico.ui.NicoUITools;
import org.eclipse.statet.r.apps.ui.AppRegistry;
import org.eclipse.statet.r.apps.ui.RApp;
import org.eclipse.statet.r.apps.ui.VariablesData;
import org.eclipse.statet.r.console.core.RConsoleTool;
import org.eclipse.statet.r.console.core.RProcess;
import org.eclipse.statet.r.console.core.RWorkspace;
import org.eclipse.statet.r.console.core.util.RCodeVariableText;
import org.eclipse.statet.r.core.tool.AbstractStatetRRunnable;
import org.eclipse.statet.r.core.tool.IRConsoleService;
import org.eclipse.statet.r.nico.impl.RjsUtil;
import org.eclipse.statet.rj.ts.core.AbstractRToolRunnable;
import org.eclipse.statet.rj.ts.core.RToolService;
@NonNullByDefault
public class AppRunner extends AbstractStatetRRunnable implements RApp {
public static final String RUN_TASK_ID= "org.eclipse.statet.r.apps/RunApp"; //$NON-NLS-1$
public static final String STOP_TASK_ID= "org.eclipse.statet.r.apps/StopApp"; //$NON-NLS-1$
private static final String LOCALHOST= "127.0.0.1"; //$NON-NLS-1$
private static final Status NOT_RUNNING_DATA_STATUS= new InfoStatus(RAppUIPlugin.BUNDLE_ID,
"The app is not running." );
private static final Status NOT_LOADED_DATA_STATUS= new InfoStatus(RAppUIPlugin.BUNDLE_ID,
"Variabes are not yet available." );
public static RProcess fetchRProcess(final IWorkbenchPage page) throws CoreException {
final Tool tool= NicoUI.getToolRegistry().getActiveToolSession(page).getTool();
NicoUITools.accessTool(RConsoleTool.TYPE, tool);
return (RProcess)tool;
}
private class AppSession {
private final RProcess rProcess;
private Queue.Section queueSection;
private String host;
private String remoteHost;
private int remotePort;
private @Nullable Session sshSession;
private int sshLocalPort= -1;
private @Nullable URL localUrl;
private @Nullable URL idUrl;
private boolean isRunning;
private long startedTimestamp;
public AppSession(final RProcess tool) {
this.rProcess= tool;
}
public RProcess getTool() {
return this.rProcess;
}
public boolean isRunning() {
return this.isRunning;
}
public void init(final ProgressMonitor m) throws StatusException {
{ this.queueSection= this.rProcess.getController().getCurrentQueueSection();
}
{ final String host= AppRunner.this.config.getAppHost();
if (host.isEmpty()) {
final RWorkspace workspaceData= this.rProcess.getWorkspaceData();
if (workspaceData.isRemote()) {
this.host= workspaceData.getRemoteAddress();
final Map<String, Object> connectionInfo= this.rProcess.getConnectionInfo();
if (connectionInfo != null && Objects.equals(connectionInfo.get("protocol"), "ssh")) {
this.remoteHost= LOCALHOST;
this.sshSession= RjsUtil.getSession(connectionInfo, m);
}
}
else {
this.host= LOCALHOST;
}
}
else {
this.host= host;
}
if (this.remoteHost == null) {
this.remoteHost= this.host;
}
this.remotePort= AppRunner.this.config.getAppPort();
}
}
public String getStartCode() throws StatusException {
{ final String code= AppRunner.this.config.getStartCode();
final Map<String, IStringVariable> variables= AppRunner.this.config.getVariables();
VariableUtils.add(variables, new StaticVariable(
AppControlConfigs.APP_HOST_VAR,
this.remoteHost ));
VariableUtils.add(variables, new StaticVariable(
AppControlConfigs.APP_PORT_VAR,
(this.remotePort > 0) ? Integer.toString(this.remotePort) : "NULL" )); //$NON-NLS-1$
final VariableText2 variableText= new RCodeVariableText(
this.rProcess.getWorkspaceData(), variables );
try {
return variableText.performStringSubstitution(code, null);
}
catch (final CoreException e) {
throw new StatusException(new ErrorStatus(RAppUIPlugin.BUNDLE_ID,
NLS.bind(Messages.Operation_StartApp_RCode_error_SpecInvalid_message,
e.getMessage() )));
}
}
}
public boolean onStarted(final String url) {
try {
final URL rUrl;
try {
rUrl= new URL(url);
}
catch (final MalformedURLException e) {
throw new StatusException(new ErrorStatus(RAppUIPlugin.BUNDLE_ID,
NLS.bind("Invalid URL of the R app from R= ''{0}''.", url),
e ));
}
if (this.sshSession != null) {
try {
this.sshLocalPort= this.sshSession.setPortForwardingL(0, LOCALHOST,
(rUrl.getPort() != -1) ? rUrl.getPort() : 80 );
this.localUrl= new URL(rUrl.getProtocol(), LOCALHOST, this.sshLocalPort,
rUrl.getFile() );
this.idUrl= new URL(rUrl.getProtocol(), this.host, rUrl.getPort(),
rUrl.getFile() );
}
catch (final JSchException e) {
throw new StatusException(new ErrorStatus(RAppUIPlugin.BUNDLE_ID,
"Failed create SSH tunnel for http connection of the R app.",
e ));
}
}
else if (!this.host.equals(rUrl.getHost())) {
this.localUrl= new URL(rUrl.getProtocol(), this.host, rUrl.getPort(),
rUrl.getFile() );
this.idUrl= this.localUrl;
}
else {
this.localUrl= rUrl;
this.idUrl= this.localUrl;
}
}
catch (final Exception e) {
onError(IStatus.ERROR, "An error occurred when preparing to show the R app.", e,
(AppRunner.this.config.getViewerId() != null) ?
StatusManager.LOG | StatusManager.SHOW :
StatusManager.LOG );
return false;
}
synchronized (AppRunner.this) {
this.isRunning= true;
this.startedTimestamp= System.nanoTime();
return true;
}
}
public URL getLocalUrl() {
return this.localUrl;
}
public URL getIdUrl() {
return this.idUrl;
}
public void onAppStop() {
synchronized (AppRunner.this) {
this.isRunning= false;
}
this.queueSection= null;
if (this.sshSession != null && this.sshLocalPort > 0) {
try {
this.sshSession.delPortForwardingL(this.sshLocalPort);
}
catch (final JSchException e) {
RAppUIPlugin.logError("Failed delete SSH tunnel for http connection of the R app.",
e );
}
this.sshLocalPort= -1;
}
}
}
public class StopRunnable extends AbstractRToolRunnable {
public StopRunnable() {
super(STOP_TASK_ID, "Stop R App"); //$NON-NLS-1$
}
@Override
public boolean changed(final int event, final Tool tool) {
switch (event) {
case MOVING_FROM:
return false;
default:
return true;
}
}
@Override
protected void run(final RToolService r,
final ProgressMonitor m) throws StatusException {
AppSession session;
synchronized (AppRunner.this) {
session= AppRunner.this.session;
if (session == null || !session.isRunning()) {
return;
}
}
final String code= AppRunner.this.config.getStopCode();
r.evalVoid(code, m);
}
}
private final AppControlLaunchConfig config;
private final CopyOnWriteIdentityListSet<Listener> listeners= new CopyOnWriteIdentityListSet<>();
private @Nullable AppSession session;
private @Nullable RProcess tool;
private @Nullable IWorkbenchPage workbenchPage;
private @Nullable DataLoader variablesLoader;
private @Nullable volatile VariablesData variablesData;
public AppRunner(final AppControlLaunchConfig config) {
super(RUN_TASK_ID, NLS.bind("Run R App ''{0}''",
config.getAppFolder().getFullPath().toString() ));
this.config= config;
initVars();
}
@Override
public IResource getResource() {
return this.config.getAppFolder();
}
@Override
protected void run(final IRConsoleService r,
final ProgressMonitor m) throws StatusException {
final AppSession session;
AppRCommandHandler listener= null;
try {
synchronized (this) {
this.tool= (RProcess) r.getTool();
session= new AppSession(this.tool);
this.session= session;
}
session.init(m);
listener= AppRCommandHandler.connect(this, r, m);
r.briefAboutToChange();
r.submitToConsole(session.getStartCode(), m);
}
finally {
r.briefChanged(IRConsoleService.AUTO_CHANGE);
onAppStopped(null);
if (listener != null) {
listener.disconnect(this);
}
}
}
protected void onAppStarted(final String url, final String typeId) {
final AppSession session;
synchronized (this) {
session= this.session;
if (session == null) {
return;
}
}
final boolean isRunning= session.onStarted(url);
updateVarsOnStarted();
if (isRunning) {
AppRegistry.getInstance().onAppStarted(session.getIdUrl(), this);
if (this.config.getViewerId() != null) {
showViewer(session, this.config.getViewerId());
}
if (this.config.getVariablesViewAction() != 0 && this.variablesData != null) {
showVariablesView(session, this.config.getVariablesViewAction());
}
refreshVariables();
}
}
protected void onAppStopped(final @Nullable String url) {
final AppSession session;
synchronized (this) {
session= this.session;
if (session == null) {
return;
}
this.session= null;
}
session.onAppStop();
if (session.getIdUrl() != null) {
AppRegistry.getInstance().onAppStopped(session.getIdUrl(), this);
}
updateVarsOnStopped();
}
@Override
public @Nullable RProcess getTool() {
return this.tool;
}
protected IWorkbenchPage getWorkbenchPage() {
IWorkbenchPage page= this.workbenchPage;
if (page == null) {
page= this.config.getWorkbenchPage();
}
if (page != null && page.getWorkbenchWindow().getActivePage() == page) {
return page;
}
page= UIAccess.getActiveWorkbenchPage(true);
this.workbenchPage= page;
return page;
}
@Override
public boolean isRunning() {
final AppSession session;
synchronized (this) {
session= this.session;
return (session != null && session.isRunning());
}
}
long getStartedTimestamp() {
final AppSession session;
synchronized (this) {
session= this.session;
return (session != null && session.isRunning()) ? session.startedTimestamp : Long.MIN_VALUE;
}
}
@Override
public void startApp(final IWorkbenchPage page) throws CoreException {
final RProcess tool= fetchRProcess(page);
final Status status= tryStart(tool);
if (status.getSeverity() < Status.ERROR) {
return;
}
throw new CoreException(StatusUtils.convert(status));
}
@Override
public boolean canRestartApp() { // (!isRunning() || canStopApp())
final AppSession session;
synchronized (this) {
session= this.session;
if (session == null || !session.isRunning()) {
return true;
}
}
return (this.config.getStopCode() != null);
}
@Override
public void restartApp(final IWorkbenchPage page) throws CoreException {
final AppSession session;
RProcess tool;
Queue.Section queueSection;
synchronized (this) {
tool= this.tool;
session= this.session;
queueSection= (session != null && session.isRunning()) ? session.queueSection : null;
}
final AppRunner runner= new AppRunner(this.config);
runner.workbenchPage= this.workbenchPage;
Status status= null;
if (tool != null) {
if (queueSection != null && queueSection != tool.getQueue().getTopLevelSection()) {
final Status status0= runner.tryRestart(tool, queueSection);
if (status0.getSeverity() < Status.ERROR) {
return;
}
}
{ final Status status0= runner.tryRestart(tool, tool.getQueue().getTopLevelSection());
if (status0.getSeverity() < Status.ERROR) {
return;
}
if (status == null) {
status= status0;
}
}
}
{ tool= fetchRProcess(page);
final Status status0= runner.tryRestart(tool, tool.getQueue().getTopLevelSection());
if (status0.getSeverity() < Status.ERROR) {
return;
}
if (status == null) {
status= status0;
}
}
throw new CoreException(StatusUtils.convert(status));
}
private Status tryStart(final RProcess tool) {
final Queue queue= tool.getQueue();
final Status status= queue.add(this);
if (status.getSeverity() < Status.ERROR) {
stopBlocking(tool, this.config.getStopBlocking());
}
return status;
}
private Status tryRestart(final RProcess tool, final Queue.Section queueSection) {
final Queue queue= tool.getQueue();
final Status status= queue.add(this, queueSection, Queue.IF_ABSENT);
if (status.getSeverity() < Status.ERROR) {
final AppRunner runner= (AppRunner) ((RunnableStatus) status).getRunnable();
runner.stopBlocking(tool, this.config.getStopBlocking() | 2);
}
return status;
}
private void stopBlocking(final RProcess tool, final int mode) {
if (mode == 0) {
return;
}
final ToolController controller= tool.getController();
if (controller != null) {
final ToolRunnable currentRunnable= controller.getCurrentRunnable();
if (currentRunnable != this && currentRunnable instanceof AppRunner) {
final AppRunner runner= (AppRunner) currentRunnable;
if ((mode & 1) != 0
|| ((mode & 2) != 0 && runner.config == this.config) ) {
runner.stopApp();
}
}
}
}
@Override
public boolean canStopApp() {
final AppSession session;
synchronized (this) {
session= this.session;
if (session == null || !session.isRunning()) {
return false;
}
}
return (this.config.getStopCode() != null);
}
@Override
public void stopApp() {
final AppSession session;
synchronized (this) {
session= this.session;
if (session == null) {
return;
}
}
if (this.config.getStopCode() != null) {
session.getTool().getQueue().addHot(new StopRunnable());
}
}
private void showViewer(final AppSession session, final String viewerId) {
if (session.getLocalUrl() == null) {
onError(IStatus.ERROR, "Cannot open viewer to show the R app: URL is missing.", null,
StatusManager.SHOW );
return;
}
UIAccess.getDisplay().asyncExec(() -> {
synchronized (AppRunner.this) {
if (session != this.session || !session.isRunning()) {
return;
}
}
try {
switch (viewerId) {
case AppControlConfigs.WORKBENCH_EXTERNAL_BROWSER_ID:
openExternalBrowser(session);
return;
case AppControlConfigs.WORKBENCH_VIEW_BROWSER_ID:
openViewBrowser(getWorkbenchPage(), session);
return;
default:
throw new UnsupportedOperationException("viewerId= " + viewerId); //$NON-NLS-1$
}
}
catch (final Exception e) {
onError(IStatus.ERROR, Messages.Operation_Viewer_error_Run_message, e,
StatusManager.LOG | StatusManager.SHOW );
}
});
}
private void openExternalBrowser(final AppSession session) throws PartInitException {
final IWorkbenchBrowserSupport browserSupport= PlatformUI.getWorkbench().getBrowserSupport();
final IWebBrowser webBrowser= browserSupport.getExternalBrowser();
webBrowser.openURL(session.getLocalUrl());
}
private AppBrowserView getView(final IWorkbenchPage page) throws PartInitException {
return (AppBrowserView) page.showView(AppBrowserView.VIEW_ID, null,
IWorkbenchPage.VIEW_VISIBLE );
}
private void openViewBrowser(final IWorkbenchPage page, final AppSession session) throws PartInitException {
final AppBrowserView view= getView(page);
AppBrowserSession viewSession= (AppBrowserSession) BrowserSession.findSessionById(
view.getSessions(), session.getIdUrl() );
if (viewSession == null) {
viewSession= new AppBrowserSession(session.getIdUrl());
}
view.openUrl(session.getLocalUrl().toExternalForm(), viewSession);
}
private void showVariablesView(final AppSession session, final int viewActionMode) {
UIAccess.getDisplay().asyncExec(() -> {
synchronized (AppRunner.this) {
if (session != this.session || !session.isRunning()) {
return;
}
}
try {
final IWorkbenchPage page= getWorkbenchPage();
final IViewReference viewRef= page.findViewReference(AppVarView.VIEW_ID);
if (viewRef != null) {
if (viewRef.isFastView()) {
return;
}
final IViewPart view= viewRef.getView(false);
if (view != null && page.isPartVisible(view)) {
return;
}
}
final AppVarView view= (AppVarView) page.showView(AppVarView.VIEW_ID, null,
viewActionMode );
view.setShownByLauncher(this);
}
catch (final PartInitException e) {
onError(IStatus.ERROR, Messages.Operation_Variables_error_Run_message, e);
}
});
}
@Override
public void addListener(final Listener listener) {
boolean refresh;
synchronized (this.listeners) {
refresh= (this.listeners.isEmpty() && this.variablesLoader == null);
this.listeners.add(listener);
}
if (refresh) {
refreshVariables();
}
}
@Override
public void removeListener(final Listener listener) {
this.listeners.remove(listener);
}
@Override
public @Nullable VariablesData getVariables() {
return this.variablesData;
}
private void initVars() {
final String code= this.config.getVariablesCode();
if (code == null) {
return;
}
this.variablesData= new VariablesData(code, NOT_RUNNING_DATA_STATUS);
}
private void updateVarsOnStarted() {
final VariablesData data= this.variablesData;
if (data == null || data.getStatus() != NOT_RUNNING_DATA_STATUS) {
return;
}
setData(new VariablesData(data.getExpression(), NOT_LOADED_DATA_STATUS));
}
@Override
public void refreshVariables() {
final VariablesData data= this.variablesData;
if (data == null) {
return;
}
DataLoader loader;
synchronized (this.listeners) {
loader= this.variablesLoader;
if (loader == null) {
if (this.listeners.isEmpty() || !isRunning()) {
return;
}
loader= new DataLoader(this, data.getExpression());
this.variablesLoader= loader;
}
}
loader.schedule();
}
private void updateVarsOnStopped() {
final VariablesData data= this.variablesData;
if (data == null) {
return;
}
DataLoader loader;
synchronized (this.listeners) {
loader= this.variablesLoader;
this.variablesLoader= null;
}
if (loader != null) {
loader.stop();
}
setData(new VariablesData(data.getExpression(), NOT_RUNNING_DATA_STATUS));
}
void setData(final VariablesData variables) {
this.variablesData= variables;
final AppEvent event= new AppEvent(this);
for (final Listener listener : this.listeners) {
listener.onVariablesChanged(event);
}
}
private void onError(final int severity, final String message, final @Nullable Throwable e,
final int style) {
StatusManager.getManager().handle(new org.eclipse.core.runtime.Status(
severity, RAppUIPlugin.BUNDLE_ID, message, e ),
style );
}
private void onError(final int severity, final String message, final @Nullable Throwable e) {
onError(severity, message, e, StatusManager.LOG);
}
@Override
public int hashCode() {
return this.config.hashCode();
}
@Override
public boolean equals(final @Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof AppRunner) {
return (this.config == ((AppRunner) obj).config);
}
return false;
}
}