Vuo 2.4.4
Loading...
Searching...
No Matches
VuoModuleCache.cc
Go to the documentation of this file.
1
10#include "VuoModuleCache.hh"
11
12#include "VuoCompiler.hh"
13#include "VuoCompilerIssue.hh"
14#include "VuoException.hh"
16#include "VuoModuleInfo.hh"
17#include "VuoStringUtilities.hh"
18#include "VuoTimeUtilities.hh"
19#include <climits>
20#include <sstream>
21#include <CoreFoundation/CoreFoundation.h>
22
23std::mutex VuoModuleCache::buildMutex;
24int VuoModuleCache::buildsInProgress = 0;
25std::condition_variable VuoModuleCache::buildsInProgressCondition;
26std::mutex VuoModuleCache::buildsInProgressMutex;
27const string VuoModuleCache::builtInCacheDirName = "Builtin";
28const string VuoModuleCache::systemCacheDirName = "System";
29const string VuoModuleCache::userCacheDirName = "User";
30const string VuoModuleCache::pidCacheDirPrefix = "pid-";
31map<string, VuoModuleCache::LockInfo *> VuoModuleCache::interprocessLockInfo;
32
36VuoModuleCache::LockInfo::LockInfo(void)
37{
38 needsLock = true;
39 dylibLockFile = nullptr;
40 compiledModulesLockFile = nullptr;
41 hasLockForWriting = false;
42}
43
47VuoModuleCache::LockInfo::~LockInfo(void)
48{
49 if (dylibLockFile)
50 dylibLockFile->unlock();
51
52 if (compiledModulesLockFile)
53 compiledModulesLockFile->unlock();
54
55 delete dylibLockFile;
56 delete compiledModulesLockFile;
57}
58
62shared_ptr<VuoModuleCache> VuoModuleCache::newBuiltInCache(void)
63{
64 try
65 {
66 return shared_ptr<VuoModuleCache>(new VuoModuleCache(VuoCompiler::getVuoFrameworkPath() + "/Modules/" + builtInCacheDirName, true));
67 }
68 catch (VuoException &e)
69 {
70 return nullptr;
71 }
72}
73
77shared_ptr<VuoModuleCache> VuoModuleCache::newSystemCache(void)
78{
79 try
80 {
81 return shared_ptr<VuoModuleCache>(new VuoModuleCache(VuoFileUtilities::getCachePath() + "/" + systemCacheDirName, false));
82 }
83 catch (VuoException &e)
84 {
85 return nullptr;
86 }
87}
88
92shared_ptr<VuoModuleCache> VuoModuleCache::newUserCache(void)
93{
94 try
95 {
96 return shared_ptr<VuoModuleCache>(new VuoModuleCache(VuoFileUtilities::getCachePath() + "/" + userCacheDirName, false));
97 }
98 catch (VuoException &e)
99 {
100 return nullptr;
101 }
102}
103
109shared_ptr<VuoModuleCache> VuoModuleCache::newCache(const string &uniquePath)
110{
111 try
112 {
113 return shared_ptr<VuoModuleCache>(new VuoModuleCache(VuoModuleCache::getCacheDirectoryPath(uniquePath), false));
114 }
115 catch (VuoException &e)
116 {
117 return nullptr;
118 }
119}
120
130VuoModuleCache::VuoModuleCache(const string &cacheDirectoryPath, bool builtIn)
131{
132 this->cacheDirectoryPath = cacheDirectoryPath;
133 VuoFileUtilities::canonicalizePath(this->cacheDirectoryPath);
134 this->builtIn = builtIn;
135 available = false;
136 invalidated = true;
137 lastRebuild = 0;
138 hasInterprocessLock = false;
139
140 LockInfo *lockInfo = interprocessLockInfo[cacheDirectoryPath];
141 if (! lockInfo)
142 {
143 VuoFileUtilities::makeDir(cacheDirectoryPath);
144
145 lockInfo = new LockInfo;
146 interprocessLockInfo[cacheDirectoryPath] = lockInfo;
147
148 if (builtIn)
149 {
150 lockInfo->needsLock = false;
151 }
152 else
153 {
154 string lockFileName = "modules.lock";
155 VuoFileUtilities::createFile(cacheDirectoryPath + "/" + lockFileName);
156
157 lockInfo->compiledModulesLockFile = new VuoFileUtilities::File(cacheDirectoryPath, lockFileName);
158 lockInfo->hasLockForWriting = lockInfo->compiledModulesLockFile->lockForWriting(true);
159
160 if (lockInfo->hasLockForWriting)
161 {
162 ostringstream pid;
163 pid << getpid() << endl;
164 VuoFileUtilities::writeStringToFile(pid.str(), lockInfo->compiledModulesLockFile->path());
165 }
166 }
167 }
168
169 hasInterprocessLock = ! lockInfo->needsLock || lockInfo->hasLockForWriting;
170}
171
178string VuoModuleCache::getCacheDirectoryPath(const string &uniquePath)
179{
180 string modifierLetterColon("꞉"); // Unicode "Modifier Letter Colon", not a regular ASCII colon. OK to include in a file name.
181 string relativePath = uniquePath.empty() ? "/" : uniquePath;
182 VuoStringUtilities::replaceAll(relativePath, "/", modifierLetterColon);
183 return VuoFileUtilities::getCachePath() + "/" + relativePath;
184}
185
189string VuoModuleCache::getRelativePathOfModulesDirectory(bool isGenerated, const string &targetArch)
190{
191 return string() + (isGenerated ? "Generated" : "Installed") + "/Modules/" + targetArch;
192}
193
197string VuoModuleCache::getCompiledModulesPath(bool isGenerated, const string &targetArch)
198{
199 return cacheDirectoryPath + "/" + getRelativePathOfModulesDirectory(isGenerated, targetArch);
200}
201
205string VuoModuleCache::getOverriddenCompiledModulesPath(bool isGenerated, const string &targetArch)
206{
207 ostringstream pid;
208 pid << getpid();
209
210 string dir, scope, unused;
211 VuoFileUtilities::splitPath(cacheDirectoryPath, dir, scope, unused);
213
214 return VuoFileUtilities::getCachePath() + "/" + pidCacheDirPrefix + pid.str() + "/" + scope + "/" + getRelativePathOfModulesDirectory(isGenerated, targetArch);
215}
216
224bool VuoModuleCache::modifyCompiledModules(std::function<bool(void)> modify)
225{
226 if (hasInterprocessLock)
227 return modify();
228
229 return false;
230}
231
235string VuoModuleCache::getDescription(void)
236{
237 return string() + "the module cache at '" + cacheDirectoryPath + "'";
238}
239
243string VuoModuleCache::getManifestPath(const string &targetArch)
244{
245 string path = cacheDirectoryPath + "/manifest.txt";
246
247 // Give the manifest for each built-in cache arch a unique name, so they can be built in parallel.
248 if (builtIn && ! targetArch.empty())
249 path += "-" + targetArch;
250
251 return path;
252}
253
259string VuoModuleCache::getDylibPath(const string &targetArch)
260{
261 string partialPath = cacheDirectoryPath + "/libVuoModuleCache";
262
263 if (builtIn)
264 {
265 string path = partialPath + ".dylib";
266
267 // Give the dylib for each built-in cache arch a unique name, so they can be built in parallel.
268 if (! targetArch.empty())
269 path += "-" + targetArch;
270
271 return path;
272 }
273
274 auto isPathAvailable = [](const string &path)
275 {
276 return ! VuoFileUtilities::fileExists(path + ".dylib");
277 };
278
279 string preferredPath = partialPath + "-" + VuoTimeUtilities::getCurrentDateTimeForFileName();
280 string uniquePath = VuoStringUtilities::formUniqueIdentifier(isPathAvailable, preferredPath, preferredPath + "-");
281 return uniquePath + ".dylib";
282}
283
287string VuoModuleCache::findLatestRevisionOfDylib(double &lastModified)
288{
289 if (builtIn)
290 throw VuoException("Only call this function on a non-built-in module cache.");
291
292 string hypotheticalPath = getDylibPath();
293
294 string dir, file, ext;
295 VuoFileUtilities::splitPath(hypotheticalPath, dir, file, ext);
296 string hypotheticalFileName = file + "." + ext;
297
298 vector< pair<string, double> > existingRevisions;
299 for (VuoFileUtilities::File *existingFile : VuoFileUtilities::findFilesInDirectory(cacheDirectoryPath, {"dylib"}))
300 {
301 if (areDifferentRevisionsOfSameDylib(existingFile->getRelativePath(), hypotheticalFileName))
302 existingRevisions.push_back({existingFile->path(), VuoFileUtilities::getFileLastModifiedInSeconds(existingFile->path())});
303
304 delete existingFile;
305 }
306
307 if (existingRevisions.empty())
308 {
309 lastModified = 0;
310 return "";
311 }
312
313 auto latest = std::max_element(existingRevisions.begin(), existingRevisions.end(),
314 [](pair<string, double> p1, pair<string, double> p2) { return p1.second < p2.second; });
315
316 lastModified = latest->second;
317 return latest->first;
318}
319
323bool VuoModuleCache::areDifferentRevisionsOfSameDylib(const string &dylibPath1, const string &dylibPath2)
324{
325 string dir1, dir2, file1, file2, ext;
326 VuoFileUtilities::splitPath(dylibPath1, dir1, file1, ext);
327 VuoFileUtilities::splitPath(dylibPath2, dir2, file2, ext);
328
329 if (VuoFileUtilities::arePathsEqual(dir1, dir2))
330 {
331 vector<string> parts1 = VuoStringUtilities::split(file1, '-');
332 vector<string> parts2 = VuoStringUtilities::split(file2, '-');
333
334 if (parts1.size() == 2 && parts2.size() == 2)
335 return parts1.front() == parts2.front() && parts1.back() != parts2.back();
336 }
337
338 return false;
339}
340
371void VuoModuleCache::makeAvailable(bool shouldUseExistingCache,
372 vector<shared_ptr<VuoModuleCache>> prerequisiteModuleCaches, double &lastPrerequisiteModuleCacheRebuild,
373 const VuoModuleCacheManifest &expectedManifest, vector<VuoModuleInfoIterator> expectedModules,
374 const set<string> &dylibsToLinkTo, const set<string> &frameworksToLinkTo, const vector<string> &runPathSearchPaths,
375 VuoCompiler *compiler, const string &targetArch)
376{
377 string cacheDescription = getDescription();
378
379 std::unique_lock<std::mutex> statusLock(statusMutex);
380
381 // Don't bother rechecking the cache if neither the cacheable modules nor the caches that it depends on have changed.
382
383 if (! invalidated && (builtIn || lastRebuild >= lastPrerequisiteModuleCacheRebuild))
384 {
385 VDebugLog("No need to recheck %s.", cacheDescription.c_str());
386 lastPrerequisiteModuleCacheRebuild = lastRebuild;
387 return;
388 }
389
390 try
391 {
392 VDebugLog("Checking if %s is up-to-date…", cacheDescription.c_str());
393
394 bool isCacheUpToDate = true;
395 invalidated = false;
396
397 statusLock.unlock();
398
399 string manifestPath = getManifestPath(targetArch);
400
401 // Lock the cache for reading.
402
403 LockInfo *lockInfo = interprocessLockInfo[cacheDirectoryPath];
404 VuoFileUtilities::File *fileForLocking = nullptr;
405
406 if (lockInfo->needsLock)
407 {
408 if (! lockInfo->dylibLockFile)
409 {
410 VuoFileUtilities::makeDir(cacheDirectoryPath);
411
412 string lockFileName = "dylib.lock";
413 VuoFileUtilities::createFile(cacheDirectoryPath + "/" + lockFileName);
414
415 lockInfo->dylibLockFile = new VuoFileUtilities::File(cacheDirectoryPath, lockFileName);
416 }
417
418 fileForLocking = lockInfo->dylibLockFile;
419 if (! fileForLocking->lockForReading())
420 VDebugLog("\tWarning: Couldn't lock for reading.");
421 }
422
423 // Create the manifest file if it doesn't already exist. (If it does exist, don't affect the last-modified time.)
424
425 bool manifestFileExists = VuoFileUtilities::fileExists(manifestPath);
426 if (! manifestFileExists)
427 {
428 if (shouldUseExistingCache)
429 throw VuoException("Trying to use the existing cache, but the cache manifest doesn't exist.", false);
430
431 VuoFileUtilities::createFile(manifestPath);
432 isCacheUpToDate = false;
433 }
434
435 // If this is the first time making the cache available, see if there's a dylib on disk.
436
437 double lastRebuild_local = 0;
438 string dylibPath = builtIn ? getDylibPath(targetArch) : findLatestRevisionOfDylib(lastRebuild_local);
439
440 if (dylibPath.empty())
441 {
442 if (shouldUseExistingCache)
443 throw VuoException("Trying to use the existing cache, but the cache dylib doesn't exist.", false);
444
445 dylibPath = getDylibPath();
446 }
447
448 // Check if the dylib is newer than the other caches that it depends on.
449
450 if (isCacheUpToDate)
451 isCacheUpToDate = lastRebuild_local >= lastPrerequisiteModuleCacheRebuild;
452
453 // Check if the dylib looks remotely valid.
454
455 if (isCacheUpToDate)
456 {
457 bool dylibHasData = VuoFileUtilities::fileContainsReadableData(dylibPath);
458 if (! dylibHasData)
459 {
460 if (shouldUseExistingCache)
461 throw VuoException("Trying to use the existing cache, but the cache doesn't contain readable data.", false);
462 else
463 isCacheUpToDate = false;
464 }
465 }
466
467 // List the items actually in the cache, according to its manifest.
468
469 VuoModuleCacheManifest actualManifest;
470 bool usingExistingCache = false;
471
472 if (isCacheUpToDate || shouldUseExistingCache)
473 {
474 actualManifest.readFromFile(manifestPath);
475
476 if (shouldUseExistingCache)
477 usingExistingCache = true;
478 }
479
480 // Check if the list of actual items matches the list of expected items.
481
482 if (isCacheUpToDate && ! usingExistingCache)
483 {
484 if (! actualManifest.hasSameContentsAs(expectedManifest))
485 isCacheUpToDate = false;
486 }
487
488 // Check if the cache is newer than all of the modules in it.
489
490 if (isCacheUpToDate && ! usingExistingCache)
491 {
492 for (VuoModuleInfoIterator i : expectedModules)
493 {
494 VuoModuleInfo *moduleInfo;
495 while ((moduleInfo = i.next()))
496 {
497 if (! moduleInfo->isOlderThan(lastRebuild_local))
498 {
499 isCacheUpToDate = false;
500 break;
501 }
502 }
503 }
504 }
505
506 // If the cache is up-to-date, we're done.
507
508 if (isCacheUpToDate || usingExistingCache)
509 {
510 VDebugLog("Up-to-date.");
511
512 lastPrerequisiteModuleCacheRebuild = lastRebuild_local;
513
514 std::lock_guard<std::mutex> contentsLock(contentsMutex);
515 if (! currentRevision)
516 currentRevision = VuoModuleCacheRevision::createAndUse(dylibPath, actualManifest, builtIn, ! builtIn);
517
518 std::lock_guard<std::mutex> statusLock(statusMutex);
519 lastRebuild = lastRebuild_local;
520 if (! invalidated)
521 available = true;
522
523 return;
524 }
525
526 // If this process is not allowed to rebuild the cache, give up.
527
528 if (! hasInterprocessLock)
529 throw VuoException("The cache file is out-of-date but can't be rebuilt because it's being used by another process. "
530 "If any composition windows are open from previous Vuo sessions, quit them. "
531 "If any processes whose names start with \"VuoComposition\" or one of your composition file names appear in Activity Monitor, force-quit them.",
532 false);
533
534 // Otherwise, (re)build the cache asynchronously.
535
536 lastPrerequisiteModuleCacheRebuild = ULONG_MAX;
537
538 {
539 std::lock_guard<std::mutex> contentsLock(contentsMutex);
540 if (currentRevision)
541 {
542 currentRevision->disuse();
543 currentRevision = nullptr;
544 }
545 else if (VuoFileUtilities::fileExists(dylibPath))
546 {
547 // Clean up the dylib that existed before making this VuoModuleCache available for the first time.
548 auto revision = VuoModuleCacheRevision::createAndUse(dylibPath, actualManifest, builtIn, true);
549 revision->disuse();
550 }
551
552 std::lock_guard<std::mutex> statusLock(statusMutex);
553 available = false;
554 }
555
556 auto rebuild = [this, prerequisiteModuleCaches, expectedManifest, dylibsToLinkTo, frameworksToLinkTo, runPathSearchPaths, compiler, targetArch,
557 cacheDescription, manifestPath, fileForLocking]()
558 {
559 std::lock_guard<std::mutex> buildLock(buildMutex);
560
561 // Wait for other caches that this cache depends on to finish rebuilding so their dylibs are available to link to.
562 vector<shared_ptr<VuoModuleCacheRevision>> prerequisiteModuleCacheRevisions;
563 bool arePrerequisiteModuleCachesAvailable = true;
564 for (shared_ptr<VuoModuleCache> other : prerequisiteModuleCaches)
565 {
566 shared_ptr<VuoModuleCacheRevision> revision = other->useCurrentRevision();
567 if (! revision)
568 {
569 arePrerequisiteModuleCachesAvailable = false;
570 break;
571 }
572
573 prerequisiteModuleCacheRevisions.push_back(revision);
574 }
575
576 if (arePrerequisiteModuleCachesAvailable)
577 {
578 VUserLog("Rebuilding %s…", cacheDescription.c_str());
579
580 bool gotLockForWriting = false;
581 try
582 {
583 std::lock_guard<std::mutex> contentsLock(contentsMutex);
584
585 string dylibPath = getDylibPath(targetArch);
586
587 VuoLinkerInputs linkerInputs;
588 linkerInputs.addDependencies(expectedManifest.getContents(), {}, compiler);
589 linkerInputs.addExternalLibraries(dylibsToLinkTo);
590 linkerInputs.addFrameworks(frameworksToLinkTo);
591
592 for (shared_ptr<VuoModuleCacheRevision> revision : prerequisiteModuleCacheRevisions)
593 linkerInputs.addExternalLibrary(revision->getDylibPath());
594
595 // Try to upgrade the file lock for writing.
596 if (fileForLocking)
597 {
598 gotLockForWriting = fileForLocking->lockForWriting(true);
599 if (! gotLockForWriting)
600 throw VuoException("Couldn't upgrade the lock to writing.", false);
601
602 ostringstream pid;
603 pid << getpid() << endl;
604 VuoFileUtilities::writeStringToFile(pid.str(), fileForLocking->path());
605 }
606
607 // Link the dependencies to create the cache dylib in a temporary file.
608 string dir, file, ext;
609 VuoFileUtilities::splitPath(dylibPath, dir, file, ext);
610 string tmpPath = VuoFileUtilities::makeTmpFile(file, "dylib");
612 compiler->link(tmpPath, linkerInputs, true, runPathSearchPaths, false, issues);
613
614 if (! issues->isEmpty())
615 VUserLog("Warning: May not have fully rebuilt %s:\n%s", cacheDescription.c_str(), issues->getLongDescription(false).c_str());
616
617 // Move the temporary file into the cache.
618 VuoFileUtilities::moveFile(tmpPath, dylibPath);
619
620 // Change the dylib's ID from the temporary path to the path within the cache.
622 VuoCompiler::getVuoFrameworkPath() + "/Helpers/install_name_tool",
623 "-id",
624 dylibPath,
625 dylibPath,
626 });
627
628 // Ad-hoc code-sign the runtime-generated System and User caches,
629 // but don't ad-hoc code-sign the buildtime-generated Builtin module cache
630 // since `framework/CMakeLists.txt` later changes its ID/rpath/loads.
631 if (VuoCompiler::vuoFrameworkInProgressPath.empty())
632 VuoCompiler::adHocCodeSign(dylibPath);
633
634 // Write the list of dependencies to the manifest file.
635 expectedManifest.writeToFile(manifestPath);
636
637 // Downgrade the file lock back to reading.
638 if (fileForLocking && ! fileForLocking->lockForReading())
639 VDebugLog("\tWarning: Couldn't downgrade the lock back to reading.");
640
641 currentRevision = VuoModuleCacheRevision::createAndUse(dylibPath, expectedManifest, builtIn, ! builtIn);
642
643 std::lock_guard<std::mutex> statusLock(statusMutex);
644
645 if (! builtIn)
646 lastRebuild = VuoFileUtilities::getFileLastModifiedInSeconds(dylibPath);
647
648 if (! invalidated)
649 available = true;
650 }
651 catch (VuoException &e)
652 {
653 // Downgrade the file lock back to reading.
654 if (gotLockForWriting)
655 if (! fileForLocking->lockForReading())
656 VDebugLog("\tWarning: Couldn't downgrade the lock back to reading.");
657
658 VUserLog("Warning: Couldn't rebuild %s: %s", cacheDescription.c_str(), e.what());
659 }
660 }
661 else
662 {
663 VDebugLog("Not rebuilding %s since one of the module caches it depends on is unavailable.", cacheDescription.c_str());
664 }
665
666 for (shared_ptr<VuoModuleCacheRevision> revision : prerequisiteModuleCacheRevisions)
667 revision->disuse();
668
669 {
670 std::unique_lock<std::mutex> lck(buildsInProgressMutex);
671 --buildsInProgress;
672 buildsInProgressCondition.notify_all();
673 }
674
675 VUserLog("Done.");
676 };
677
678 {
679 std::lock_guard<std::mutex> lck(buildsInProgressMutex);
680 ++buildsInProgress;
681 }
682
683 // This function is executing on environmentQueue. The `rebuild` lambda cannot execute on environmentQueue
684 // since it calls VuoCompiler::getLinkerInputs(). So, spawn a separate thread to get off of environmentQueue.
685 std::thread t(rebuild);
686 t.detach();
687 }
688 catch (VuoException &e)
689 {
690 if (VuoCompiler::vuoFrameworkInProgressPath.empty())
691 VUserLog("Warning: Couldn't make %s available: %s", cacheDescription.c_str(), e.what());
692 else if (builtIn)
693 VUserLog("Warning: Couldn't create %s when generating built-in module caches: %s", cacheDescription.c_str(), e.what());
694
695 lastPrerequisiteModuleCacheRebuild = ULONG_MAX;
696 }
697}
698
703{
704 std::unique_lock<std::mutex> lck(buildsInProgressMutex);
705 while (buildsInProgress > 0)
706 buildsInProgressCondition.wait(lck);
707}
708
715shared_ptr<VuoModuleCacheRevision> VuoModuleCache::useCurrentRevision(void)
716{
717 std::lock_guard<std::mutex> contentsLock(contentsMutex);
718 std::lock_guard<std::mutex> statusLock(statusMutex);
719
720 if (available)
721 {
722 currentRevision->use();
723 return currentRevision;
724 }
725
726 return nullptr;
727}
728
733{
734 std::lock_guard<std::mutex> statusLock(statusMutex);
735
736 invalidated = true;
737 available = false;
738}
739
746{
747 double maxSeconds = 30 * 24 * 60 * 60; // 30 days
748
749 set<VuoFileUtilities::File *> cacheDirs = VuoFileUtilities::findAllFilesInDirectory(VuoFileUtilities::getCachePath());
750 for (set<VuoFileUtilities::File *>::iterator i = cacheDirs.begin(); i != cacheDirs.end(); ++i)
751 {
752 string path = (*i)->path();
753
754 string file = (*i)->basename();
755 if (file != builtInCacheDirName && file != systemCacheDirName && file != userCacheDirName)
756 {
757 double fileSeconds = VuoFileUtilities::getSecondsSinceFileLastAccessed(path);
758 if (fileSeconds > maxSeconds)
760 }
761
762 if (VuoStringUtilities::beginsWith(file, pidCacheDirPrefix))
763 {
764 string pidAsString = file.substr(pidCacheDirPrefix.length());
765 int pid = atoi(pidAsString.c_str());
766 if (kill(pid, 0) != 0) // no running process has this pid
768 }
769
770 delete *i;
771 }
772}