1 /*
2  * RSuspend.cpp
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 
16 #include <shared_core/FilePath.hpp>
17 
18 #include <Rembedded.h>
19 
20 #include <r/RExec.hpp>
21 #include <r/session/RClientState.hpp>
22 #include <r/session/RGraphics.hpp>
23 #include <r/session/RSession.hpp>
24 #include <r/session/RSessionState.hpp>
25 
26 #include "REmbedded.hpp"
27 #include "RRestartContext.hpp"
28 #include "RStdCallbacks.hpp"
29 #include "RSuspend.hpp"
30 
31 using namespace rstudio::core;
32 
33 namespace rstudio {
34 namespace r {
35 namespace session {
36 
37 namespace {
38 
39 // session state path
40 FilePath s_suspendedSessionPath;
41 
42 // client-state paths
43 FilePath s_clientStatePath;
44 FilePath s_projectClientStatePath;
45 
46 // are in the middle of servicing a suspend request?
47 bool s_suspended = false;
48 
saveSessionState(const RSuspendOptions & options,const FilePath & suspendedSessionPath,bool disableSaveCompression)49 bool saveSessionState(const RSuspendOptions& options,
50                       const FilePath& suspendedSessionPath,
51                       bool disableSaveCompression)
52 {
53    // notify client of serialization status
54    SerializationCallbackScope cb(kSerializationActionSuspendSession);
55 
56    // suppress interrupts which occur during saving
57    r::exec::IgnoreInterruptsScope ignoreInterrupts;
58 
59    // save
60    if (options.saveMinimal)
61    {
62       // save minimal
63       return r::session::state::saveMinimal(suspendedSessionPath,
64                                             options.saveWorkspace);
65 
66    }
67    else
68    {
69       return r::session::state::save(suspendedSessionPath,
70                                      utils::isServerMode(),
71                                      options.excludePackages,
72                                      disableSaveCompression,
73                                      options.ephemeralEnvVars);
74    }
75 }
76 
77 } // anonymous namespace
78 
setSuspendPaths(const FilePath & suspendedSessionPath,const FilePath & clientStatePath,const FilePath & projectClientStatePath)79 void setSuspendPaths(const FilePath& suspendedSessionPath,
80                      const FilePath& clientStatePath,
81                      const FilePath& projectClientStatePath)
82 {
83    s_suspendedSessionPath = suspendedSessionPath;
84    s_clientStatePath = clientStatePath;
85    s_projectClientStatePath = projectClientStatePath;
86 
87 }
88 
suspendedSessionPath()89 FilePath suspendedSessionPath()
90 {
91    return s_suspendedSessionPath;
92 }
93 
suspend(const RSuspendOptions & options,const FilePath & suspendedSessionPath,bool disableSaveCompression,bool force)94 bool suspend(const RSuspendOptions& options,
95              const FilePath& suspendedSessionPath,
96              bool disableSaveCompression,
97              bool force)
98 {
99    // validate that force == true if disableSaveCompression is specified
100    // this is because save compression is disabled and the previous options
101    // are not restored, so it is only suitable to use this when we know
102    // the process is going to go away completely
103    if (disableSaveCompression)
104       BOOST_ASSERT(force == true);
105 
106    // commit all client state
107    saveClientState(ClientStateCommitAll);
108 
109    // if we are saving minimal then clear the graphics device
110    if (options.saveMinimal)
111    {
112       r::session::graphics::display().clear();
113    }
114 
115    // save the session state. errors are handled internally and reported
116    // directly to the end user and written to the server log.
117    bool suspend = saveSessionState(options,
118                                    suspendedSessionPath,
119                                    disableSaveCompression);
120 
121    // if we failed to save the data and are being forced then warn user
122    if (!suspend && force)
123    {
124       reportAndLogWarning("Forcing suspend of process in spite of all session "
125                           "data not being fully saved.");
126       suspend = true;
127    }
128 
129    // only continue with exiting the process if we actually succeed in saving
130    if(suspend)
131    {
132       // set suspended flag so cleanup code can act accordingly
133       s_suspended = true;
134 
135       // call suspend hook
136       rCallbacks().suspended(options);
137 
138       // clean up but don't save workspace or runLast because we have
139       // been suspended
140       RCleanUp(SA_NOSAVE, options.status, FALSE);
141 
142       // keep compiler happy (this line will never execute)
143       return true;
144    }
145    else
146    {
147       return false;
148    }
149 }
150 
suspend(bool force,int status,const std::string & ephemeralEnvVars)151 bool suspend(bool force, int status, const std::string& ephemeralEnvVars)
152 {
153    return suspend(RSuspendOptions(status, ephemeralEnvVars),
154                   s_suspendedSessionPath,
155                   false,
156                   force);
157 }
158 
suspendForRestart(const RSuspendOptions & options)159 void suspendForRestart(const RSuspendOptions& options)
160 {
161    suspend(options,
162            RestartContext::createSessionStatePath(utils::scopedScratchPath(),
163                                                   utils::sessionPort()),
164            true,  // disable save compression
165            true);  // force suspend
166 }
167 
SerializationCallbackScope(int action,const FilePath & targetPath)168 SerializationCallbackScope::SerializationCallbackScope(int action,
169                            const FilePath& targetPath)
170 {
171    rCallbacks().serialization(action, targetPath);
172 }
173 
~SerializationCallbackScope()174 SerializationCallbackScope::~SerializationCallbackScope()
175 {
176    try {
177       rCallbacks().serialization(kSerializationActionCompleted,
178                                  FilePath());
179    } catch(...) {}
180 }
181 
suspended()182 bool suspended()
183 {
184    return s_suspended;
185 }
186 
saveClientState(ClientStateCommitType commitType)187 void saveClientState(ClientStateCommitType commitType)
188 {
189    using namespace r::session;
190 
191    // save client state (note we don't explicitly restore this
192    // in restoreWorkingState, rather it is restored during
193    // initialize() so that the client always has access to it when
194    // for client_init)
195    r::session::clientState().commit(commitType,
196                                     s_clientStatePath,
197                                     s_projectClientStatePath);
198 }
199 
200 namespace utils
201 {
202 
clientStatePath()203 core::FilePath clientStatePath()
204 {
205    return s_clientStatePath;
206 }
207 
projectClientStatePath()208 core::FilePath projectClientStatePath()
209 {
210    return s_projectClientStatePath;
211 }
212 
suspendedSessionPath()213 core::FilePath suspendedSessionPath()
214 {
215    return s_suspendedSessionPath;
216 }
217 
218 } // namespace utils
219 
220 } // namespace session
221 } // namespace r
222 } // namespace rstudio
223