1 /* 2 * PlumberAPI.java 3 * 4 * Copyright (C) 2021 by RStudio, PBC 5 * 6 * Unless you have received this program directly from RStudio pursuant 7 * to the terms of a commercial license agreement with RStudio, then 8 * this program is licensed to you under the terms of version 3 of the 9 * GNU Affero General Public License. This program is distributed WITHOUT 10 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, 11 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the 12 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. 13 * 14 */ 15 package org.rstudio.studio.client.plumber; 16 17 import org.rstudio.core.client.BrowseCap; 18 import org.rstudio.core.client.Size; 19 import org.rstudio.core.client.StringUtil; 20 import org.rstudio.core.client.command.CommandBinder; 21 import org.rstudio.core.client.command.Handler; 22 import org.rstudio.core.client.dom.WindowCloseMonitor; 23 import org.rstudio.core.client.dom.WindowEx; 24 import org.rstudio.studio.client.application.ApplicationInterrupt; 25 import org.rstudio.studio.client.application.Desktop; 26 import org.rstudio.studio.client.application.events.EventBus; 27 import org.rstudio.studio.client.application.events.InterruptStatusEvent; 28 import org.rstudio.studio.client.application.events.RestartStatusEvent; 29 import org.rstudio.studio.client.common.GlobalDisplay; 30 import org.rstudio.studio.client.common.dependencies.DependencyManager; 31 import org.rstudio.studio.client.common.plumber.model.PlumberServerOperations; 32 import org.rstudio.studio.client.common.satellite.SatelliteManager; 33 import org.rstudio.studio.client.common.satellite.events.WindowClosedEvent; 34 import org.rstudio.studio.client.plumber.events.LaunchPlumberAPIEvent; 35 import org.rstudio.studio.client.plumber.events.PlumberAPIStatusEvent; 36 import org.rstudio.studio.client.plumber.model.PlumberAPIParams; 37 import org.rstudio.studio.client.plumber.model.PlumberRunCmd; 38 import org.rstudio.studio.client.server.ServerError; 39 import org.rstudio.studio.client.server.ServerRequestCallback; 40 import org.rstudio.studio.client.workbench.commands.Commands; 41 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs; 42 import org.rstudio.studio.client.workbench.views.console.events.ConsoleBusyEvent; 43 import org.rstudio.studio.client.workbench.views.console.events.SendToConsoleEvent; 44 import org.rstudio.studio.client.workbench.views.environment.events.DebugModeChangedEvent; 45 import org.rstudio.studio.client.workbench.views.viewer.events.ViewerClearedEvent; 46 47 import com.google.gwt.core.client.JavaScriptObject; 48 import com.google.inject.Inject; 49 import com.google.inject.Provider; 50 import com.google.inject.Singleton; 51 52 @Singleton 53 public class PlumberAPI implements PlumberAPIStatusEvent.Handler, 54 ConsoleBusyEvent.Handler, 55 DebugModeChangedEvent.Handler, 56 RestartStatusEvent.Handler, 57 WindowClosedEvent.Handler, 58 LaunchPlumberAPIEvent.Handler, 59 InterruptStatusEvent.Handler 60 { 61 public interface Binder 62 extends CommandBinder<Commands, PlumberAPI> {} 63 64 @Inject PlumberAPI(EventBus eventBus, Commands commands, Binder binder, Provider<UserPrefs> pPrefs, final SatelliteManager satelliteManager, PlumberServerOperations server, GlobalDisplay display, DependencyManager dependencyManager, ApplicationInterrupt interrupt)65 public PlumberAPI(EventBus eventBus, 66 Commands commands, 67 Binder binder, 68 Provider<UserPrefs> pPrefs, 69 final SatelliteManager satelliteManager, 70 PlumberServerOperations server, 71 GlobalDisplay display, 72 DependencyManager dependencyManager, 73 ApplicationInterrupt interrupt) 74 { 75 eventBus_ = eventBus; 76 satelliteManager_ = satelliteManager; 77 commands_ = commands; 78 pPrefs_ = pPrefs; 79 server_ = server; 80 display_ = display; 81 isBusy_ = false; 82 currentViewType_ = UserPrefs.PLUMBER_VIEWER_TYPE_NONE; 83 dependencyManager_ = dependencyManager; 84 interrupt_ = interrupt; 85 86 eventBus_.addHandler(PlumberAPIStatusEvent.TYPE, this); 87 eventBus_.addHandler(LaunchPlumberAPIEvent.TYPE, this); 88 eventBus_.addHandler(ConsoleBusyEvent.TYPE, this); 89 eventBus_.addHandler(DebugModeChangedEvent.TYPE, this); 90 eventBus_.addHandler(RestartStatusEvent.TYPE, this); 91 eventBus_.addHandler(WindowClosedEvent.TYPE, this); 92 eventBus_.addHandler(InterruptStatusEvent.TYPE, this); 93 94 binder.bind(commands, this); 95 exportPlumberAPIClosedCallback(); 96 } 97 98 // Event handlers ---------------------------------------------------------- 99 100 @Override onPlumberAPIStatus(PlumberAPIStatusEvent event)101 public void onPlumberAPIStatus(PlumberAPIStatusEvent event) 102 { 103 if (StringUtil.equals(event.getParams().getState(), PlumberAPIParams.STATE_STARTED)) 104 { 105 currentViewType_ = event.getParams().getViewerType(); 106 107 // open the window to view the API if needed 108 if (currentViewType_ == UserPrefs.PLUMBER_VIEWER_TYPE_WINDOW) 109 { 110 activateWindow(event.getParams()); 111 } 112 else if (currentViewType_ == UserPrefs.PLUMBER_VIEWER_TYPE_BROWSER) 113 { 114 display_.openWindow(event.getParams().getUrl()); 115 } 116 params_ = event.getParams(); 117 118 // if the API was started from the same path as a pending satellite 119 // closure, don't shut down the API when the close finishes 120 if (StringUtil.equals(event.getParams().getPath(), satelliteClosePath_)) 121 { 122 stopOnNextClose_ = false; 123 } 124 } 125 else if (StringUtil.equals(event.getParams().getState(), PlumberAPIParams.STATE_STOPPED)) 126 { 127 params_ = null; 128 } 129 } 130 131 @Override onLaunchPlumberAPI(LaunchPlumberAPIEvent event)132 public void onLaunchPlumberAPI(LaunchPlumberAPIEvent event) 133 { 134 launchPlumberAPI(event.getPath()); 135 } 136 137 @Override onConsoleBusy(ConsoleBusyEvent event)138 public void onConsoleBusy(ConsoleBusyEvent event) 139 { 140 isBusy_ = event.isBusy(); 141 142 // if the browser is up and R stops being busy, presume it's because the 143 // API has stopped 144 if (!isBusy_ && params_ != null && 145 params_.getViewerType() == UserPrefs.PLUMBER_VIEWER_TYPE_BROWSER) 146 { 147 params_.setState(PlumberAPIParams.STATE_STOPPED); 148 eventBus_.fireEvent(new PlumberAPIStatusEvent(params_)); 149 } 150 } 151 152 @Override onDebugModeChanged(DebugModeChangedEvent event)153 public void onDebugModeChanged(DebugModeChangedEvent event) 154 { 155 // When leaving debug mode while the Plumber API is open in a 156 // browser, automatically return to the API by activating the window. 157 if (!event.debugging() && 158 params_ != null && 159 currentViewType_ == UserPrefs.PLUMBER_VIEWER_TYPE_WINDOW) 160 { 161 satelliteManager_.activateSatelliteWindow(PlumberAPISatellite.NAME); 162 } 163 } 164 165 @Override onRestartStatus(RestartStatusEvent event)166 public void onRestartStatus(RestartStatusEvent event) 167 { 168 // Close the satellite window when R restarts, since this leads to the 169 // Plumber API being terminated. Closing the window triggers a 170 // PlumberAPIStatusEvent that allows the rest of the UI a chance 171 // to react to the API's termination. 172 if (event.getStatus() == RestartStatusEvent.RESTART_INITIATED && 173 currentViewType_ == UserPrefs.PLUMBER_VIEWER_TYPE_WINDOW) 174 { 175 satelliteManager_.closeSatelliteWindow(PlumberAPISatellite.NAME); 176 } 177 } 178 179 @Override onWindowClosed(WindowClosedEvent event)180 public void onWindowClosed(WindowClosedEvent event) 181 { 182 // we get this event on the desktop (currently only Cocoa); it lets us 183 // know that the satellite has been shut down even in the case where the 184 // script window that ordinarily would let us know has been disconnected. 185 if (!StringUtil.equals(event.getName(), PlumberAPISatellite.NAME)) 186 return; 187 188 // stop the API if this event wasn't generated by a disconnect 189 if (params_ != null && disconnectingUrl_ == null && stopOnNextClose_) 190 { 191 params_.setState(PlumberAPIParams.STATE_STOPPING); 192 notifyPlumberAPIClosed(params_); 193 } 194 } 195 196 @Handler onPlumberRunInPane()197 public void onPlumberRunInPane() 198 { 199 setPlumberViewerType(UserPrefs.PLUMBER_VIEWER_TYPE_PANE); 200 } 201 202 @Handler onPlumberRunInViewer()203 public void onPlumberRunInViewer() 204 { 205 setPlumberViewerType(UserPrefs.PLUMBER_VIEWER_TYPE_WINDOW); 206 } 207 208 @Handler onPlumberRunInBrowser()209 public void onPlumberRunInBrowser() 210 { 211 setPlumberViewerType(UserPrefs.PLUMBER_VIEWER_TYPE_BROWSER); 212 } 213 214 @Override onInterruptStatus(InterruptStatusEvent event)215 public void onInterruptStatus(InterruptStatusEvent event) 216 { 217 // If API is stopped via Console, ensure Satellite is closed 218 if (currentViewType_ == UserPrefs.PLUMBER_VIEWER_TYPE_WINDOW) 219 { 220 satelliteManager_.closeSatelliteWindow(PlumberAPISatellite.NAME); 221 } 222 } 223 launchPlumberAPI(final String filePath)224 private void launchPlumberAPI(final String filePath) 225 { 226 String fileDir = filePath.substring(0, filePath.lastIndexOf("/")); 227 if (fileDir.equals(currentAppPath())) 228 { 229 // The API being launched is the one already running; open and reload the API. 230 if (currentViewType_ == UserPrefs.PLUMBER_VIEWER_TYPE_WINDOW) 231 { 232 satelliteManager_.dispatchCommand(commands_.reloadPlumberAPI(), PlumberAPISatellite.NAME); 233 activateWindow(); 234 } 235 else if (currentViewType_ == UserPrefs.PLUMBER_VIEWER_TYPE_PANE && 236 commands_.viewerRefresh().isEnabled()) 237 { 238 commands_.viewerRefresh().execute(); 239 } 240 else if (currentViewType_ == UserPrefs.PLUMBER_VIEWER_TYPE_BROWSER) 241 { 242 eventBus_.fireEvent(new PlumberAPIStatusEvent(params_)); 243 } 244 return; 245 } 246 else if (params_ != null && isBusy_) 247 { 248 // There's another API running. Interrupt it and then start this one. 249 interrupt_.interruptR(() -> launchPlumberFile(filePath)); 250 } 251 else 252 { 253 // Nothing else running, start this API. 254 dependencyManager_.withRPlumber("Running Plumber API", () -> launchPlumberFile(filePath)); 255 } 256 } 257 currentAppPath()258 private String currentAppPath() 259 { 260 if (params_ != null) 261 return params_.getPath(); 262 return null; 263 } 264 notifyPlumberAPIDisconnected(JavaScriptObject params)265 private void notifyPlumberAPIDisconnected(JavaScriptObject params) 266 { 267 PlumberAPIParams apiState = params.cast(); 268 if (params_ == null) 269 return; 270 271 // remember that this URL is disconnecting (so we don't interrupt R when 272 // the window is torn down) 273 disconnectingUrl_ = apiState.getUrl(); 274 } 275 notifyPlumberAPIClosed(final JavaScriptObject params)276 private void notifyPlumberAPIClosed(final JavaScriptObject params) 277 { 278 // if we don't know that an API is running, ignore this event 279 if (params_ == null) 280 return; 281 282 satelliteClosePath_ = params_.getPath(); 283 284 // wait for confirmation of window closure (could be a reload) 285 WindowCloseMonitor.monitorSatelliteClosure(PlumberAPISatellite.NAME, () -> { 286 // satellite closed for real; shut down the API 287 satelliteClosePath_ = null; 288 onPlumberAPIClosed(params); 289 }, () -> { 290 // satellite didn't actually close (it was a reload) 291 satelliteClosePath_ = null; 292 }); 293 } 294 onPlumberAPIClosed(JavaScriptObject params)295 private void onPlumberAPIClosed(JavaScriptObject params) 296 { 297 PlumberAPIParams apiState = params.cast(); 298 299 // this completes any pending disconnection 300 disconnectingUrl_ = null; 301 302 // if we were asked not to stop when the window closes (i.e. when 303 // changing viewer types), bail out 304 if (!stopOnNextClose_) 305 { 306 stopOnNextClose_ = true; 307 return; 308 } 309 310 // If the API is stopping, then the user initiated the stop by 311 // closing the API window. Interrupt R to stop the Plumber API. 312 if (StringUtil.equals(apiState.getState(), PlumberAPIParams.STATE_STOPPING)) 313 { 314 if (commands_.interruptR().isEnabled()) 315 commands_.interruptR().execute(); 316 apiState.setState(PlumberAPIParams.STATE_STOPPED); 317 } 318 eventBus_.fireEvent(new PlumberAPIStatusEvent((PlumberAPIParams) params.cast())); 319 } 320 exportPlumberAPIClosedCallback()321 private final native void exportPlumberAPIClosedCallback()/*-{ 322 var registry = this; 323 $wnd.notifyPlumberAPIClosed = $entry( 324 function(params) { 325 registry.@org.rstudio.studio.client.plumber.PlumberAPI::notifyPlumberAPIClosed(Lcom/google/gwt/core/client/JavaScriptObject;)(params); 326 } 327 ); 328 $wnd.notifyPlumberAPIDisconnected = $entry( 329 function(params) { 330 registry.@org.rstudio.studio.client.plumber.PlumberAPI::notifyPlumberAPIDisconnected(Lcom/google/gwt/core/client/JavaScriptObject;)(params); 331 } 332 ); 333 }-*/; 334 setPlumberViewerType(String viewerType)335 private void setPlumberViewerType(String viewerType) 336 { 337 UserPrefs prefs = pPrefs_.get(); 338 prefs.plumberViewerType().setGlobalValue(viewerType); 339 prefs.writeUserPrefs(); 340 341 // if we have a running Plumber API and the viewer type has changed, 342 // snap the API into the new location 343 if (currentViewType_ != viewerType && params_ != null) 344 { 345 // if transitioning away from the pane or the window, close down 346 // the old instance 347 if (currentViewType_ == UserPrefs.PLUMBER_VIEWER_TYPE_PANE || 348 currentViewType_ == UserPrefs.PLUMBER_VIEWER_TYPE_WINDOW) 349 { 350 if (currentViewType_ == UserPrefs.PLUMBER_VIEWER_TYPE_WINDOW) 351 { 352 stopOnNextClose_ = false; 353 satelliteManager_.closeSatelliteWindow(PlumberAPISatellite.NAME); 354 } 355 else 356 { 357 eventBus_.fireEvent(new ViewerClearedEvent(false)); 358 } 359 } 360 361 // assign new viewer type 362 currentViewType_ = viewerType; 363 params_.setViewerType(viewerType); 364 365 if (currentViewType_ == UserPrefs.PLUMBER_VIEWER_TYPE_PANE || 366 currentViewType_ == UserPrefs.PLUMBER_VIEWER_TYPE_WINDOW || 367 currentViewType_ == UserPrefs.PLUMBER_VIEWER_TYPE_BROWSER) 368 { 369 eventBus_.fireEvent(new PlumberAPIStatusEvent(params_)); 370 } 371 } 372 } 373 launchPlumberFile(String file)374 private void launchPlumberFile(String file) 375 { 376 server_.getPlumberRunCmd(file, 377 new ServerRequestCallback<PlumberRunCmd>() 378 { 379 @Override 380 public void onResponseReceived(PlumberRunCmd cmd) 381 { 382 eventBus_.fireEvent(new SendToConsoleEvent(cmd.getRunCmd(), true)); 383 } 384 385 @Override 386 public void onError(ServerError error) 387 { 388 display_.showErrorMessage("Plumber API Launch Failed", error.getMessage()); 389 } 390 }); 391 } 392 activateWindow()393 private void activateWindow() 394 { 395 activateWindow(null); 396 } 397 activateWindow(PlumberAPIParams params)398 private void activateWindow(PlumberAPIParams params) 399 { 400 WindowEx win = satelliteManager_.getSatelliteWindowObject(PlumberAPISatellite.NAME); 401 boolean isRefresh = win != null && 402 (params == null || (params_ != null && 403 StringUtil.equals(params.getPath(), params_.getPath()))); 404 boolean isChrome = !Desktop.isDesktop() && BrowseCap.isChrome(); 405 if (params != null) 406 params_ = params; 407 if (win == null || (!isRefresh && !isChrome)) 408 { 409 int width = 910; 410 411 // If there's no window yet, or we're switching APIs in a browser 412 // other than Chrome, do a normal open 413 satelliteManager_.openSatellite(PlumberAPISatellite.NAME, 414 params_, new Size(width, 1100)); 415 } 416 else if (isChrome) 417 { 418 // we have a window and we're Chrome, so force a close and reopen 419 satelliteManager_.forceReopenSatellite(PlumberAPISatellite.NAME, 420 params_, 421 true); 422 } 423 else 424 { 425 satelliteManager_.activateSatelliteWindow(PlumberAPISatellite.NAME); 426 } 427 } 428 429 private final EventBus eventBus_; 430 private final SatelliteManager satelliteManager_; 431 private final DependencyManager dependencyManager_; 432 private final Commands commands_; 433 private final Provider<UserPrefs> pPrefs_; 434 private final PlumberServerOperations server_; 435 private final GlobalDisplay display_; 436 private final ApplicationInterrupt interrupt_; 437 438 private PlumberAPIParams params_; 439 private String disconnectingUrl_; 440 private boolean isBusy_; 441 private boolean stopOnNextClose_ = true; 442 private String satelliteClosePath_ = null; 443 private String currentViewType_; 444 } 445