/* * ShinyApplication.java * * Copyright (C) 2009-15 by RStudio, Inc. * * Unless you have received this program directly from RStudio pursuant * to the terms of a commercial license agreement with RStudio, then * this program is licensed to you under the terms of version 3 of the * GNU Affero General Public License. This program is distributed WITHOUT * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. * */ package org.rstudio.studio.client.shiny; import org.rstudio.core.client.BrowseCap; import org.rstudio.core.client.Size; import org.rstudio.core.client.command.CommandBinder; import org.rstudio.core.client.command.Handler; import org.rstudio.core.client.dom.WindowCloseMonitor; import org.rstudio.core.client.dom.WindowEx; import org.rstudio.studio.client.application.ApplicationInterrupt; import org.rstudio.studio.client.application.Desktop; import org.rstudio.studio.client.application.ApplicationInterrupt.InterruptHandler; import org.rstudio.studio.client.application.events.EventBus; import org.rstudio.studio.client.application.events.RestartStatusEvent; import org.rstudio.studio.client.common.GlobalDisplay; import org.rstudio.studio.client.common.dependencies.DependencyManager; import org.rstudio.studio.client.common.satellite.SatelliteManager; import org.rstudio.studio.client.common.satellite.events.WindowClosedEvent; import org.rstudio.studio.client.common.shiny.model.ShinyServerOperations; import org.rstudio.studio.client.server.ServerError; import org.rstudio.studio.client.server.ServerRequestCallback; import org.rstudio.studio.client.shiny.events.LaunchShinyApplicationEvent; import org.rstudio.studio.client.shiny.events.ShinyApplicationStatusEvent; import org.rstudio.studio.client.shiny.model.ShinyApplicationParams; import org.rstudio.studio.client.shiny.model.ShinyRunCmd; import org.rstudio.studio.client.shiny.model.ShinyViewerType; import org.rstudio.studio.client.workbench.commands.Commands; import org.rstudio.studio.client.workbench.prefs.model.UIPrefs; import org.rstudio.studio.client.workbench.views.console.events.ConsoleBusyEvent; import org.rstudio.studio.client.workbench.views.console.events.SendToConsoleEvent; import org.rstudio.studio.client.workbench.views.environment.events.DebugModeChangedEvent; import org.rstudio.studio.client.workbench.views.viewer.events.ViewerClearedEvent; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.user.client.Command; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; @Singleton public class ShinyApplication implements ShinyApplicationStatusEvent.Handler, ConsoleBusyEvent.Handler, DebugModeChangedEvent.Handler, RestartStatusEvent.Handler, WindowClosedEvent.Handler, LaunchShinyApplicationEvent.Handler { public interface Binder extends CommandBinder<Commands, ShinyApplication> {} @Inject public ShinyApplication(EventBus eventBus, Commands commands, Binder binder, Provider<UIPrefs> pPrefs, final SatelliteManager satelliteManager, ShinyServerOperations server, GlobalDisplay display, DependencyManager dependencyManager, ApplicationInterrupt interrupt) { eventBus_ = eventBus; satelliteManager_ = satelliteManager; commands_ = commands; pPrefs_ = pPrefs; server_ = server; display_ = display; isBusy_ = false; currentViewType_ = ShinyViewerType.SHINY_VIEWER_NONE; dependencyManager_ = dependencyManager; interrupt_ = interrupt; eventBus_.addHandler(ShinyApplicationStatusEvent.TYPE, this); eventBus_.addHandler(LaunchShinyApplicationEvent.TYPE, this); eventBus_.addHandler(ConsoleBusyEvent.TYPE, this); eventBus_.addHandler(DebugModeChangedEvent.TYPE, this); eventBus_.addHandler(RestartStatusEvent.TYPE, this); eventBus_.addHandler(WindowClosedEvent.TYPE, this); binder.bind(commands, this); exportShinyAppClosedCallback(); } // Event handlers ---------------------------------------------------------- @Override public void onShinyApplicationStatus(ShinyApplicationStatusEvent event) { if (event.getParams().getState() == ShinyApplicationParams.STATE_STARTED) { currentViewType_ = event.getParams().getViewerType(); // open the window to view the application if needed if (currentViewType_ == ShinyViewerType.SHINY_VIEWER_WINDOW) { activateWindow(event.getParams()); } else if (currentViewType_ == ShinyViewerType.SHINY_VIEWER_BROWSER) { display_.openWindow(event.getParams().getUrl()); } params_ = event.getParams(); // if the app was started from the same path as a pending satellite // closure, don't shut down the app when the close finishes if (event.getParams().getPath() == satelliteClosePath_) { stopOnNextClose_ = false; } } else if (event.getParams().getState() == ShinyApplicationParams.STATE_STOPPED) { params_ = null; } } @Override public void onLaunchShinyApplication(LaunchShinyApplicationEvent event) { launchShinyApplication(event.getPath(), event.getExtendedType()); } @Override public void onConsoleBusy(ConsoleBusyEvent event) { isBusy_ = event.isBusy(); // if the browser is up and R stops being busy, presume it's because the // app has stopped if (!isBusy_ && params_ != null && params_.getViewerType() == ShinyViewerType.SHINY_VIEWER_BROWSER) { params_.setState(ShinyApplicationParams.STATE_STOPPED); eventBus_.fireEvent(new ShinyApplicationStatusEvent(params_)); } } @Override public void onDebugModeChanged(DebugModeChangedEvent event) { // When leaving debug mode while the Shiny application is open in a // browser, automatically return to the app by activating the window. if (!event.debugging() && params_ != null && currentViewType_ == ShinyViewerType.SHINY_VIEWER_WINDOW) { satelliteManager_.activateSatelliteWindow( ShinyApplicationSatellite.NAME); } } @Override public void onRestartStatus(RestartStatusEvent event) { // Close the satellite window when R restarts, since this leads to the // Shiny server being terminated. Closing the window triggers a // ShinyApplicationStatusEvent that allows the rest of the UI a chance // to react to the app's termination. if (event.getStatus() == RestartStatusEvent.RESTART_INITIATED && currentViewType_ == ShinyViewerType.SHINY_VIEWER_WINDOW) { satelliteManager_.closeSatelliteWindow( ShinyApplicationSatellite.NAME); } } @Override public void onWindowClosed(WindowClosedEvent event) { // we get this event on the desktop (currently only Cocoa); it lets us // know that the satellite has been shut down even in the case where the // script window that ordinarily would let us know has been disconnected. if (!event.getName().equals(ShinyApplicationSatellite.NAME)) return; // stop the app if this event wasn't generated by a disconnect if (params_ != null && disconnectingUrl_ == null && stopOnNextClose_) { params_.setState(ShinyApplicationParams.STATE_STOPPING); notifyShinyAppClosed(params_); } } // Command handlers -------------------------------------------------------- @Handler public void onShinyRunInPane() { setShinyViewerType(ShinyViewerType.SHINY_VIEWER_PANE); } @Handler public void onShinyRunInViewer() { setShinyViewerType(ShinyViewerType.SHINY_VIEWER_WINDOW); } @Handler public void onShinyRunInBrowser() { setShinyViewerType(ShinyViewerType.SHINY_VIEWER_BROWSER); } // Public methods ---------------------------------------------------------- // Private methods --------------------------------------------------------- private void launchShinyApplication(final String filePath, final String extendedType) { String fileDir = filePath.substring(0, filePath.lastIndexOf("/")); if (fileDir.equals(currentAppPath())) { // The app being launched is the one already running; open and // reload the app. if (currentViewType_ == ShinyViewerType.SHINY_VIEWER_WINDOW) { satelliteManager_.dispatchCommand(commands_.reloadShinyApp(), ShinyApplicationSatellite.NAME); activateWindow(); } else if (currentViewType_ == ShinyViewerType.SHINY_VIEWER_PANE && commands_.viewerRefresh().isEnabled()) { commands_.viewerRefresh().execute(); } else if (currentViewType_ == ShinyViewerType.SHINY_VIEWER_BROWSER) { eventBus_.fireEvent(new ShinyApplicationStatusEvent(params_)); } return; } else if (params_ != null && isBusy_) { // There's another app running. Interrupt it and then start this one. interrupt_.interruptR(new InterruptHandler() { @Override public void onInterruptFinished() { launchShinyFile(filePath, extendedType); } }); } else { // Nothing else running, start this app. dependencyManager_.withShiny("Running Shiny applications", new Command() { @Override public void execute() { launchShinyFile(filePath, extendedType); } }); } } private String currentAppPath() { if (params_ != null) return params_.getPath(); return null; } private void notifyShinyAppDisconnected(JavaScriptObject params) { ShinyApplicationParams appState = params.cast(); if (params_ == null) return; // remember that this URL is disconnecting (so we don't interrupt R when // the window is torn down) disconnectingUrl_ = appState.getUrl(); } private void notifyShinyAppClosed(final JavaScriptObject params) { // if we don't know that an app is running, ignore this event if (params_ == null) return; satelliteClosePath_ = params_.getPath(); // wait for confirmation of window closure (could be a reload) WindowCloseMonitor.monitorSatelliteClosure( ShinyApplicationSatellite.NAME, new Command() { @Override public void execute() { // satellite closed for real; shut down the app satelliteClosePath_ = null; onShinyApplicationClosed(params); } }, new Command() { @Override public void execute() { // satellite didn't actually close (it was a reload) satelliteClosePath_ = null; } }); } private void onShinyApplicationClosed(JavaScriptObject params) { ShinyApplicationParams appState = params.cast(); // this completes any pending disconnection disconnectingUrl_ = null; // if we were asked not to stop when the window closes (i.e. when // changing viewer types), bail out if (!stopOnNextClose_) { stopOnNextClose_ = true; return; } // If the application is stopping, then the user initiated the stop by // closing the app window. Interrupt R to stop the Shiny app. if (appState.getState().equals(ShinyApplicationParams.STATE_STOPPING)) { if (commands_.interruptR().isEnabled()) commands_.interruptR().execute(); appState.setState(ShinyApplicationParams.STATE_STOPPED); } eventBus_.fireEvent(new ShinyApplicationStatusEvent( (ShinyApplicationParams) params.cast())); } private final native void exportShinyAppClosedCallback()/*-{ var registry = this; $wnd.notifyShinyAppClosed = $entry( function(params) { registry.@org.rstudio.studio.client.shiny.ShinyApplication::notifyShinyAppClosed(Lcom/google/gwt/core/client/JavaScriptObject;)(params); } ); $wnd.notifyShinyAppDisconnected = $entry( function(params) { registry.@org.rstudio.studio.client.shiny.ShinyApplication::notifyShinyAppDisconnected(Lcom/google/gwt/core/client/JavaScriptObject;)(params); } ); }-*/; private void setShinyViewerType(int viewerType) { UIPrefs prefs = pPrefs_.get(); prefs.shinyViewerType().setGlobalValue(viewerType); prefs.writeUIPrefs(); // if we have a running Shiny app and the viewer type has changed, // snap the app into the new location if (currentViewType_ != viewerType && params_ != null) { // if transitioning away from the pane or the window, close down // the old instance if (currentViewType_ == ShinyViewerType.SHINY_VIEWER_PANE || currentViewType_ == ShinyViewerType.SHINY_VIEWER_WINDOW) { if (currentViewType_ == ShinyViewerType.SHINY_VIEWER_WINDOW) { stopOnNextClose_ = false; satelliteManager_.closeSatelliteWindow( ShinyApplicationSatellite.NAME); } else { eventBus_.fireEvent(new ViewerClearedEvent(false)); } } // assign new viewer type currentViewType_ = viewerType; params_.setViewerType(viewerType); if (currentViewType_ == ShinyViewerType.SHINY_VIEWER_PANE || currentViewType_ == ShinyViewerType.SHINY_VIEWER_WINDOW || currentViewType_ == ShinyViewerType.SHINY_VIEWER_BROWSER) { eventBus_.fireEvent(new ShinyApplicationStatusEvent(params_)); } } } private void launchShinyFile(String file, String extendedType) { server_.getShinyRunCmd(file, extendedType, new ServerRequestCallback<ShinyRunCmd>() { @Override public void onResponseReceived(ShinyRunCmd cmd) { eventBus_.fireEvent( new SendToConsoleEvent(cmd.getRunCmd(), true)); } @Override public void onError(ServerError error) { display_.showErrorMessage("Shiny App Launch Failed", error.getMessage()); } }); } private void activateWindow() { activateWindow(null); } private void activateWindow(ShinyApplicationParams params) { WindowEx win = satelliteManager_.getSatelliteWindowObject( ShinyApplicationSatellite.NAME); boolean isRefresh = win != null && (params == null || (params_ != null && params.getPath().equals(params_.getPath()))); boolean isChrome = !Desktop.isDesktop() && BrowseCap.isChrome(); if (params != null) params_ = params; if (win == null || (!isRefresh && !isChrome)) { // If there's no window yet, or we're switching apps in a browser // other than Chrome, do a normal open satelliteManager_.openSatellite(ShinyApplicationSatellite.NAME, params_, new Size(960,1100)); } else if (isChrome) { // we have a window and we're Chrome, so force a close and reopen satelliteManager_.forceReopenSatellite(ShinyApplicationSatellite.NAME, params_, true); } else { satelliteManager_.activateSatelliteWindow( ShinyApplicationSatellite.NAME); } } private final EventBus eventBus_; private final SatelliteManager satelliteManager_; private final DependencyManager dependencyManager_; private final Commands commands_; private final Provider<UIPrefs> pPrefs_; private final ShinyServerOperations server_; private final GlobalDisplay display_; private final ApplicationInterrupt interrupt_; private ShinyApplicationParams params_; private String disconnectingUrl_; private boolean isBusy_; private boolean stopOnNextClose_ = true; private String satelliteClosePath_ = null; private int currentViewType_; }