/* * ApplicationQuit.java * * Copyright (C) 2009-12 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.application; import java.util.ArrayList; import org.rstudio.core.client.Barrier; import org.rstudio.core.client.Barrier.Token; import org.rstudio.core.client.StringUtil; import org.rstudio.core.client.command.CommandBinder; import org.rstudio.core.client.command.Handler; import org.rstudio.core.client.events.BarrierReleasedEvent; import org.rstudio.core.client.events.BarrierReleasedHandler; import org.rstudio.core.client.files.FileSystemItem; import org.rstudio.core.client.resources.ImageResource2x; import org.rstudio.core.client.widget.MessageDialog; import org.rstudio.core.client.widget.Operation; import org.rstudio.core.client.widget.OperationWithInput; import org.rstudio.studio.client.RStudioGinjector; import org.rstudio.studio.client.application.events.EventBus; import org.rstudio.studio.client.application.events.HandleUnsavedChangesEvent; import org.rstudio.studio.client.application.events.HandleUnsavedChangesHandler; import org.rstudio.studio.client.application.events.QuitInitiatedEvent; import org.rstudio.studio.client.application.events.RestartStatusEvent; import org.rstudio.studio.client.application.events.SaveActionChangedEvent; import org.rstudio.studio.client.application.events.SaveActionChangedHandler; import org.rstudio.studio.client.application.events.SuspendAndRestartEvent; import org.rstudio.studio.client.application.events.SuspendAndRestartHandler; import org.rstudio.studio.client.application.model.ApplicationServerOperations; import org.rstudio.studio.client.application.model.RVersionSpec; import org.rstudio.studio.client.application.model.SaveAction; import org.rstudio.studio.client.application.model.SuspendOptions; import org.rstudio.studio.client.common.GlobalDisplay; import org.rstudio.studio.client.common.GlobalProgressDelayer; import org.rstudio.studio.client.common.SuperDevMode; import org.rstudio.studio.client.common.TimedProgressIndicator; import org.rstudio.studio.client.common.filetypes.FileIconResources; import org.rstudio.studio.client.server.ServerError; import org.rstudio.studio.client.server.ServerRequestCallback; import org.rstudio.studio.client.server.VoidServerRequestCallback; import org.rstudio.studio.client.workbench.WorkbenchContext; import org.rstudio.studio.client.workbench.commands.Commands; import org.rstudio.studio.client.workbench.events.LastChanceSaveEvent; import org.rstudio.studio.client.workbench.model.UnsavedChangesItem; import org.rstudio.studio.client.workbench.model.UnsavedChangesTarget; import org.rstudio.studio.client.workbench.prefs.model.UIPrefs; import org.rstudio.studio.client.workbench.ui.unsaved.UnsavedChangesDialog; import org.rstudio.studio.client.workbench.ui.unsaved.UnsavedChangesDialog.Result; import org.rstudio.studio.client.workbench.views.console.events.ConsoleRestartRCompletedEvent; import org.rstudio.studio.client.workbench.views.console.events.SendToConsoleEvent; import org.rstudio.studio.client.workbench.views.source.Source; import org.rstudio.studio.client.workbench.views.source.SourceShim; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.RepeatingCommand; import com.google.gwt.resources.client.ImageResource; 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 ApplicationQuit implements SaveActionChangedHandler, HandleUnsavedChangesHandler, SuspendAndRestartHandler { public interface Binder extends CommandBinder<Commands, ApplicationQuit> {} @Inject public ApplicationQuit(ApplicationServerOperations server, GlobalDisplay globalDisplay, EventBus eventBus, WorkbenchContext workbenchContext, SourceShim sourceShim, Provider<UIPrefs> pUiPrefs, Commands commands, Binder binder) { // save references server_ = server; globalDisplay_ = globalDisplay; eventBus_ = eventBus; workbenchContext_ = workbenchContext; sourceShim_ = sourceShim; pUiPrefs_ = pUiPrefs; // bind to commands binder.bind(commands, this); // only enable suspendSession() in devmode commands.suspendSession().setVisible(SuperDevMode.isActive()); // subscribe to events eventBus.addHandler(SaveActionChangedEvent.TYPE, this); eventBus.addHandler(HandleUnsavedChangesEvent.TYPE, this); eventBus.addHandler(SuspendAndRestartEvent.TYPE, this); } // notification that we are ready to quit public interface QuitContext { void onReadyToQuit(boolean saveChanges); } public void prepareForQuit(final String caption, final QuitContext quitContext) { prepareForQuit(caption, false, quitContext); } public void prepareForQuit(final String caption, final boolean forceSaveAll, final QuitContext quitContext) { boolean busy = workbenchContext_.isServerBusy() || workbenchContext_.isTerminalBusy(); String msg = null; if (busy) { if (workbenchContext_.isServerBusy() && !workbenchContext_.isTerminalBusy()) msg = "The R session is currently busy."; else if (workbenchContext_.isServerBusy() && workbenchContext_.isTerminalBusy()) msg = "The R session and the terminal are currently busy."; else msg = "The terminal is currently busy."; } eventBus_.fireEvent(new QuitInitiatedEvent()); if (busy && !forceSaveAll) { globalDisplay_.showYesNoMessage( MessageDialog.QUESTION, caption, msg + " Are you sure you want to quit?", new Operation() { @Override public void execute() { handleUnsavedChanges(caption, forceSaveAll, quitContext); }}, true); } else { // if we aren't restoring source documents then close them all now if (!pUiPrefs_.get().restoreSourceDocuments().getValue()) { sourceShim_.closeAllSourceDocs(caption, new Command() { @Override public void execute() { handleUnsavedChanges(caption, forceSaveAll, quitContext); } }); } else { handleUnsavedChanges(caption, forceSaveAll, quitContext); } } } private void handleUnsavedChanges(String caption, boolean forceSaveAll, QuitContext quitContext) { handleUnsavedChanges(saveAction_.getAction(), caption, forceSaveAll, sourceShim_, workbenchContext_, globalEnvTarget_, quitContext); } private static boolean handlingUnsavedChanges_; public static boolean isHandlingUnsavedChanges() { return handlingUnsavedChanges_; } public static void handleUnsavedChanges(final int saveAction, String caption, boolean forceSaveAll, final SourceShim sourceShim, final WorkbenchContext workbenchContext, final UnsavedChangesTarget globalEnvTarget, final QuitContext quitContext) { // see what the unsaved changes situation is and prompt accordingly ArrayList<UnsavedChangesTarget> unsavedSourceDocs = sourceShim.getUnsavedChanges(Source.TYPE_FILE_BACKED); // force save all if (forceSaveAll) { // save all unsaved documents and then quit sourceShim.handleUnsavedChangesBeforeExit( unsavedSourceDocs, new Command() { @Override public void execute() { boolean saveChanges = saveAction != SaveAction.NOSAVE; quitContext.onReadyToQuit(saveChanges); } }); return; } // no unsaved changes at all else if (saveAction != SaveAction.SAVEASK && unsavedSourceDocs.size() == 0) { // define quit operation final Operation quitOperation = new Operation() { public void execute() { quitContext.onReadyToQuit(saveAction == SaveAction.SAVE); }}; // if this is a quit session then we always prompt if (ApplicationAction.isQuit()) { RStudioGinjector.INSTANCE.getGlobalDisplay().showYesNoMessage( MessageDialog.QUESTION, caption, "Are you sure you want to quit the R session?", quitOperation, true); } else { quitOperation.execute(); } return; } // just an unsaved environment if (unsavedSourceDocs.size() == 0 && workbenchContext != null) { // confirm quit and do it String prompt = "Save workspace image to " + workbenchContext.getREnvironmentPath() + "?"; RStudioGinjector.INSTANCE.getGlobalDisplay().showYesNoMessage( GlobalDisplay.MSG_QUESTION, caption, prompt, true, new Operation() { public void execute() { quitContext.onReadyToQuit(true); }}, new Operation() { public void execute() { quitContext.onReadyToQuit(false); }}, new Operation() { public void execute() { }}, "Save", "Don't Save", true); } // a single unsaved document (can be any document in desktop mode, but // must be from the main window in web mode) else if (saveAction != SaveAction.SAVEASK && unsavedSourceDocs.size() == 1 && (Desktop.isDesktop() || !(unsavedSourceDocs.get(0) instanceof UnsavedChangesItem))) { sourceShim.saveWithPrompt( unsavedSourceDocs.get(0), sourceShim.revertUnsavedChangesBeforeExitCommand(new Command() { @Override public void execute() { quitContext.onReadyToQuit(saveAction == SaveAction.SAVE); }}), null); } // multiple save targets else { ArrayList<UnsavedChangesTarget> unsaved = new ArrayList<UnsavedChangesTarget>(); if (saveAction == SaveAction.SAVEASK && globalEnvTarget != null) unsaved.add(globalEnvTarget); unsaved.addAll(unsavedSourceDocs); new UnsavedChangesDialog( caption, unsaved, new OperationWithInput<UnsavedChangesDialog.Result>() { @Override public void execute(Result result) { ArrayList<UnsavedChangesTarget> saveTargets = result.getSaveTargets(); // remote global env target from list (if specified) and // compute the saveChanges value boolean saveGlobalEnv = saveAction == SaveAction.SAVE; if (saveAction == SaveAction.SAVEASK && globalEnvTarget != null) saveGlobalEnv = saveTargets.remove(globalEnvTarget); final boolean saveChanges = saveGlobalEnv; // save specified documents and then quit sourceShim.handleUnsavedChangesBeforeExit( saveTargets, new Command() { @Override public void execute() { quitContext.onReadyToQuit(saveChanges); } }); } }, // no cancel operation null ).showModal(); } } public void performQuit(boolean saveChanges) { performQuit(saveChanges, null, null); } public void performQuit(boolean saveChanges, String switchToProject) { performQuit(saveChanges, switchToProject, null); } public void performQuit(boolean saveChanges, String switchToProject, RVersionSpec switchToRVersion) { performQuit(null, saveChanges, switchToProject, switchToRVersion); } public void performQuit(String progressMessage, boolean saveChanges, String switchToProject, RVersionSpec switchToRVersion) { performQuit(progressMessage, saveChanges, switchToProject, switchToRVersion, null); } public void performQuit(String progressMessage, boolean saveChanges, String switchToProject, RVersionSpec switchToRVersion, Command onQuitAcknowledged) { new QuitCommand(progressMessage, saveChanges, switchToProject, switchToRVersion, onQuitAcknowledged).execute(); } @Override public void onSaveActionChanged(SaveActionChangedEvent event) { saveAction_ = event.getAction(); } @Override public void onHandleUnsavedChanges(HandleUnsavedChangesEvent event) { // command which will be used to callback the server class HandleUnsavedCommand implements Command { public HandleUnsavedCommand(boolean handled) { handled_ = handled; } @Override public void execute() { // this codepath is for when the user quits R using the q() // function -- in this case our standard client quit codepath // isn't invoked, and as a result the desktop is not notified // that there is a pending quit (so thinks R has crashed when // the process exits). since this codepath is only for the quit // case (and not the restart or restart and reload cases) // we can set the pending quit bit here if (Desktop.isDesktop()) { Desktop.getFrame().setPendingQuit( DesktopFrame.PENDING_QUIT_AND_EXIT); } server_.handleUnsavedChangesCompleted( handled_, new VoidServerRequestCallback()); } private final boolean handled_; }; // get unsaved source docs ArrayList<UnsavedChangesTarget> unsavedSourceDocs = sourceShim_.getUnsavedChanges(Source.TYPE_FILE_BACKED); if (unsavedSourceDocs.size() == 1) { sourceShim_.saveWithPrompt( unsavedSourceDocs.get(0), sourceShim_.revertUnsavedChangesBeforeExitCommand( new HandleUnsavedCommand(true)), new HandleUnsavedCommand(false)); } else if (unsavedSourceDocs.size() > 1) { new UnsavedChangesDialog( "Quit R Session", unsavedSourceDocs, new OperationWithInput<UnsavedChangesDialog.Result>() { @Override public void execute(Result result) { // save specified documents and then quit sourceShim_.handleUnsavedChangesBeforeExit( result.getSaveTargets(), new HandleUnsavedCommand(true)); } }, new HandleUnsavedCommand(false) ).showModal(); } else { new HandleUnsavedCommand(true).execute(); } } @Handler public void onRestartR() { boolean saveChanges = saveAction_.getAction() != SaveAction.NOSAVE; eventBus_.fireEvent(new SuspendAndRestartEvent( SuspendOptions.createSaveMinimal(saveChanges), null)); } @Handler public void onSuspendSession() { server_.suspendSession(true, new VoidServerRequestCallback()); } @Override public void onSuspendAndRestart(final SuspendAndRestartEvent event) { // Ignore nested restarts once restart starts if (suspendingAndRestarting_) return; // set restart pending for desktop setPendinqQuit(DesktopFrame.PENDING_QUIT_AND_RESTART); final TimedProgressIndicator progress = new TimedProgressIndicator( globalDisplay_.getProgressIndicator("Error")); progress.onTimedProgress("Restarting R", 1000); final Operation onRestartComplete = new Operation() { @Override public void execute() { suspendingAndRestarting_ = false; progress.onCompleted(); } }; // perform the suspend and restart suspendingAndRestarting_ = true; eventBus_.fireEvent( new RestartStatusEvent(RestartStatusEvent.RESTART_INITIATED)); server_.suspendForRestart(event.getSuspendOptions(), new VoidServerRequestCallback() { @Override protected void onSuccess() { // send pings until the server restarts sendPing(event.getAfterRestartCommand(), 200, 25, new Command() { @Override public void execute() { onRestartComplete.execute(); eventBus_.fireEvent(new RestartStatusEvent( RestartStatusEvent.RESTART_COMPLETED)); } }); } @Override protected void onFailure() { onRestartComplete.execute(); eventBus_.fireEvent( new RestartStatusEvent(RestartStatusEvent.RESTART_COMPLETED)); setPendinqQuit(DesktopFrame.PENDING_QUIT_NONE); } }); } private void setPendinqQuit(int pendingQuit) { if (Desktop.isDesktop()) Desktop.getFrame().setPendingQuit(pendingQuit); } private void sendPing(final String afterRestartCommand, int delayMs, final int maxRetries, final Command onCompleted) { Scheduler.get().scheduleFixedDelay(new RepeatingCommand() { private int retries_ = 0; private boolean pingDelivered_ = false; private boolean pingInFlight_ = false; @Override public boolean execute() { // if we've already delivered the ping or our retry count // is exhausted then return false if (pingDelivered_ || (++retries_ > maxRetries)) return false; if (!pingInFlight_) { pingInFlight_ = true; server_.ping(new VoidServerRequestCallback() { @Override protected void onSuccess() { pingInFlight_ = false; if (!pingDelivered_) { pingDelivered_ = true; // issue after restart command if (!StringUtil.isNullOrEmpty(afterRestartCommand)) { eventBus_.fireEvent( new SendToConsoleEvent(afterRestartCommand, true, true)); } // otherwise make sure the console knows we // restarted (ensure prompt and set focus) else { eventBus_.fireEvent( new ConsoleRestartRCompletedEvent()); } } if (onCompleted != null) onCompleted.execute(); } @Override protected void onFailure() { pingInFlight_ = false; if (onCompleted != null) onCompleted.execute(); } }); } // keep trying until the ping is delivered return true; } }, delayMs); } @Handler public void onQuitSession() { prepareForQuit("Quit R Session", new QuitContext() { public void onReadyToQuit(boolean saveChanges) { performQuit(saveChanges); } }); } private UnsavedChangesTarget globalEnvTarget_ = new UnsavedChangesTarget() { @Override public String getId() { return "F59C8727-3C63-41F4-989C-B1E1D47760E3"; } @Override public ImageResource getIcon() { return new ImageResource2x(FileIconResources.INSTANCE.iconRdata2x()); } @Override public String getTitle() { return "Workspace image (.RData)"; } @Override public String getPath() { return workbenchContext_.getREnvironmentPath(); } }; private String buildSwitchMessage(String switchToProject) { String msg = !switchToProject.equals("none") ? "Switching to project " + FileSystemItem.createFile(switchToProject).getParentPathString() : "Closing project"; return msg + "..."; } private class QuitCommand implements Command { public QuitCommand(String progressMessage, boolean saveChanges, String switchToProject, RVersionSpec switchToRVersion, Command onQuitAcknowledged) { progressMessage_ = progressMessage; saveChanges_ = saveChanges; switchToProject_ = switchToProject; switchToRVersion_ = switchToRVersion; onQuitAcknowledged_ = onQuitAcknowledged; } public void execute() { // show delayed progress String msg = progressMessage_; if (msg == null) { msg = switchToProject_ != null ? buildSwitchMessage(switchToProject_) : "Quitting R Session..."; } final GlobalProgressDelayer progress = new GlobalProgressDelayer( globalDisplay_, 250, msg); // Use a barrier and LastChanceSaveEvent to allow source documents // and client state to be synchronized before quitting. Barrier barrier = new Barrier(); barrier.addBarrierReleasedHandler(new BarrierReleasedHandler() { public void onBarrierReleased(BarrierReleasedEvent event) { // All last chance save operations have completed (or possibly // failed). Now do the real quit. // notify the desktop frame that we are about to quit String switchToProject = new String(switchToProject_); if (Desktop.isDesktop()) { if (Desktop.getFrame().isCocoa() && switchToProject_ != null) { // on Cocoa there's an ugly intermittent crash that occurs // when we reload, so exit this instance and start a new // one when switching projects Desktop.getFrame().setPendingProject(switchToProject_); // Since we're going to be starting a new process we don't // want to pass a switchToProject argument to quitSession switchToProject = null; } else { Desktop.getFrame().setPendingQuit(switchToProject_ != null ? DesktopFrame.PENDING_QUIT_RESTART_AND_RELOAD : DesktopFrame.PENDING_QUIT_AND_EXIT); } } server_.quitSession( saveChanges_, switchToProject, switchToRVersion_, GWT.getHostPageBaseURL(), new ServerRequestCallback<Boolean>() { @Override public void onResponseReceived(Boolean response) { if (response) { // clear progress only if we aren't switching projects // (otherwise we want to leave progress up until // the app reloads) if (switchToProject_ == null) progress.dismiss(); // fire onQuitAcknowledged if (onQuitAcknowledged_ != null) onQuitAcknowledged_.execute(); } else { onFailedToQuit(); } } @Override public void onError(ServerError error) { onFailedToQuit(); } private void onFailedToQuit() { progress.dismiss(); if (Desktop.isDesktop()) { Desktop.getFrame().setPendingQuit( DesktopFrame.PENDING_QUIT_NONE); } } }); } }); // We acquire a token to make sure that the barrier doesn't fire before // all the LastChanceSaveEvent listeners get a chance to acquire their // own tokens. Token token = barrier.acquire(); try { eventBus_.fireEvent(new LastChanceSaveEvent(barrier)); } finally { token.release(); } } private final boolean saveChanges_; private final String switchToProject_; private final RVersionSpec switchToRVersion_; private final String progressMessage_; private final Command onQuitAcknowledged_; }; private final ApplicationServerOperations server_; private final GlobalDisplay globalDisplay_; private final Provider<UIPrefs> pUiPrefs_; private final EventBus eventBus_; private final WorkbenchContext workbenchContext_; private final SourceShim sourceShim_; private SaveAction saveAction_ = SaveAction.saveAsk(); private boolean suspendingAndRestarting_ = false; }