1 /*
2  * ApplicationQuit.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.application;
16 
17 import java.util.ArrayList;
18 
19 import org.rstudio.core.client.Barrier;
20 import org.rstudio.core.client.Barrier.Token;
21 import org.rstudio.core.client.StringUtil;
22 import org.rstudio.core.client.command.CommandBinder;
23 import org.rstudio.core.client.command.Handler;
24 import org.rstudio.core.client.files.FileSystemItem;
25 import org.rstudio.core.client.widget.MessageDialog;
26 import org.rstudio.core.client.widget.Operation;
27 import org.rstudio.core.client.widget.OperationWithInput;
28 import org.rstudio.studio.client.RStudioGinjector;
29 import org.rstudio.studio.client.application.events.EventBus;
30 import org.rstudio.studio.client.application.events.HandleUnsavedChangesEvent;
31 import org.rstudio.studio.client.application.events.QuitInitiatedEvent;
32 import org.rstudio.studio.client.application.events.RestartStatusEvent;
33 import org.rstudio.studio.client.application.events.SaveActionChangedEvent;
34 import org.rstudio.studio.client.application.events.SuspendAndRestartEvent;
35 import org.rstudio.studio.client.application.model.ApplicationServerOperations;
36 import org.rstudio.studio.client.application.model.RVersionSpec;
37 import org.rstudio.studio.client.application.model.SaveAction;
38 import org.rstudio.studio.client.application.model.SuspendOptions;
39 import org.rstudio.studio.client.application.model.TutorialApiCallContext;
40 import org.rstudio.studio.client.common.GlobalDisplay;
41 import org.rstudio.studio.client.common.GlobalProgressDelayer;
42 import org.rstudio.studio.client.common.TimedProgressIndicator;
43 import org.rstudio.studio.client.common.filetypes.FileIcon;
44 import org.rstudio.studio.client.projects.Projects;
45 import org.rstudio.studio.client.projects.events.OpenProjectNewWindowEvent;
46 import org.rstudio.studio.client.server.ServerError;
47 import org.rstudio.studio.client.server.ServerRequestCallback;
48 import org.rstudio.studio.client.server.VoidServerRequestCallback;
49 import org.rstudio.studio.client.workbench.WorkbenchContext;
50 import org.rstudio.studio.client.workbench.commands.Commands;
51 import org.rstudio.studio.client.workbench.events.LastChanceSaveEvent;
52 import org.rstudio.studio.client.workbench.model.Session;
53 import org.rstudio.studio.client.workbench.model.SessionOpener;
54 import org.rstudio.studio.client.workbench.model.UnsavedChangesItem;
55 import org.rstudio.studio.client.workbench.model.UnsavedChangesTarget;
56 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs;
57 import org.rstudio.studio.client.workbench.ui.unsaved.UnsavedChangesDialog;
58 import org.rstudio.studio.client.workbench.ui.unsaved.UnsavedChangesDialog.Result;
59 import org.rstudio.studio.client.workbench.views.jobs.model.JobManager;
60 import org.rstudio.studio.client.workbench.views.source.Source;
61 import org.rstudio.studio.client.workbench.views.terminal.TerminalHelper;
62 
63 import com.google.gwt.core.client.GWT;
64 import com.google.gwt.user.client.Command;
65 import com.google.inject.Inject;
66 import com.google.inject.Provider;
67 import com.google.inject.Singleton;
68 
69 @Singleton
70 public class ApplicationQuit implements SaveActionChangedEvent.Handler,
71                                         HandleUnsavedChangesEvent.Handler,
72                                         SuspendAndRestartEvent.Handler
73 {
74    public interface Binder extends CommandBinder<Commands, ApplicationQuit> {}
75 
76    @Inject
ApplicationQuit(ApplicationServerOperations server, GlobalDisplay globalDisplay, EventBus eventBus, WorkbenchContext workbenchContext, Provider<Source> pSource, Provider<UserPrefs> pUiPrefs, Commands commands, Binder binder, TerminalHelper terminalHelper, Provider<JobManager> pJobManager, Provider<SessionOpener> pSessionOpener)77    public ApplicationQuit(ApplicationServerOperations server,
78                           GlobalDisplay globalDisplay,
79                           EventBus eventBus,
80                           WorkbenchContext workbenchContext,
81                           Provider<Source> pSource,
82                           Provider<UserPrefs> pUiPrefs,
83                           Commands commands,
84                           Binder binder,
85                           TerminalHelper terminalHelper,
86                           Provider<JobManager> pJobManager,
87                           Provider<SessionOpener> pSessionOpener)
88    {
89       // save references
90       server_ = server;
91       globalDisplay_ = globalDisplay;
92       eventBus_ = eventBus;
93       workbenchContext_ = workbenchContext;
94       pSource_ = pSource;
95       pUserPrefs_ = pUiPrefs;
96       terminalHelper_ = terminalHelper;
97       pJobManager_ = pJobManager;
98       pSessionOpener_ = pSessionOpener;
99 
100       // bind to commands
101       binder.bind(commands, this);
102 
103       // subscribe to events
104       eventBus.addHandler(SaveActionChangedEvent.TYPE, this);
105       eventBus.addHandler(HandleUnsavedChangesEvent.TYPE, this);
106       eventBus.addHandler(SuspendAndRestartEvent.TYPE, this);
107    }
108 
109 
110    // notification that we are ready to quit
111    public interface QuitContext
112    {
onReadyToQuit(boolean saveChanges)113       void onReadyToQuit(boolean saveChanges);
114    }
115 
prepareForQuit(final String caption, final QuitContext quitContext)116    public void prepareForQuit(final String caption,
117                               final QuitContext quitContext)
118    {
119       prepareForQuit(caption, true /*allowCancel*/, false /*forceSaveAll*/, quitContext);
120    }
121 
prepareForQuit(final String caption, final boolean allowCancel, final boolean forceSaveAll, final QuitContext quitContext)122    public void prepareForQuit(final String caption,
123                               final boolean allowCancel,
124                               final boolean forceSaveAll,
125                               final QuitContext quitContext)
126    {
127       String busyMode = pUserPrefs_.get().busyDetection().getValue();
128 
129       boolean busy = workbenchContext_.isServerBusy() || terminalHelper_.warnBeforeClosing(busyMode);
130       String msg = null;
131       if (busy)
132       {
133          if (workbenchContext_.isServerBusy() && !terminalHelper_.warnBeforeClosing(busyMode))
134             msg = "The R session is currently busy.";
135          else if (workbenchContext_.isServerBusy() && terminalHelper_.warnBeforeClosing(busyMode))
136             msg = "The R session and the terminal are currently busy.";
137          else
138             msg = "The terminal is currently busy.";
139       }
140 
141       eventBus_.fireEvent(new QuitInitiatedEvent());
142 
143       if (busy && !forceSaveAll)
144       {
145          if (allowCancel)
146          {
147             globalDisplay_.showYesNoMessage(
148                   MessageDialog.QUESTION,
149                   caption,
150                   msg + " Are you sure you want to quit?",
151                   () -> handleUnfinishedWork(caption, allowCancel, forceSaveAll, quitContext),
152                   true);
153          }
154          else
155          {
156             handleUnfinishedWork(caption, allowCancel, forceSaveAll, quitContext);
157          }
158       }
159       else
160       {
161          // if we aren't restoring source documents then close them all now
162          if (pSource_.get() != null && !pUserPrefs_.get().restoreSourceDocuments().getValue())
163          {
164             pSource_.get().closeAllSourceDocs(caption,
165                   () -> handleUnfinishedWork(caption, allowCancel, forceSaveAll, quitContext),
166                   null);
167          }
168          else
169          {
170             handleUnfinishedWork(caption, allowCancel, forceSaveAll, quitContext);
171          }
172       }
173    }
174 
handleUnfinishedWork(String caption, boolean allowCancel, boolean forceSaveAll, QuitContext quitContext)175    private void handleUnfinishedWork(String caption,
176                                      boolean allowCancel,
177                                      boolean forceSaveAll,
178                                      QuitContext quitContext)
179    {
180       Command handleUnsaved = () -> {
181          // handle unsaved editor changes
182          handleUnsavedChanges(saveAction_.getAction(), caption, allowCancel, forceSaveAll,
183                pSource_.get(), workbenchContext_, globalEnvTarget_, quitContext);
184       };
185 
186       if (allowCancel)
187       {
188          // check for running jobs
189          pJobManager_.get().promptForTermination((confirmed) ->
190          {
191             if (confirmed)
192             {
193                handleUnsaved.execute();
194             }
195          });
196       }
197       else
198       {
199          handleUnsaved.execute();
200       }
201    }
202 
203 
204    private static boolean handlingUnsavedChanges_;
isHandlingUnsavedChanges()205    public static boolean isHandlingUnsavedChanges()
206    {
207       return handlingUnsavedChanges_;
208    }
209 
handleUnsavedChanges(final int saveAction, String caption, boolean allowCancel, boolean forceSaveAll, final Source source, final WorkbenchContext workbenchContext, final UnsavedChangesTarget globalEnvTarget, final QuitContext quitContext)210    public static void handleUnsavedChanges(final int saveAction,
211                                      String caption,
212                                      boolean allowCancel,
213                                      boolean forceSaveAll,
214                                      final Source source,
215                                      final WorkbenchContext workbenchContext,
216                                      final UnsavedChangesTarget globalEnvTarget,
217                                      final QuitContext quitContext)
218    {
219       // see what the unsaved changes situation is and prompt accordingly
220       ArrayList<UnsavedChangesTarget> unsavedSourceDocs =
221                         source.getUnsavedChanges(Source.TYPE_FILE_BACKED);
222 
223       // force save all
224       if (forceSaveAll)
225       {
226          // save all unsaved documents and then quit
227          source.handleUnsavedChangesBeforeExit(
228                unsavedSourceDocs,
229                new Command() {
230                   @Override
231                   public void execute()
232                   {
233                      boolean saveChanges = saveAction != SaveAction.NOSAVE;
234                      quitContext.onReadyToQuit(saveChanges);
235                   }
236                });
237 
238          return;
239       }
240       // no unsaved changes at all
241       else if (saveAction != SaveAction.SAVEASK && unsavedSourceDocs.size() == 0)
242       {
243          // define quit operation
244          final Operation quitOperation = new Operation() { public void execute()
245          {
246             quitContext.onReadyToQuit(saveAction == SaveAction.SAVE);
247          }};
248 
249          // if this is a quit session then we always prompt
250          if (ApplicationAction.isQuit())
251          {
252             RStudioGinjector.INSTANCE.getGlobalDisplay().showYesNoMessage(
253                   MessageDialog.QUESTION,
254                   caption,
255                   "Are you sure you want to quit the R session?",
256                   quitOperation,
257                   true);
258          }
259          else
260          {
261             quitOperation.execute();
262          }
263 
264          return;
265       }
266 
267       // just an unsaved environment
268       if (unsavedSourceDocs.size() == 0 && workbenchContext != null)
269       {
270          // confirm quit and do it
271          String prompt = "Save workspace image to " +
272                          workbenchContext.getREnvironmentPath() + "?";
273          RStudioGinjector.INSTANCE.getGlobalDisplay().showYesNoMessage(
274                GlobalDisplay.MSG_QUESTION,
275                caption,
276                prompt,
277                allowCancel,
278                () -> quitContext.onReadyToQuit(true),
279                () -> quitContext.onReadyToQuit(false),
280                () -> {},
281                "Save",
282                "Don't Save",
283                true);
284       }
285 
286       // a single unsaved document (can be any document in desktop mode, but
287       // must be from the main window in web mode)
288       else if (saveAction != SaveAction.SAVEASK &&
289                unsavedSourceDocs.size() == 1 &&
290                (Desktop.hasDesktopFrame() ||
291                 !(unsavedSourceDocs.get(0) instanceof UnsavedChangesItem)))
292       {
293          source.saveWithPrompt(
294            unsavedSourceDocs.get(0),
295            source.revertUnsavedChangesBeforeExitCommand(new Command() {
296                @Override
297                public void execute()
298                {
299                   quitContext.onReadyToQuit(saveAction == SaveAction.SAVE);
300                }}),
301            null);
302       }
303 
304       // multiple save targets
305       else
306       {
307          ArrayList<UnsavedChangesTarget> unsaved = new ArrayList<>();
308          if (saveAction == SaveAction.SAVEASK && globalEnvTarget != null)
309             unsaved.add(globalEnvTarget);
310          unsaved.addAll(unsavedSourceDocs);
311          new UnsavedChangesDialog(
312             caption,
313             unsaved,
314             new OperationWithInput<UnsavedChangesDialog.Result>() {
315 
316                @Override
317                public void execute(Result result)
318                {
319                   ArrayList<UnsavedChangesTarget> saveTargets =
320                                                 result.getSaveTargets();
321 
322                   // remote global env target from list (if specified) and
323                   // compute the saveChanges value
324                   boolean saveGlobalEnv = saveAction == SaveAction.SAVE;
325                   if (saveAction == SaveAction.SAVEASK &&
326                       globalEnvTarget != null)
327                      saveGlobalEnv = saveTargets.remove(globalEnvTarget);
328                   final boolean saveChanges = saveGlobalEnv;
329 
330                   // save specified documents and then quit
331                   source.handleUnsavedChangesBeforeExit(
332                         saveTargets,
333                         new Command() {
334                            @Override
335                            public void execute()
336                            {
337                               quitContext.onReadyToQuit(saveChanges);
338                            }
339                         });
340                }
341             },
342 
343             // no cancel operation
344             null
345             ).showModal();
346       }
347    }
348 
performQuit(TutorialApiCallContext callContext, boolean saveChanges)349    public void performQuit(TutorialApiCallContext callContext, boolean saveChanges)
350    {
351       performQuit(callContext, saveChanges, null, null);
352    }
353 
performQuit(TutorialApiCallContext callContext, boolean saveChanges, Command onQuitAcknowledged)354    public void performQuit(TutorialApiCallContext callContext,
355                            boolean saveChanges,
356                            Command onQuitAcknowledged)
357    {
358       performQuit(callContext, null, saveChanges, null, null, onQuitAcknowledged);
359    }
360 
performQuit(TutorialApiCallContext callContext, boolean saveChanges, String switchToProject)361    public void performQuit(TutorialApiCallContext callContext,
362                            boolean saveChanges,
363                            String switchToProject)
364    {
365       performQuit(callContext, saveChanges, switchToProject, null);
366    }
367 
performQuit(TutorialApiCallContext callContext, boolean saveChanges, String switchToProject, RVersionSpec switchToRVersion)368    public void performQuit(TutorialApiCallContext callContext,
369                            boolean saveChanges,
370                            String switchToProject,
371                            RVersionSpec switchToRVersion)
372    {
373       performQuit(callContext, null, saveChanges, switchToProject, switchToRVersion);
374    }
375 
performQuit(TutorialApiCallContext callContext, String progressMessage, boolean saveChanges, String switchToProject, RVersionSpec switchToRVersion)376    public void performQuit(TutorialApiCallContext callContext,
377                            String progressMessage,
378                            boolean saveChanges,
379                            String switchToProject,
380                            RVersionSpec switchToRVersion)
381    {
382       performQuit(callContext,
383                   progressMessage,
384                   saveChanges,
385                   switchToProject,
386                   switchToRVersion,
387                   null);
388    }
389 
performQuit(TutorialApiCallContext callContext, String progressMessage, boolean saveChanges, String switchToProject, RVersionSpec switchToRVersion, Command onQuitAcknowledged)390    public void performQuit(TutorialApiCallContext callContext,
391                            String progressMessage,
392                            boolean saveChanges,
393                            String switchToProject,
394                            RVersionSpec switchToRVersion,
395                            Command onQuitAcknowledged)
396    {
397       new QuitCommand(callContext,
398                       progressMessage,
399                       saveChanges,
400                       switchToProject,
401                       switchToRVersion,
402                       onQuitAcknowledged).execute();
403    }
404 
405    @Override
onSaveActionChanged(SaveActionChangedEvent event)406    public void onSaveActionChanged(SaveActionChangedEvent event)
407    {
408       saveAction_ = event.getAction();
409    }
410 
411    @Override
onHandleUnsavedChanges(HandleUnsavedChangesEvent event)412    public void onHandleUnsavedChanges(HandleUnsavedChangesEvent event)
413    {
414       // command which will be used to callback the server
415       class HandleUnsavedCommand implements Command
416       {
417          public HandleUnsavedCommand(boolean handled)
418          {
419             handled_ = handled;
420          }
421 
422          @Override
423          public void execute()
424          {
425             // this codepath is for when the user quits R using the q()
426             // function -- in this case our standard client quit codepath
427             // isn't invoked, and as a result the desktop is not notified
428             // that there is a pending quit (so thinks R has crashed when
429             // the process exits). since this codepath is only for the quit
430             // case (and not the restart or restart and reload cases)
431             // we can set the pending quit bit here
432             if (Desktop.hasDesktopFrame())
433             {
434                Desktop.getFrame().setPendingQuit(
435                         DesktopFrame.PENDING_QUIT_AND_EXIT);
436             }
437 
438             server_.handleUnsavedChangesCompleted(
439                                           handled_,
440                                           new VoidServerRequestCallback());
441          }
442 
443          private final boolean handled_;
444       }
445 
446       // get unsaved source docs
447       ArrayList<UnsavedChangesTarget> unsavedSourceDocs =
448                         pSource_.get().getUnsavedChanges(Source.TYPE_FILE_BACKED);
449 
450       if (unsavedSourceDocs.size() == 1)
451       {
452          pSource_.get().saveWithPrompt(
453                unsavedSourceDocs.get(0),
454                pSource_.get().revertUnsavedChangesBeforeExitCommand(
455                   new HandleUnsavedCommand(true)),
456                new HandleUnsavedCommand(false));
457       }
458       else if (unsavedSourceDocs.size() > 1)
459       {
460          new UnsavedChangesDialog(
461                "Quit R Session",
462                unsavedSourceDocs,
463                new OperationWithInput<UnsavedChangesDialog.Result>() {
464                   @Override
465                   public void execute(Result result)
466                   {
467                      // save specified documents and then quit
468                      pSource_.get().handleUnsavedChangesBeforeExit(
469                            result.getSaveTargets(),
470                            new HandleUnsavedCommand(true));
471                   }
472                 },
473                 new HandleUnsavedCommand(false)
474          ).showModal();
475       }
476       else
477       {
478          new HandleUnsavedCommand(true).execute();
479       }
480    }
481 
482 
483    @Handler
onRestartR()484    public void onRestartR()
485    {
486          // check for running jobs
487          pJobManager_.get().promptForTermination((confirmed) ->
488          {
489             if (confirmed)
490             {
491                terminalHelper_.warnBusyTerminalBeforeCommand(() ->
492                {
493                   boolean saveChanges = saveAction_.getAction() != SaveAction.NOSAVE;
494                   eventBus_.fireEvent(new SuspendAndRestartEvent(
495                         SuspendOptions.createSaveMinimal(saveChanges),
496                         null));
497                }, "Restart R", "Terminal jobs will be terminated. Are you sure?",
498                   pUserPrefs_.get().busyDetection().getValue());
499             }
500          });
501    }
502 
503    @Handler
onSuspendSession()504    public void onSuspendSession()
505    {
506       server_.suspendSession(true, new VoidServerRequestCallback());
507    }
508 
509    @Override
onSuspendAndRestart(final SuspendAndRestartEvent event)510    public void onSuspendAndRestart(final SuspendAndRestartEvent event)
511    {
512       // Ignore nested restarts once restart starts
513       if (suspendingAndRestarting_) return;
514 
515       // set restart pending for desktop
516       setPendingQuit(DesktopFrame.PENDING_QUIT_AND_RESTART);
517 
518       final TimedProgressIndicator progress = new TimedProgressIndicator(
519             globalDisplay_.getProgressIndicator("Error"));
520       progress.onTimedProgress("Restarting R...", 1000);
521 
522       final Operation onRestartComplete = () -> {
523          suspendingAndRestarting_ = false;
524          progress.onCompleted();
525          eventBus_.fireEvent(new RestartStatusEvent(RestartStatusEvent.RESTART_COMPLETED));
526       };
527 
528       // perform the suspend and restart
529       suspendingAndRestarting_ = true;
530       eventBus_.fireEvent(new RestartStatusEvent(RestartStatusEvent.RESTART_INITIATED));
531       pSessionOpener_.get().suspendForRestart(
532          event.getAfterRestartCommand(),
533          event.getSuspendOptions(),
534          () -> { // success
535             onRestartComplete.execute();
536          }, () -> { // failure
537             onRestartComplete.execute();
538             setPendingQuit(DesktopFrame.PENDING_QUIT_NONE);
539          });
540    }
541 
setPendingQuit(int pendingQuit)542    private void setPendingQuit(int pendingQuit)
543    {
544       if (Desktop.hasDesktopFrame())
545          Desktop.getFrame().setPendingQuit(pendingQuit);
546    }
547 
548    @Handler
onQuitSession()549    public void onQuitSession()
550    {
551       prepareForQuit("Quit R Session", (boolean saveChanges) -> performQuit(null, saveChanges));
552    }
553 
554    @Handler
onForceQuitSession()555    public void onForceQuitSession()
556    {
557       prepareForQuit("Quit R Session", false /*allowCancel*/, false /*forceSaveChanges*/,
558             (boolean saveChanges) -> performQuit(null, saveChanges));
559    }
560 
doRestart(Session session)561    public void doRestart(Session session)
562    {
563       prepareForQuit(
564             "Restarting RStudio",
565             saveChanges -> {
566                String project = session.getSessionInfo().getActiveProjectFile();
567                if (project == null)
568                   project = Projects.NONE;
569 
570                final String finalProject = project;
571                performQuit(null, saveChanges, () -> {
572                   eventBus_.fireEvent(new OpenProjectNewWindowEvent(finalProject, null));
573                });
574             });
575    }
576 
577    private UnsavedChangesTarget globalEnvTarget_ = new UnsavedChangesTarget()
578    {
579       @Override
580       public String getId()
581       {
582          return "F59C8727-3C63-41F4-989C-B1E1D47760E3";
583       }
584 
585       @Override
586       public FileIcon getIcon()
587       {
588          return FileIcon.RDATA_ICON;
589       }
590 
591       @Override
592       public String getTitle()
593       {
594          return "Workspace image (.RData)";
595       }
596 
597       @Override
598       public String getPath()
599       {
600          return workbenchContext_.getREnvironmentPath();
601       }
602 
603    };
604 
buildSwitchMessage(String switchToProject)605    private String buildSwitchMessage(String switchToProject)
606    {
607       String msg = switchToProject != "none" ?
608         "Switching to project " +
609            FileSystemItem.createFile(switchToProject).getParentPathString() :
610         "Closing project";
611       return msg + "...";
612    }
613 
614    private class QuitCommand implements Command
615    {
QuitCommand(TutorialApiCallContext callContext, String progressMessage, boolean saveChanges, String switchToProject, RVersionSpec switchToRVersion, Command onQuitAcknowledged)616       public QuitCommand(TutorialApiCallContext callContext,
617                          String progressMessage,
618                          boolean saveChanges,
619                          String switchToProject,
620                          RVersionSpec switchToRVersion,
621                          Command onQuitAcknowledged)
622       {
623          callContext_ = callContext;
624          progressMessage_ = progressMessage;
625          saveChanges_ = saveChanges;
626          switchToProject_ = switchToProject;
627          switchToRVersion_ = switchToRVersion;
628          onQuitAcknowledged_ = onQuitAcknowledged;
629       }
630 
execute()631       public void execute()
632       {
633          // show delayed progress
634          String msg = progressMessage_;
635          if (msg == null)
636          {
637             msg = switchToProject_ != null ?
638                                     buildSwitchMessage(switchToProject_) :
639                                     "Quitting R Session...";
640          }
641          final GlobalProgressDelayer progress = new GlobalProgressDelayer(
642                                                                globalDisplay_,
643                                                                250,
644                                                                msg);
645 
646          // Use a barrier and LastChanceSaveEvent to allow source documents
647          // and client state to be synchronized before quitting.
648          Barrier barrier = new Barrier();
649          barrier.addBarrierReleasedHandler(releasedEvent ->
650          {
651             // All last chance save operations have completed (or possibly
652             // failed). Now do the real quit.
653 
654             // notify the desktop frame that we are about to quit
655             String switchToProject = StringUtil.create(switchToProject_);
656             if (Desktop.hasDesktopFrame())
657             {
658                Desktop.getFrame().setPendingQuit(switchToProject_ != null ?
659                      DesktopFrame.PENDING_QUIT_RESTART_AND_RELOAD :
660                      DesktopFrame.PENDING_QUIT_AND_EXIT);
661             }
662 
663             server_.quitSession(
664                saveChanges_,
665                switchToProject,
666                switchToRVersion_,
667                GWT.getHostPageBaseURL(),
668                new ServerRequestCallback<Boolean>()
669                {
670                   @Override
671                   public void onResponseReceived(Boolean response)
672                   {
673                      if (response)
674                      {
675                         // clear progress only if we aren't switching projects
676                         // (otherwise we want to leave progress up until
677                         // the app reloads)
678                         if (switchToProject_ == null)
679                            progress.dismiss();
680 
681                         if (callContext_ != null)
682                         {
683                            eventBus_.fireEvent(new ApplicationTutorialEvent(
684                                  ApplicationTutorialEvent.API_SUCCESS, callContext_));
685                         }
686 
687                         // fire onQuitAcknowledged
688                         if (onQuitAcknowledged_ != null)
689                            onQuitAcknowledged_.execute();
690                      }
691                      else
692                      {
693                         onFailedToQuit("server quitSession responded false");
694                      }
695                   }
696 
697                   @Override
698                   public void onError(ServerError error)
699                   {
700                      onFailedToQuit(error.getMessage());
701                   }
702 
703                   private void onFailedToQuit(String message)
704                   {
705                      progress.dismiss();
706 
707                      if (callContext_ != null)
708                      {
709                         eventBus_.fireEvent(new ApplicationTutorialEvent(
710                               ApplicationTutorialEvent.API_ERROR,
711                               message,
712                               callContext_));
713                      }
714                      if (Desktop.hasDesktopFrame())
715                      {
716                         Desktop.getFrame().setPendingQuit(
717                                       DesktopFrame.PENDING_QUIT_NONE);
718                      }
719                   }
720                });
721          });
722 
723          // We acquire a token to make sure that the barrier doesn't fire before
724          // all the LastChanceSaveEvent listeners get a chance to acquire their
725          // own tokens.
726          Token token = barrier.acquire();
727          try
728          {
729             eventBus_.fireEvent(new LastChanceSaveEvent(barrier));
730          }
731          finally
732          {
733             token.release();
734          }
735       }
736 
737       private final TutorialApiCallContext callContext_;
738       private final boolean saveChanges_;
739       private final String switchToProject_;
740       private final RVersionSpec switchToRVersion_;
741       private final String progressMessage_;
742       private final Command onQuitAcknowledged_;
743    }
744 
745    private SaveAction saveAction_ = SaveAction.saveAsk();
746    private boolean suspendingAndRestarting_ = false;
747 
748    // injected
749    private final ApplicationServerOperations server_;
750    private final GlobalDisplay globalDisplay_;
751    private final Provider<UserPrefs> pUserPrefs_;
752    private final EventBus eventBus_;
753    private final WorkbenchContext workbenchContext_;
754    private final Provider<Source> pSource_;
755    private final TerminalHelper terminalHelper_;
756    private final Provider<JobManager> pJobManager_;
757    private final Provider<SessionOpener> pSessionOpener_;
758 }
759