1 /*
2 *
3 * SessionSourceDatabaseSupervisor.cpp
4 *
5 * Copyright (C) 2021 by RStudio, PBC
6 *
7 * Unless you have received this program directly from RStudio pursuant
8 * to the terms of a commercial license agreement with RStudio, then
9 * this program is licensed to you under the terms of version 3 of the
10 * GNU Affero General Public License. This program is distributed WITHOUT
11 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
12 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
13 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
14 *
15 */
16
17 #include "SessionSourceDatabaseSupervisor.hpp"
18
19 #ifdef _WIN32
20 # include <winsock2.h>
21 # include <windows.h>
22 #endif
23
24 #include <vector>
25
26 #include <boost/scope_exit.hpp>
27 #include <boost/algorithm/string/predicate.hpp>
28
29 #include <shared_core/Error.hpp>
30 #include <shared_core/FilePath.hpp>
31 #include <core/FileSerializer.hpp>
32 #include <core/FileLock.hpp>
33 #include <core/FileUtils.hpp>
34 #include <core/BoostErrors.hpp>
35
36 #include <r/session/RSession.hpp>
37
38 #include <core/system/System.hpp>
39
40 #include <session/SessionOptions.hpp>
41 #include <session/SessionModuleContext.hpp>
42 #include "session/SessionSourceDatabase.hpp"
43
44 using namespace rstudio::core;
45
46 namespace rstudio {
47 namespace session {
48 namespace source_database {
49 namespace supervisor {
50
51 namespace {
52
53 const char * const kSessionDirPrefix = "s-";
54
sdbSourceDatabaseRoot()55 FilePath sdbSourceDatabaseRoot()
56 {
57 return module_context::scopedScratchPath().completePath("sdb");
58 }
59
sourceDatabaseRoot()60 FilePath sourceDatabaseRoot()
61 {
62 return module_context::scopedScratchPath().completePath(kSessionSourceDatabasePrefix);
63 }
64
mostRecentTitledDir()65 FilePath mostRecentTitledDir()
66 {
67 return module_context::scopedScratchPath().completePath(kSessionSourceDatabasePrefix "/mt");
68 }
69
mostRecentUntitledDir()70 FilePath mostRecentUntitledDir()
71 {
72 return module_context::scopedScratchPath().completePath(kSessionSourceDatabasePrefix "/mu");
73 }
74
persistentTitledDir(bool multiSession=true)75 FilePath persistentTitledDir(bool multiSession = true)
76 {
77 if (multiSession && options().multiSession() && options().programMode() == kSessionProgramModeServer)
78 {
79 std::string id = module_context::activeSession().id();
80 return sourceDatabaseRoot().completePath("per/t/" + id);
81 }
82 else
83 {
84 return sourceDatabaseRoot().completePath("per/t");
85 }
86 }
87
oldPersistentTitledDir()88 FilePath oldPersistentTitledDir()
89 {
90 FilePath oldPath = module_context::oldScopedScratchPath();
91 if (oldPath.exists())
92 return oldPath.completePath("source_database_v2/persistent/titled");
93 else
94 return FilePath();
95 }
96
persistentUntitledDir(bool multiSession=true)97 FilePath persistentUntitledDir(bool multiSession = true)
98 {
99 if (multiSession && options().multiSession() && options().programMode() == kSessionProgramModeServer)
100 {
101 std::string id = module_context::activeSession().id();
102 return sourceDatabaseRoot().completePath("per/u/" + id);
103 }
104 else
105 {
106 return sourceDatabaseRoot().completePath("per/u");
107 }
108 }
109
oldPersistentUntitledDir()110 FilePath oldPersistentUntitledDir()
111 {
112 FilePath oldPath = module_context::oldScopedScratchPath();
113 if (oldPath.exists())
114 return oldPath.completePath("source_database_v2/persistent/untitled");
115 else
116 return FilePath();
117 }
118
sessionLockFilePath(const FilePath & sessionDir)119 FilePath sessionLockFilePath(const FilePath& sessionDir)
120 {
121 return sessionDir.completePath("lock_file");
122 }
123
sessionSuspendFilePath(const FilePath & sessionDir)124 FilePath sessionSuspendFilePath(const FilePath& sessionDir)
125 {
126 return sessionDir.completePath("suspend_file");
127 }
128
sessionRestartFilePath(const FilePath & sessionDir)129 FilePath sessionRestartFilePath(const FilePath& sessionDir)
130 {
131 return sessionDir.completePath("restart_file");
132 }
133
134 // session dir lock (lock is acquired within 'attachToSourceDatabase()')
createSessionDirLock()135 boost::shared_ptr<FileLock> createSessionDirLock()
136 {
137 return FileLock::createDefault();
138 }
139
sessionDirLock()140 FileLock& sessionDirLock()
141 {
142 static boost::shared_ptr<FileLock> instance = createSessionDirLock();
143 return *instance;
144 }
145
removeSessionDir(const FilePath & sessionDir)146 Error removeSessionDir(const FilePath& sessionDir)
147 {
148 // first remove children
149 std::vector<FilePath> children;
150 Error error = sessionDir.getChildren(children);
151 if (error)
152 LOG_ERROR(error);
153 for (const FilePath& filePath : children)
154 {
155 error = filePath.remove();
156 if (error)
157 LOG_ERROR(error);
158 }
159
160 // then remove dir
161 return sessionDir.remove();
162 }
163
isNotSessionDir(const FilePath & filePath)164 bool isNotSessionDir(const FilePath& filePath)
165 {
166 return !filePath.isDirectory() || !boost::algorithm::starts_with(
167 filePath.getFilename(),
168 kSessionDirPrefix);
169 }
170
enumerateSessionDirs(std::vector<FilePath> * pSessionDirs)171 Error enumerateSessionDirs(std::vector<FilePath>* pSessionDirs)
172 {
173 // get the directories
174 Error error = sourceDatabaseRoot().getChildren(*pSessionDirs);
175 if (error)
176 return error;
177
178 // clean out non session dirs
179 pSessionDirs->erase(std::remove_if(pSessionDirs->begin(),
180 pSessionDirs->end(),
181 isNotSessionDir),
182 pSessionDirs->end());
183
184 // return success
185 return Success();
186 }
187
attemptToMoveSourceDbFiles(const FilePath & fromPath,const FilePath & toPath)188 void attemptToMoveSourceDbFiles(const FilePath& fromPath,
189 const FilePath& toPath)
190 {
191 // enumerate the from path
192 std::vector<FilePath> children;
193 Error error = fromPath.getChildren(children);
194 if (error)
195 LOG_ERROR(error);
196
197 // move the files
198 for (const FilePath& filePath : children)
199 {
200 // skip directories (directories can exist because multi-session
201 // mode writes top level directories into the /sdb/t and /sdb/u
202 // stores -- these directories correspond to the persistent docs
203 // of particular long-running sessions)
204 if (filePath.isDirectory())
205 continue;
206
207 // if the target path already exists then skip it and log
208 // (we used to generate a new uniqueFilePath however this
209 // caused the filename and id (stored in the source doc)
210 // to get out of sync, making documents unclosable. The
211 // chance of file with the same name already existing is
212 // close to zero (collision probability of uniqueFilePath)
213 // so it's no big deal to punt here.
214 FilePath targetPath = toPath.completePath(filePath.getFilename());
215 if (targetPath.exists())
216 {
217 LOG_WARNING_MESSAGE("Skipping source db move from: " +
218 filePath.getAbsolutePath() + " to " +
219 targetPath.getAbsolutePath());
220
221 Error error = filePath.remove();
222 if (error)
223 LOG_ERROR(error);
224
225 continue;
226 }
227
228 Error error = filePath.move(targetPath);
229 if (error)
230 LOG_ERROR(error);
231 }
232 }
233
234 // NOTE: the supervisor needs to return a session dir in order for the process
235 // to start. therefore, in the createSessionDir family of functions below
236 // once we successfully create and lock the session dir other errors (such
237 // as trying to move files into the session dir) are simply logged
238
createSessionDir()239 Error createSessionDir()
240 {
241 Error error = sessionDirPath().ensureDirectory();
242 if (error)
243 return error;
244
245 // attempt to acquire the lock. if we can't then we still continue
246 // so we can support filesystems that don't have file locks.
247 error = sessionDirLock().acquire(sessionLockFilePath(sessionDirPath()));
248 if (error)
249 LOG_ERROR(error);
250
251 return Success();
252 }
253
createSessionDirFromPersistent()254 Error createSessionDirFromPersistent()
255 {
256 // note whether we are in multi-session mode
257 bool multiSession = options().multiSession() && options().programMode() == kSessionProgramModeServer;
258
259 // create new session dir
260 Error error = createSessionDir();
261 if (error)
262 return error;
263
264 // move persistent titled files. if we don't have any this is a
265 // brand new instantiation of this project within this session
266 // so we try to grab the MRU list
267 if (persistentTitledDir().exists())
268 {
269 attemptToMoveSourceDbFiles(persistentTitledDir(), sessionDirPath());
270 }
271 // check for the most recent titled directory
272 else if (multiSession && mostRecentTitledDir().exists())
273 {
274 attemptToMoveSourceDbFiles(mostRecentTitledDir(), sessionDirPath());
275 }
276 // last resort: if we are in multi-session mode see if there is a set of
277 // mono-session source documents that we can migrate
278 else if (multiSession && persistentTitledDir(false).exists())
279 {
280 attemptToMoveSourceDbFiles(persistentTitledDir(false), sessionDirPath());
281 }
282
283 // get legacy titled docs if they exist
284 if (oldPersistentTitledDir().exists())
285 attemptToMoveSourceDbFiles(oldPersistentTitledDir(), sessionDirPath());
286
287 // move persistent untitled files
288 if (persistentUntitledDir().exists())
289 {
290 attemptToMoveSourceDbFiles(persistentUntitledDir(), sessionDirPath());
291 }
292 // check for the most recent untitled directory
293 else if (multiSession && mostRecentUntitledDir().exists())
294 {
295 attemptToMoveSourceDbFiles(mostRecentUntitledDir(), sessionDirPath());
296 }
297 // last resort: if we are in multi-session mode see if there is a set of
298 // mono-session source documents that we can migrate
299 else if (multiSession && persistentUntitledDir(false).exists())
300 {
301 attemptToMoveSourceDbFiles(persistentUntitledDir(false), sessionDirPath());
302 }
303
304 // get legacy untitled docs if they exist
305 if (oldPersistentUntitledDir().exists())
306 attemptToMoveSourceDbFiles(oldPersistentUntitledDir(), sessionDirPath());
307
308 // return success
309 return Success();
310 }
311
reclaimOrphanedSession()312 bool reclaimOrphanedSession()
313 {
314 // check for existing sessions
315 std::vector<FilePath> sessionDirs;
316 Error error = enumerateSessionDirs(&sessionDirs);
317 if (error)
318 {
319 LOG_ERROR(error);
320 return false;
321 }
322
323 for (const FilePath& sessionDir : sessionDirs)
324 {
325 // if the suspend file exists, this session is only sleeping, not dead
326 if (sessionSuspendFilePath(sessionDir).exists())
327 continue;
328
329 FilePath restartFile = sessionRestartFilePath(sessionDir);
330 if (restartFile.exists())
331 {
332 if (std::time(nullptr) - restartFile.getLastWriteTime() >
333 (1000 * 60 * 5))
334 {
335 // the file exists, but it's more than five minutes old, so
336 // something went wrong
337 Error error = restartFile.remove();
338 if (error)
339 LOG_ERROR(error);
340 }
341 else
342 {
343 // the restart file exists and is new, so it represents a
344 // session currently undergoing a suspend for restart -- leave
345 // it alone
346 continue;
347 }
348 }
349
350 FilePath lockFilePath = sessionLockFilePath(sessionDir);
351 if (!sessionDirLock().isLocked(lockFilePath))
352 {
353 // adopt by giving the session dir our own name
354 Error error = sessionDir.move(sessionDirPath());
355 if (error)
356 LOG_ERROR(error);
357 else
358 {
359 error = sessionDirLock().acquire(
360 sessionLockFilePath(sessionDirPath()));
361 if (!error)
362 {
363 return true;
364 }
365 else
366 {
367 LOG_ERROR(error);
368 }
369 }
370 }
371 }
372
373 return false;
374 }
375
removeAndRecreate(const FilePath & dir)376 Error removeAndRecreate(const FilePath& dir)
377 {
378 // blow it away if it exists then recreate it
379 Error error = dir.removeIfExists();
380 if (error)
381 LOG_ERROR(error);
382 return dir.ensureDirectory();
383 }
384
385 } // anonymous namespace
386
387
388 // NOTE: we attempt to use file locks to coordinate between disperate
389 // processes all attempting to open a session in the same context (project
390 // or global). Locks are used to implement recovery of crashed sessions
391 // as follows: if there is an existing source-db directory on disk that
392 // is NOT locked then it's presumed to be an orphan (resulting from a crash)
393 // and we should initialize with this directory to "recover" it
394 //
395 // Unfortunately, some file systems (mostly remote network volumes) don't
396 // support file-locking. In these cases we need to gracefully fall back
397 // to some sane behavior. To implement this we use the following scheme:
398 //
399 // (1) Always attempt to call FileLock::acquire to create an advisory lock
400 // but if it fails we still allow the process to start up.
401 //
402 // (2) When checking for "orphan" source-db directories we try to acquire
403 // a lock on them -- for volumes that don't support locks this will
404 // always be an error so we'll never be able to recover an orphan dir
405 //
406 // In some multi-machine cases it's actually possible for two proccesses
407 // to both get a lock on the same file. For this reason if we are running
408 // multi-machine (i.e. load balancing enabled) we don't attempt orphan
409 // recovery because it could result in one session stealing the other's
410 // source database out from under it.
411 //
412
attachToSourceDatabase()413 Error attachToSourceDatabase()
414 {
415 // this session may already have a source database; if it does, re-acquire a
416 // lock and then use it. don't log warnings as this should only fail when
417 // e.g. the filesystem does not support the active locking scheme
418 FilePath existingSdb = sessionDirPath();
419 if (existingSdb.exists())
420 {
421 Error error = sessionDirLock().acquire(sessionLockFilePath(existingSdb));
422 if (error)
423 {
424 LOG_ERROR(error);
425 }
426 else
427 {
428 return Success();
429 }
430 }
431
432 // migrate from 'sdb' to current folder layout if needed
433 bool needsSdbMigration =
434 !sourceDatabaseRoot().exists() &&
435 sdbSourceDatabaseRoot().exists();
436
437 if (needsSdbMigration)
438 {
439 Error error =
440 sdbSourceDatabaseRoot().copyDirectoryRecursive(sourceDatabaseRoot());
441
442 if (error)
443 LOG_ERROR(error);
444 }
445
446 // ensure the root path exists
447 Error error = sourceDatabaseRoot().ensureDirectory();
448 if (error)
449 return error;
450
451 // if there is an orphan (crash) then reclaim it.
452 if (reclaimOrphanedSession())
453 {
454 return Success();
455 }
456
457 // attempt to create from persistent
458 else
459 {
460 return createSessionDirFromPersistent();
461 }
462 }
463
464 // preserve documents for re-opening in a future session
saveMostRecentDocuments()465 Error saveMostRecentDocuments()
466 {
467 // only do this for multi-session contexts
468 if (options().multiSession() && options().programMode() == kSessionProgramModeServer)
469 {
470 // most recent docs is last one wins so we remove and recreate
471 FilePath mostRecentDir = mostRecentTitledDir();
472 Error error = removeAndRecreate(mostRecentDir);
473 if (error)
474 return error;
475
476 // untitled are aggregated (so we never lose unsaved docs)
477 // so we just ensure the directory exists)
478 FilePath mostRecentDirUntitled = mostRecentUntitledDir();
479 error = mostRecentDirUntitled.ensureDirectory();
480 if (error)
481 return error;
482
483 // list all current source docs
484 std::vector<boost::shared_ptr<SourceDocument> > sourceDocs;
485 error = source_database::list(&sourceDocs);
486 if (error)
487 return error;
488
489 // write the docs into the mru directories
490 for (boost::shared_ptr<SourceDocument> pDoc : sourceDocs)
491 {
492 FilePath targetDir = pDoc->isUntitled() ? mostRecentDirUntitled :
493 mostRecentDir;
494
495 Error error = pDoc->writeToFile(targetDir.completeChildPath(pDoc->id()));
496 if (error)
497 LOG_ERROR(error);
498 }
499 }
500
501 return Success();
502 }
503
detachFromSourceDatabase()504 Error detachFromSourceDatabase()
505 {
506 // list all current source docs
507 std::vector<boost::shared_ptr<SourceDocument> > sourceDocs;
508 Error error = source_database::list(&sourceDocs);
509 if (error)
510 return error;
511
512 // get references to persistent subdirs
513 FilePath titledDir = persistentTitledDir();
514 FilePath untitledDir = persistentUntitledDir();
515
516 // first blow away the existing persistent titled dir
517 error = titledDir.removeIfExists();
518 if (error)
519 LOG_ERROR(error);
520
521 // ensure both directories exist -- if they don't it is a fatal error
522 error = titledDir.ensureDirectory();
523 if (error)
524 return error;
525 error = untitledDir.ensureDirectory();
526 if (error)
527 return error;
528
529 // now write the source database entries to the appropriate places
530 for (boost::shared_ptr<SourceDocument> pDoc : sourceDocs)
531 {
532 if (pDoc->isUntitled())
533 {
534 // compute the target path (manage uniqueness since this
535 // directory is appended to from multiple processes who
536 // could have created docs with the same id)
537 FilePath targetPath = untitledDir.completePath(pDoc->id());
538 if (targetPath.exists())
539 targetPath = file_utils::uniqueFilePath(untitledDir);
540
541 error = pDoc->writeToFile(targetPath);
542 if (error)
543 LOG_ERROR(error);
544 }
545 else
546 {
547 error = pDoc->writeToFile(titledDir.completePath(pDoc->id()));
548 if (error)
549 LOG_ERROR(error);
550 }
551 }
552
553 // record session dir (parent of lock file)
554 FilePath sessionDir = sessionDirLock().lockFilePath().getParent();
555
556 // give up our lock
557 error = sessionDirLock().release();
558 if (error)
559 LOG_ERROR(error);
560
561 // remove the session directory
562 return removeSessionDir(sessionDir);
563 }
564
sessionDirPath()565 FilePath sessionDirPath()
566 {
567 return sourceDatabaseRoot().completePath(
568 kSessionDirPrefix +
569 module_context::activeSession().id());
570 }
571
suspendSourceDatabase(int status)572 void suspendSourceDatabase(int status)
573 {
574 // write a sentinel so we can differentiate between a sdb that's orphaned
575 // from a crash, and an sdb that represents a suspended session
576 Error error = (status == EX_SUSPEND_RESTART_LAUNCHER_SESSION ||
577 status == EX_CONTINUE) ?
578 sessionRestartFilePath(sessionDirPath()).ensureFile() :
579 sessionSuspendFilePath(sessionDirPath()).ensureFile();
580 if (error)
581 LOG_ERROR(error);
582 }
583
resumeSourceDatabase()584 void resumeSourceDatabase()
585 {
586 Error error = sessionSuspendFilePath(sessionDirPath()).removeIfExists();
587 if (error)
588 LOG_ERROR(error);
589 error = sessionRestartFilePath(sessionDirPath()).removeIfExists();
590 if (error)
591 LOG_ERROR(error);
592 }
593
594 } // namespace supervisor
595 } // namespace source_database
596 } // namespace session
597 } // namespace rstudio
598
599
600
601