Vuo  2.2.0
VuoRunner.cc
Go to the documentation of this file.
1 
10 #include "VuoRunner.hh"
11 #include "VuoFileUtilities.hh"
12 #include "VuoStringUtilities.hh"
13 #include "VuoEventLoop.h"
14 #include "VuoException.hh"
15 #include "VuoRuntime.h"
17 
18 #include <CoreServices/CoreServices.h>
19 #include <pthread.h>
20 #include <stdio.h>
21 #include <dlfcn.h>
22 #include <sstream>
23 #include <copyfile.h>
24 #include <mach-o/loader.h>
25 #include <sys/proc_info.h>
26 #include <sys/stat.h>
27 
28 void *VuoApp_mainThread = NULL;
29 static const char *mainThreadChecker = "/Applications/Xcode.app/Contents/Developer/usr/lib/libMainThreadChecker.dylib";
31 static bool VuoRunner_isHostVDMX = false;
32 
37 static void VuoRunner_closeOnExec(int fd)
38 {
39  int flags = fcntl(fd, F_GETFD);
40  if (flags < 0)
41  {
42  VUserLog("Error: Couldn't get flags for desciptor %d: %s", fd, strerror(errno));
43  return;
44  }
45 
46  flags |= FD_CLOEXEC;
47 
48  if (fcntl(fd, F_SETFD, flags) != 0)
49  VUserLog("Error: Couldn't set FD_CLOEXEC on descriptor %d: %s", fd, strerror(errno));
50 }
51 
55 static void __attribute__((constructor)) VuoRunner_init()
56 {
57  VuoApp_mainThread = (void *)pthread_self();
58 
59 #pragma clang diagnostic push
60 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
61  // Calls _TSGetMainThread().
62  // https://b33p.net/kosada/node/12944
63  YieldToAnyThread();
64 #pragma clang diagnostic pop
65 
67 
68  // Ensure that the write end of this pipe gets closed upon fork()/exec(),
69  // so child processes don't prop open this pipe,
70  // which would prevent Vuo compositions from quitting when the VuoRunner process quits.
72 
73  if (VuoStringUtilities::makeFromCFString(CFBundleGetIdentifier(CFBundleGetMainBundle())) == "com.vidvox.VDMX5")
74  VuoRunner_isHostVDMX = true;
75 }
76 
80 static bool isMainThread(void)
81 {
82  return VuoApp_mainThread == (void *)pthread_self();
83 }
84 
88 static void VuoRunner_configureSocket(void *zmqSocket, int timeoutInSeconds)
89 {
90  if (timeoutInSeconds >= 0)
91  {
92  int timeoutInMilliseconds = timeoutInSeconds * 1000;
93  zmq_setsockopt(zmqSocket, ZMQ_RCVTIMEO, &timeoutInMilliseconds, sizeof timeoutInMilliseconds);
94  zmq_setsockopt(zmqSocket, ZMQ_SNDTIMEO, &timeoutInMilliseconds, sizeof timeoutInMilliseconds);
95  }
96 
97  int linger = 0; // avoid having zmq_term block if the runner has tried to send a message on a broken connection
98  zmq_setsockopt(zmqSocket, ZMQ_LINGER, &linger, sizeof linger);
99 }
100 
105 {
106 public:
107  Private() :
108  lastWidth(0),
109  lastHeight(0)
110  {
111  }
112 
114  typedef void *(*vuoImageMakeFromJsonWithDimensionsType)(json_object *, unsigned int, unsigned int);
116  typedef json_object *(*vuoImageGetJsonType)(void *);
118 
119  uint64_t lastWidth;
120  uint64_t lastHeight;
121 };
122 
133 VuoRunner * VuoRunner::newSeparateProcessRunnerFromExecutable(string executablePath, string sourceDir,
134  bool continueIfRunnerDies, bool deleteExecutableWhenFinished)
135 {
136  VuoRunner * vr = new VuoRunner();
137  vr->executablePath = executablePath;
138  vr->shouldContinueIfRunnerDies = continueIfRunnerDies;
139  vr->shouldDeleteBinariesWhenFinished = deleteExecutableWhenFinished;
140  vr->sourceDir = sourceDir;
141  return vr;
142 }
143 
159 VuoRunner * VuoRunner::newSeparateProcessRunnerFromDynamicLibrary(string compositionLoaderPath, string compositionDylibPath,
160  const std::shared_ptr<VuoRunningCompositionLibraries> &runningCompositionLibraries,
161  string sourceDir, bool continueIfRunnerDies, bool deleteDylibsWhenFinished)
162 {
163  VuoRunner * vr = new VuoRunner();
164  vr->executablePath = compositionLoaderPath;
165  vr->dylibPath = compositionDylibPath;
166  vr->dependencyLibraries = runningCompositionLibraries;
167  vr->sourceDir = sourceDir;
168  vr->shouldContinueIfRunnerDies = continueIfRunnerDies;
169  vr->shouldDeleteBinariesWhenFinished = deleteDylibsWhenFinished;
170  runningCompositionLibraries->setDeleteResourceLibraries(deleteDylibsWhenFinished);
171  return vr;
172 }
173 
184  bool deleteDylibWhenFinished)
185 {
186  VuoRunner * vr = new VuoRunner();
187  vr->dylibPath = dylibPath;
188  vr->shouldDeleteBinariesWhenFinished = deleteDylibWhenFinished;
189  vr->sourceDir = sourceDir;
190  return vr;
191 }
192 
199 {
200  dispatch_release(stoppedSemaphore);
201  dispatch_release(terminatedZMQContextSemaphore);
202  dispatch_release(beganListeningSemaphore);
203  dispatch_release(endedListeningSemaphore);
204  dispatch_release(lastFiredEventSemaphore);
205  dispatch_release(delegateQueue);
206  delete p;
207 }
208 
213 void VuoRunner::setRuntimeChecking(bool runtimeCheckingEnabled)
214 {
215  if (!stopped)
216  {
217  VUserLog("Error: Only call VuoRunner::setRuntimeChecking() prior to starting the composition.");
218  return;
219  }
220 
221  isRuntimeCheckingEnabled = runtimeCheckingEnabled && VuoFileUtilities::fileExists(mainThreadChecker);
222 }
223 
227 VuoRunner::VuoRunner(void)
228 {
229  p = new Private;
230  dylibHandle = NULL;
231  dependencyLibraries = NULL;
232  shouldContinueIfRunnerDies = false;
233  shouldDeleteBinariesWhenFinished = false;
234  isRuntimeCheckingEnabled = false;
235  paused = true;
236  stopped = true;
237  lostContact = false;
238  listenCanceled = false;
239  stoppedSemaphore = dispatch_semaphore_create(1);
240  terminatedZMQContextSemaphore = dispatch_semaphore_create(0);
241  beganListeningSemaphore = dispatch_semaphore_create(0);
242  endedListeningSemaphore = dispatch_semaphore_create(1);
243  lastFiredEventSemaphore = dispatch_semaphore_create(0);
244  lastFiredEventSignaled = false;
245  controlQueue = dispatch_queue_create("org.vuo.runner.control", NULL);
246  ZMQContext = NULL;
247  ZMQSelfSend = NULL;
248  ZMQSelfReceive = NULL;
249  ZMQControl = NULL;
250  ZMQTelemetry = NULL;
251  ZMQLoaderControl = NULL;
252  delegate = NULL;
253  delegateQueue = dispatch_queue_create("org.vuo.runner.delegate", NULL);
254  arePublishedInputPortsCached = false;
255  arePublishedOutputPortsCached = false;
256 }
257 
274 {
275  try
276  {
277  startInternal();
278 
279  if (isInCurrentProcess())
280  {
281  dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
282  dispatch_async(queue, ^{
283  unpause();
284  });
285  while (paused)
286  {
288  usleep(USEC_PER_SEC / 1000);
289  }
290  }
291  else
292  {
293  unpause();
294  }
295  }
296  catch (VuoException &e)
297  {
298  stopBecauseLostContact(e.what());
299  }
300 }
301 
321 {
322  try
323  {
324  startInternal();
325  }
326  catch (VuoException &e)
327  {
328  stopBecauseLostContact(e.what());
329  }
330 }
331 
336 void VuoRunner::copyDylibAndChangeId(string dylibPath, string &outputDylibPath)
337 {
338  string directory, file, extension;
339  VuoFileUtilities::splitPath(dylibPath, directory, file, extension);
340 
341  const int makeTmpFileExtension = 7;
342  if (file.length() > makeTmpFileExtension)
343  {
344  // makeTmpFile() appends "-XXXXXX"; make room for that.
345  string trimmedFile = file.substr(0, file.length() - makeTmpFileExtension);
346 
347  bool alreadyLoaded;
348  do
349  {
350  outputDylibPath = VuoFileUtilities::makeTmpFile(trimmedFile, "dylib");
351  alreadyLoaded = dlopen(outputDylibPath.c_str(), RTLD_NOLOAD);
352  } while (alreadyLoaded);
353  }
354  else
355  {
356  // For short names, like those generated by VDMX, just replace the entire name with a hash.
357  // https://b33p.net/kosada/node/12917
358  bool alreadyLoaded;
359  do
360  {
361  string hash = VuoStringUtilities::makeRandomHash(file.length());
362  outputDylibPath = "/tmp/" + hash + ".dylib";
363  alreadyLoaded = dlopen(outputDylibPath.c_str(), RTLD_NOLOAD);
364  } while (alreadyLoaded);
365  }
366 
367  string newDirectory, newFile, newExtension;
368  VuoFileUtilities::splitPath(outputDylibPath, newDirectory, newFile, newExtension);
369 
370  if (newFile.length() > file.length())
371  throw VuoException("The composition couldn't start because the uniqued dylib name (" + newFile + ") is longer than the original dylib name (" + file + ").");
372 
373  if (copyfile(dylibPath.c_str(), outputDylibPath.c_str(), NULL, COPYFILE_ALL))
374  throw VuoException("The composition couldn't start because a copy of the dylib couldn't be made.");
375 
376  FILE *fp = fopen(outputDylibPath.c_str(), "r+b");
377  if (!fp)
378  throw VuoException("The composition couldn't start because the dylib's header couldn't be opened.");
379  VuoDefer(^{ fclose(fp); });
380 
381  struct mach_header_64 header;
382  if (fread(&header, sizeof(header), 1, fp) != 1)
383  throw VuoException("The composition couldn't start because the dylib's header couldn't be read.");
384 
385  if (header.magic != MH_MAGIC_64
386  || header.cputype != CPU_TYPE_X86_64)
387  throw VuoException("The composition couldn't start because the dylib isn't an x86_64-only (non-fat) Mach-O binary.");
388 
389  for (int i = 0; i < header.ncmds; ++i)
390  {
391  struct load_command lc;
392  if (fread(&lc, sizeof(lc), 1, fp) != 1)
393  throw VuoException("The composition couldn't start because the dylib's command couldn't be read.");
394 
395  // VLog("cmd[%d]: %x (size %d)",i,lc.cmd,lc.cmdsize);
396  if (lc.cmd == LC_ID_DYLIB)
397  {
398  fseek(fp, sizeof(struct dylib), SEEK_CUR);
399 
400  size_t nameLength = lc.cmdsize - sizeof(struct dylib_command);
401  char *name = (char *)calloc(nameLength + 1, 1);
402  if (fread(name, nameLength, 1, fp) != 1)
403  throw VuoException("The composition couldn't start because the dylib's ID command couldn't be read.");
404 
405 // VLog("Changing name \"%s\" to \"%s\"…", name, outputDylibPath.c_str());
406  fseek(fp, -nameLength, SEEK_CUR);
407  bzero(name, nameLength);
408  memcpy(name, outputDylibPath.c_str(), min(nameLength, outputDylibPath.length()));
409  fwrite(name, nameLength, 1, fp);
410  return;
411  }
412  else
413  fseek(fp, lc.cmdsize-sizeof(lc), SEEK_CUR);
414  }
415 
416  throw VuoException("The composition couldn't start because the dylib's LC_ID_DYLIB command couldn't be found.");
417 }
418 
428 void VuoRunner::startInternal(void)
429 {
430  stopped = false;
431  dispatch_semaphore_wait(stoppedSemaphore, DISPATCH_TIME_FOREVER);
432 
433  ZMQContext = zmq_init(1);
434 
435  if (isInCurrentProcess())
436  {
437  // Start the composition in the current process.
438 
439  bool alreadyLoaded = dlopen(dylibPath.c_str(), RTLD_NOLOAD);
440  if (alreadyLoaded)
441  {
442  // Each composition instance needs its own global variables.
443  // Change the dylib's internal name, to convince dlopen() to load another instance of it.
444 
445  string uniquedDylibPath;
446  copyDylibAndChangeId(dylibPath, uniquedDylibPath);
447  VDebugLog("\"%s\" is already loaded, so I duplicated it and changed its LC_ID_DYLIB to \"%s\".", dylibPath.c_str(), uniquedDylibPath.c_str());
448 
449  if (shouldDeleteBinariesWhenFinished)
450  remove(dylibPath.c_str());
451 
452  dylibPath = uniquedDylibPath;
453  shouldDeleteBinariesWhenFinished = true;
454  }
455 
456  dylibHandle = dlopen(dylibPath.c_str(), RTLD_NOW);
457  if (!dylibHandle)
458  throw VuoException("The composition couldn't start because the library '" + dylibPath + "' couldn't be loaded : " + dlerror());
459 
460  try
461  {
463  if (! vuoInitInProcess)
464  throw VuoException("The composition couldn't start because vuoInitInProcess() couldn't be found in '" + dylibPath + "' : " + dlerror());
465 
466  ZMQControlURL = "inproc://" + VuoFileUtilities::makeTmpFile("vuo-control", "");
467  ZMQTelemetryURL = "inproc://" + VuoFileUtilities::makeTmpFile("vuo-telemetry", "");
468 
469  vuoInitInProcess(ZMQContext, ZMQControlURL.c_str(), ZMQTelemetryURL.c_str(), true, getpid(), -1, false,
470  sourceDir.c_str(), dylibHandle, NULL, false);
471  }
472  catch (VuoException &e)
473  {
474  VUserLog("error: %s", e.what());
475  dlclose(dylibHandle);
476  dylibHandle = NULL;
477  throw;
478  }
479  }
480  else
481  {
482  // Start the composition or composition loader in a new process.
483 
484  vector<string> args;
485 
486  string executableName;
487  if (isUsingCompositionLoader())
488  {
489  // If we're using the loader, set the executable's display name to the dylib,
490  // so that composition's name shows up in the process list.
491  string dir, file, ext;
492  VuoFileUtilities::splitPath(dylibPath, dir, file, ext);
493  executableName = file;
494  }
495  else
496  {
497  string dir, file, ext;
498  VuoFileUtilities::splitPath(executablePath, dir, file, ext);
499  string executableName = file;
500  if (! ext.empty())
501  executableName += "." + ext;
502  }
503  args.push_back(executableName);
504 
505  // https://b33p.net/kosada/node/16374
506  // The socket's full pathname (`sockaddr_un::sun_path`) must be 104 characters or less
507  // (https://opensource.apple.com/source/xnu/xnu-2782.1.97/bsd/sys/un.h.auto.html).
508  // "/Users/me/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/vuo-telemetry-rr8Br3"
509  // is 101 characters, which limits the username to 5 characters.
510  // "/Users/me/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/v-rr8Br3"
511  // is 89 characters, which limits the username to 17 characters
512  // (still not a lot, but more likely to work with typical macOS usernames).
513  ZMQControlURL = "ipc://" + VuoFileUtilities::makeTmpFile("v", "");
514  ZMQTelemetryURL = "ipc://" + VuoFileUtilities::makeTmpFile("v", "");
515  args.push_back("--vuo-control=" + ZMQControlURL);
516  args.push_back("--vuo-telemetry=" + ZMQTelemetryURL);
517 
518  {
519  ostringstream oss;
520  oss << getpid();
521  args.push_back("--vuo-runner-pid=" + oss.str());
522  }
523 
524  {
525  ostringstream oss;
527  args.push_back("--vuo-runner-pipe=" + oss.str());
528  }
529 
530  if (shouldContinueIfRunnerDies)
531  args.push_back("--vuo-continue-if-runner-dies");
532 
533  if (isUsingCompositionLoader())
534  {
535  ZMQLoaderControlURL = "ipc://" + VuoFileUtilities::makeTmpFile("v", "");
536  args.push_back("--vuo-loader=" + ZMQLoaderControlURL);
537  }
538  else
539  args.push_back("--vuo-pause");
540 
541  int fd[2];
542  int ret = pipe(fd);
543  if (ret)
544  throw VuoException("The composition couldn't start because a pipe couldn't be opened : " + string(strerror(errno)));
545 
546  char * argv[7];
547  int argSize = args.size();
548  for (size_t i = 0; i < argSize; ++i)
549  {
550  size_t mallocSize = args[i].length() + 1;
551  argv[i] = (char *)malloc(mallocSize);
552  strlcpy(argv[i], args[i].c_str(), mallocSize);
553  }
554  argv[argSize] = NULL;
555 
556  string errorWorkingDirectory = "The composition couldn't start because the working directory couldn't be changed to '" + sourceDir + "' : ";
557  string errorExecutable = "The composition couldn't start because the file '" + executablePath + "' couldn't be executed : ";
558  string errorFork = "The composition couldn't start because the composition process couldn't be forked : ";
559  const size_t ERROR_BUFFER_LEN = 256;
560  char errorBuffer[ERROR_BUFFER_LEN];
561 
562  pipe(runnerReadCompositionWritePipe);
563 
564  pid_t childPid = fork();
565  if (childPid == 0)
566  {
567  // There are only a limited set of functions you're allowed to call in the child process
568  // after fork() and before exec(). Functions such as VUserLog() and exit() aren't allowed,
569  // so instead we're calling alternatives such as write() and _exit().
570 
571  close(runnerReadCompositionWritePipe[0]);
572 
573  pid_t grandchildPid = fork();
574  if (grandchildPid == 0)
575  {
576  close(fd[0]);
577  close(fd[1]);
578 
579  // Set the current working directory to that of the source .vuo composition so that
580  // relative URL paths are resolved correctly.
581  if (!sourceDir.empty())
582  {
583  ret = chdir(sourceDir.c_str());
584  if (ret)
585  {
586  strerror_r(errno, errorBuffer, ERROR_BUFFER_LEN);
587  write(STDERR_FILENO, errorWorkingDirectory.c_str(), errorWorkingDirectory.length());
588  write(STDERR_FILENO, errorBuffer, strlen(errorBuffer));
589  write(STDERR_FILENO, "\n", 1);
590  _exit(-1);
591  }
592  }
593 
594  if (isRuntimeCheckingEnabled)
595  setenv("DYLD_INSERT_LIBRARIES", mainThreadChecker, 1);
596 
597  ret = execv(executablePath.c_str(), argv);
598  if (ret)
599  {
600  strerror_r(errno, errorBuffer, ERROR_BUFFER_LEN);
601  write(STDERR_FILENO, errorExecutable.c_str(), errorExecutable.length());
602  write(STDERR_FILENO, errorBuffer, strlen(errorBuffer));
603  write(STDERR_FILENO, "\n", 1);
604  for (size_t i = 0; i < argSize; ++i)
605  free(argv[i]);
606  _exit(-1);
607  }
608  }
609  else if (grandchildPid > 0)
610  {
611  close(fd[0]);
612 
613  write(fd[1], &grandchildPid, sizeof(pid_t));
614  close(fd[1]);
615 
616  _exit(0);
617  }
618  else
619  {
620  close(fd[0]);
621  close(fd[1]);
622 
623  strerror_r(errno, errorBuffer, ERROR_BUFFER_LEN);
624  write(STDERR_FILENO, errorFork.c_str(), errorFork.length());
625  write(STDERR_FILENO, errorBuffer, strlen(errorBuffer));
626  write(STDERR_FILENO, "\n", 1);
627  _exit(-1);
628  }
629  }
630  else if (childPid > 0)
631  {
632  close(fd[1]);
633 
634  // If this process launches compositions in addition to this one,
635  // ensure they don't prop open this pipe,
636  // which would prevent VuoRunner::stop's `read()` from terminating.
637  VuoRunner_closeOnExec(runnerReadCompositionWritePipe[1]);
638 
639  for (size_t i = 0; i < argSize; ++i)
640  free(argv[i]);
641 
642  pid_t grandchildPid;
643  read(fd[0], &grandchildPid, sizeof(pid_t));
644  close(fd[0]);
645 
646  // Reap the child process.
647  int status;
648  int ret;
649  do {
650  ret = waitpid(childPid, &status, 0);
651  } while (ret == -1 && errno == EINTR);
652  if (WIFEXITED(status) && WEXITSTATUS(status))
653  throw VuoException("The composition couldn't start because the parent of the composition process exited with an error.");
654  else if (WIFSIGNALED(status))
655  throw VuoException("The composition couldn't start because the parent of the composition process exited abnormally : " + string(strsignal(WTERMSIG(status))));
656 
657  if (grandchildPid > 0)
658  compositionPid = grandchildPid;
659  else
660  throw VuoException("The composition couldn't start because the composition process id couldn't be obtained");
661  }
662  else
663  {
664  for (size_t i = 0; i < argSize; ++i)
665  free(argv[i]);
666 
667  throw VuoException("The composition couldn't start because the parent of the composition process couldn't be forked : " + string(strerror(errno)));
668  }
669  }
670 
671  // Connect to the composition loader (if any) and composition.
672  if (isUsingCompositionLoader())
673  {
674  ZMQLoaderControl = zmq_socket(ZMQContext,ZMQ_REQ);
676 
677  // Try to connect to the composition loader. If at first we don't succeed, give the composition loader a little more time to set up the socket.
678  int numTries = 0;
679  while (zmq_connect(ZMQLoaderControl,ZMQLoaderControlURL.c_str()))
680  {
681  if (++numTries == 1000)
682  throw VuoException("The composition couldn't start because the runner couldn't establish communication with the composition loader : " + string(strerror(errno)));
683  usleep(USEC_PER_SEC / 1000);
684  }
685 
686  replaceComposition(dylibPath, "");
687  }
688  else
689  {
690  __block string errorMessage;
691  dispatch_sync(controlQueue, ^{
692  try {
693  setUpConnections();
694  } catch (VuoException &e) {
695  errorMessage = e.what();
696  }
697  });
698  if (! errorMessage.empty())
699  throw VuoException(errorMessage);
700  }
701 }
702 
707 void *VuoRunner_listen(void *context)
708 {
709  pthread_detach(pthread_self());
710  VuoRunner *runner = static_cast<VuoRunner *>(context);
711  runner->listen();
712  return NULL;
713 }
714 
720 void VuoRunner::setUpConnections(void)
721 {
722  ZMQControl = zmq_socket(ZMQContext,ZMQ_REQ);
724 
725  // Try to connect to the composition. If at first we don't succeed, give the composition a little more time to set up the socket.
726  int numTries = 0;
727  while (zmq_connect(ZMQControl,ZMQControlURL.c_str()))
728  {
729  if (++numTries == 1000)
730  throw VuoException("The composition couldn't start because the runner couldn't establish communication to control the composition : " + string(strerror(errno)));
731  usleep(USEC_PER_SEC / 1000);
732  }
733 
734  // Cache published ports so they're available whenever a caller starts listening for published port value changes.
735  arePublishedInputPortsCached = false;
736  arePublishedOutputPortsCached = false;
737  if (isInCurrentProcess())
738  {
739  dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
740  __block string publishedPortsError;
741  dispatch_async(queue, ^{
742  try {
743  getCachedPublishedPorts(false);
744  getCachedPublishedPorts(true);
745  } catch (VuoException &e) {
746  publishedPortsError = e.what();
747  }
748  });
749  while (! (arePublishedInputPortsCached && arePublishedOutputPortsCached) )
750  {
752  usleep(USEC_PER_SEC / 1000);
753 
754  if (! publishedPortsError.empty())
755  throw VuoException(publishedPortsError);
756  }
757  }
758  else
759  {
760  getCachedPublishedPorts(false);
761  getCachedPublishedPorts(true);
762  }
763 
764  listenCanceled = false;
765  dispatch_semaphore_wait(endedListeningSemaphore, DISPATCH_TIME_FOREVER);
766 
767  pthread_t listenThread;
768  int ret = pthread_create(&listenThread, nullptr, &VuoRunner_listen, this);
769  if (ret)
770  throw VuoException(string("The composition couldn't start because the runner couldn't create a thread: ") + strerror(ret));
771 
772  dispatch_semaphore_wait(beganListeningSemaphore, DISPATCH_TIME_FOREVER);
773  if (!listenError.empty())
774  throw VuoException("The composition couldn't start because the runner couldn't establish communication to listen to the composition: " + listenError);
775 
778 }
779 
795 {
796  if (! isInCurrentProcess())
797  throw VuoException("The composition is not running in the current process. Only use this function if the composition was constructed with newCurrentProcessRunnerFromDynamicLibrary().");
798 
799  if (! isMainThread())
800  throw VuoException("This is not the main thread. Only call this function from the main thread.");
801 
802  while (! stopped)
803  VuoEventLoop_processEvent(VuoEventLoop_WaitIndefinitely);
804 }
805 
831 {
832  if (! isInCurrentProcess())
833  throw VuoException("The composition is not running in the current process. Only use this function if the composition was constructed with newCurrentProcessRunnerFromDynamicLibrary().");
834 
835  if (! isMainThread())
836  throw VuoException("This is not the main thread. Only call this function from the main thread.");
837 
838  VuoEventLoop_processEvent(VuoEventLoop_RunOnce);
839 }
840 
849 {
850  dispatch_sync(controlQueue, ^{
851  if (stopped || lostContact) {
852  return;
853  }
854 
856 
857  try
858  {
861  }
862  catch (VuoException &e)
863  {
864  stopBecauseLostContact(e.what());
865  }
866 
867  paused = true;
868  });
869 }
870 
877 {
878  dispatch_sync(controlQueue, ^{
879  if (stopped || lostContact) {
880  return;
881  }
882 
884 
885  try
886  {
889  }
890  catch (VuoException &e)
891  {
892  stopBecauseLostContact(e.what());
893  }
894 
895  paused = false;
896  });
897 }
898 
918 void VuoRunner::replaceComposition(string compositionDylibPath, string compositionDiff)
919 {
920  if (! isUsingCompositionLoader())
921  throw VuoException("The runner is not using a composition loader. Only use this function if the composition was constructed with newSeparateProcessRunnerFromDynamicLibrary().");
922 
923  dispatch_sync(controlQueue, ^{
924  if (stopped || lostContact) {
925  return;
926  }
927 
928  VDebugLog("Loading composition…");
929 
930  if (dylibPath != compositionDylibPath)
931  {
932  if (shouldDeleteBinariesWhenFinished)
933  {
934  remove(dylibPath.c_str());
935  }
936 
937  dylibPath = compositionDylibPath;
938  }
939 
941 
942  try
943  {
944  if (! paused)
945  {
946  VDebugLog(" Pausing…");
949  }
950 
951  cleanUpConnections();
952 
953  vector<string> dependencyDylibPathsRemoved = dependencyLibraries->dequeueLibrariesToUnload();
954  vector<string> dependencyDylibPathsAdded = dependencyLibraries->dequeueLibrariesToLoad();
955 
956  unsigned int messageCount = 4 + dependencyDylibPathsAdded.size() + dependencyDylibPathsRemoved.size();
957  zmq_msg_t *messages = (zmq_msg_t *)malloc(messageCount * sizeof(zmq_msg_t));
958  int index = 0;
959 
960  vuoInitMessageWithString(&messages[index++], dylibPath.c_str());
961 
962  vuoInitMessageWithInt(&messages[index++], dependencyDylibPathsAdded.size());
963  for (vector<string>::iterator i = dependencyDylibPathsAdded.begin(); i != dependencyDylibPathsAdded.end(); ++i) {
964  vuoInitMessageWithString(&messages[index++], (*i).c_str());
965  }
966 
967  vuoInitMessageWithInt(&messages[index++], dependencyDylibPathsRemoved.size());
968  for (vector<string>::iterator i = dependencyDylibPathsRemoved.begin(); i != dependencyDylibPathsRemoved.end(); ++i) {
969  vuoInitMessageWithString(&messages[index++], (*i).c_str());
970  }
971 
972  vuoInitMessageWithString(&messages[index], compositionDiff.c_str());
973 
974  if (! paused)
975  VDebugLog(" Replacing composition…");
976 
977  vuoLoaderControlRequestSend(VuoLoaderControlRequestCompositionReplace,messages,messageCount);
978  vuoLoaderControlReplyReceive(VuoLoaderControlReplyCompositionReplaced);
979 
980  setUpConnections();
981 
982  if (! paused)
983  {
984  VDebugLog(" Unpausing…");
987  }
988 
989  VDebugLog(" Done.");
990  }
991  catch (VuoException &e)
992  {
993  stopBecauseLostContact(e.what());
994  }
995  });
996 }
997 
1012 {
1013  dispatch_sync(controlQueue, ^{
1014  if (stopped) {
1015  return;
1016  }
1017 
1018  vuoMemoryBarrier();
1019 
1020  // Only tell the composition to stop if it hasn't already ended on its own.
1021  if (! lostContact)
1022  {
1023  try
1024  {
1025  int timeoutInSeconds = (isInCurrentProcess() ? -1 : 5);
1026  zmq_msg_t messages[3];
1027  vuoInitMessageWithInt(&messages[0], timeoutInSeconds);
1028  vuoInitMessageWithBool(&messages[1], false); // isBeingReplaced
1029  vuoInitMessageWithBool(&messages[2], !isInCurrentProcess()); // isLastEverInProcess
1031 
1032  if (isInCurrentProcess() && isMainThread())
1033  {
1034  // If VuoRunner::stop() is blocking the main thread, wait for the composition's reply on another thread, and drain the main queue, in case the composition needs to shut down stuff that requires the main queue.
1035  __block bool replyReceived = false;
1036  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
1037  vuoMemoryBarrier();
1038  try
1039  {
1041  }
1042  catch (...)
1043  {
1044  // do nothing; doesn't matter if connection timed out
1045  }
1046  replyReceived = true;
1047  });
1048  while (!replyReceived)
1049  {
1051  usleep(USEC_PER_SEC / 1000);
1052  }
1053  vuoMemoryBarrier();
1054  }
1055  else
1057  }
1058  catch (...)
1059  {
1060  // do nothing; doesn't matter if connection timed out
1061  }
1062  }
1063 
1064  cleanUpConnections();
1065 
1066  if (isUsingCompositionLoader() && ZMQLoaderControl)
1067  {
1068  zmq_close(ZMQLoaderControl);
1069  ZMQLoaderControl = NULL;
1070  }
1071 
1072  if (isInCurrentProcess() && dylibHandle)
1073  {
1074  VuoInitInProcessType *vuoInitInProcess = (VuoInitInProcessType *)dlsym(dylibHandle, "vuoInitInProcess");
1075  if (vuoInitInProcess) // Avoid double jeopardy if startInternal() already failed for missing vuoInitInProcess.
1076  {
1077  VuoFiniType *vuoFini = (VuoFiniType *)dlsym(dylibHandle, "vuoFini");
1078  if (! vuoFini)
1079  {
1080  VUserLog("The composition couldn't stop because vuoFini() couldn't be found in '%s' : %s", dylibPath.c_str(), dlerror());
1081  return;
1082  }
1083  void *runtimeState = vuoFini();
1084 
1086  if (! vuoFiniRuntimeState)
1087  {
1088  VUserLog("The composition couldn't stop because vuoFiniRuntimeState() couldn't be found in '%s' : %s", dylibPath.c_str(), dlerror());
1089  return;
1090  }
1092  }
1093 
1094  dlclose(dylibHandle);
1095  dylibHandle = NULL;
1096  }
1097  else if (isInCurrentProcess() && !dylibHandle)
1098  {
1099  // If the dylib isn't open, the composition isn't running, so there's nothing to clean up.
1100  }
1101  else
1102  {
1103  char buf[1];
1104  close(runnerReadCompositionWritePipe[1]);
1105 
1106  if (! lostContact)
1107  {
1108  // Wait for child process to end.
1109  // Can't use waitpid() since it only waits on child processes, yet compositionPid is a grandchild.
1110  // Instead, do a blocking read() — the grandchild never writes anything to the pipe, and when the grandchild exits,
1111  // read() will return EOF (since it was the last process that had it open for writing).
1112  read(runnerReadCompositionWritePipe[0], &buf, 1);
1113  }
1114 
1115  close(runnerReadCompositionWritePipe[0]);
1116 
1117  if (! lostContact)
1118  {
1119  zmq_term(ZMQContext);
1120  ZMQContext = NULL;
1121  }
1122  else
1123  {
1124  dispatch_semaphore_wait(terminatedZMQContextSemaphore, DISPATCH_TIME_FOREVER);
1125  }
1126  }
1127 
1128  if (shouldDeleteBinariesWhenFinished)
1129  {
1130  if (isUsingCompositionLoader())
1131  {
1132  remove(dylibPath.c_str());
1133  }
1134  else if (isInCurrentProcess())
1135  {
1136  remove(dylibPath.c_str());
1137  }
1138  else
1139  {
1140  remove(executablePath.c_str());
1141  }
1142  }
1143 
1144  dependencyLibraries = nullptr; // release shared_ptr
1145 
1146  stopped = true;
1147  dispatch_semaphore_signal(stoppedSemaphore);
1149  });
1150 }
1151 
1155 void VuoRunner::cleanUpConnections(void)
1156 {
1157  if (! ZMQControl)
1158  return;
1159 
1160  zmq_close(ZMQControl);
1161  ZMQControl = NULL;
1162 
1163  if (ZMQSelfSend)
1164  // Break out of zmq_poll().
1165  vuoSend("VuoRunner::ZMQSelfSend", ZMQSelfSend, 0, nullptr, 0, false, nullptr);
1166 
1167  dispatch_semaphore_wait(endedListeningSemaphore, DISPATCH_TIME_FOREVER);
1168  dispatch_semaphore_signal(endedListeningSemaphore);
1169 }
1170 
1177 {
1178  dispatch_retain(stoppedSemaphore);
1179  dispatch_semaphore_wait(stoppedSemaphore, DISPATCH_TIME_FOREVER);
1180  dispatch_semaphore_signal(stoppedSemaphore);
1181  dispatch_release(stoppedSemaphore);
1182 }
1183 
1199 void VuoRunner::setInputPortValue(string compositionIdentifier, string portIdentifier, json_object *value)
1200 {
1201  const char *valueAsString = json_object_to_json_string_ext(value, JSON_C_TO_STRING_PLAIN);
1202 
1203  dispatch_sync(controlQueue, ^{
1204  if (stopped || lostContact) {
1205  return;
1206  }
1207 
1208  vuoMemoryBarrier();
1209 
1210  try
1211  {
1212  zmq_msg_t messages[3];
1213  vuoInitMessageWithString(&messages[0], compositionIdentifier.c_str());
1214  vuoInitMessageWithString(&messages[1], portIdentifier.c_str());
1215  vuoInitMessageWithString(&messages[2], valueAsString);
1218  }
1219  catch (VuoException &e)
1220  {
1221  stopBecauseLostContact(e.what());
1222  }
1223  });
1224 }
1225 
1239 void VuoRunner::fireTriggerPortEvent(string compositionIdentifier, string portIdentifier)
1240 {
1241  dispatch_sync(controlQueue, ^{
1242  if (stopped || lostContact) {
1243  return;
1244  }
1245 
1246  vuoMemoryBarrier();
1247 
1248  try
1249  {
1250  zmq_msg_t messages[2];
1251  vuoInitMessageWithString(&messages[0], compositionIdentifier.c_str());
1252  vuoInitMessageWithString(&messages[1], portIdentifier.c_str());
1255  }
1256  catch (VuoException &e)
1257  {
1258  stopBecauseLostContact(e.what());
1259  }
1260  });
1261 }
1262 
1276 json_object * VuoRunner::getInputPortValue(string compositionIdentifier, string portIdentifier)
1277 {
1278  __block string valueAsString;
1279  dispatch_sync(controlQueue, ^{
1280  if (stopped || lostContact) {
1281  return;
1282  }
1283 
1284  vuoMemoryBarrier();
1285 
1286  try
1287  {
1288  zmq_msg_t messages[3];
1289  vuoInitMessageWithBool(&messages[0], !isInCurrentProcess());
1290  vuoInitMessageWithString(&messages[1], compositionIdentifier.c_str());
1291  vuoInitMessageWithString(&messages[2], portIdentifier.c_str());
1294  valueAsString = receiveString("null");
1295  }
1296  catch (VuoException &e)
1297  {
1298  stopBecauseLostContact(e.what());
1299  }
1300  });
1301  return json_tokener_parse(valueAsString.c_str());
1302 }
1303 
1317 json_object * VuoRunner::getOutputPortValue(string compositionIdentifier, string portIdentifier)
1318 {
1319  __block string valueAsString;
1320  dispatch_sync(controlQueue, ^{
1321  if (stopped || lostContact) {
1322  return;
1323  }
1324 
1325  vuoMemoryBarrier();
1326 
1327  try
1328  {
1329  zmq_msg_t messages[3];
1330  vuoInitMessageWithBool(&messages[0], !isInCurrentProcess());
1331  vuoInitMessageWithString(&messages[1], compositionIdentifier.c_str());
1332  vuoInitMessageWithString(&messages[2], portIdentifier.c_str());
1335  valueAsString = receiveString("null");
1336  }
1337  catch (VuoException &e)
1338  {
1339  stopBecauseLostContact(e.what());
1340  }
1341  });
1342  return json_tokener_parse(valueAsString.c_str());
1343 }
1344 
1358 string VuoRunner::getInputPortSummary(string compositionIdentifier, string portIdentifier)
1359 {
1360  __block string summary;
1361  dispatch_sync(controlQueue, ^{
1362  if (stopped || lostContact) {
1363  return;
1364  }
1365 
1366  vuoMemoryBarrier();
1367 
1368  try
1369  {
1370  zmq_msg_t messages[2];
1371  vuoInitMessageWithString(&messages[0], compositionIdentifier.c_str());
1372  vuoInitMessageWithString(&messages[1], portIdentifier.c_str());
1375  summary = receiveString("");
1376  }
1377  catch (VuoException &e)
1378  {
1379  stopBecauseLostContact(e.what());
1380  }
1381  });
1382  return summary;
1383 }
1384 
1398 string VuoRunner::getOutputPortSummary(string compositionIdentifier, string portIdentifier)
1399 {
1400  __block string summary;
1401  dispatch_sync(controlQueue, ^{
1402  if (stopped || lostContact) {
1403  return;
1404  }
1405 
1406  vuoMemoryBarrier();
1407 
1408  try
1409  {
1410  zmq_msg_t messages[2];
1411  vuoInitMessageWithString(&messages[0], compositionIdentifier.c_str());
1412  vuoInitMessageWithString(&messages[1], portIdentifier.c_str());
1415  summary = receiveString("");
1416  }
1417  catch (VuoException &e)
1418  {
1419  stopBecauseLostContact(e.what());
1420  }
1421  });
1422  return summary;
1423 }
1424 
1438 string VuoRunner::subscribeToInputPortTelemetry(string compositionIdentifier, string portIdentifier)
1439 {
1440  __block string summary;
1441  dispatch_sync(controlQueue, ^{
1442  if (stopped || lostContact) {
1443  return;
1444  }
1445 
1446  vuoMemoryBarrier();
1447 
1448  try
1449  {
1450  zmq_msg_t messages[2];
1451  vuoInitMessageWithString(&messages[0], compositionIdentifier.c_str());
1452  vuoInitMessageWithString(&messages[1], portIdentifier.c_str());
1455  summary = receiveString("");
1456  }
1457  catch (VuoException &e)
1458  {
1459  stopBecauseLostContact(e.what());
1460  }
1461  });
1462  return summary;
1463 }
1464 
1478 string VuoRunner::subscribeToOutputPortTelemetry(string compositionIdentifier, string portIdentifier)
1479 {
1480  __block string summary;
1481  dispatch_sync(controlQueue, ^{
1482  if (stopped || lostContact) {
1483  return;
1484  }
1485 
1486  vuoMemoryBarrier();
1487 
1488  try
1489  {
1490  zmq_msg_t messages[2];
1491  vuoInitMessageWithString(&messages[0], compositionIdentifier.c_str());
1492  vuoInitMessageWithString(&messages[1], portIdentifier.c_str());
1495  summary = receiveString("");
1496  }
1497  catch (VuoException &e)
1498  {
1499  stopBecauseLostContact(e.what());
1500  }
1501  });
1502  return summary;
1503 }
1504 
1515 void VuoRunner::unsubscribeFromInputPortTelemetry(string compositionIdentifier, string portIdentifier)
1516 {
1517  dispatch_sync(controlQueue, ^{
1518  if (stopped || lostContact) {
1519  return;
1520  }
1521 
1522  vuoMemoryBarrier();
1523 
1524  try
1525  {
1526  zmq_msg_t messages[2];
1527  vuoInitMessageWithString(&messages[0], compositionIdentifier.c_str());
1528  vuoInitMessageWithString(&messages[1], portIdentifier.c_str());
1531  }
1532  catch (VuoException &e)
1533  {
1534  stopBecauseLostContact(e.what());
1535  }
1536  });
1537 }
1538 
1549 void VuoRunner::unsubscribeFromOutputPortTelemetry(string compositionIdentifier, string portIdentifier)
1550 {
1551  dispatch_sync(controlQueue, ^{
1552  if (stopped || lostContact) {
1553  return;
1554  }
1555 
1556  vuoMemoryBarrier();
1557 
1558  try
1559  {
1560  zmq_msg_t messages[2];
1561  vuoInitMessageWithString(&messages[0], compositionIdentifier.c_str());
1562  vuoInitMessageWithString(&messages[1], portIdentifier.c_str());
1565  }
1566  catch (VuoException &e)
1567  {
1568  stopBecauseLostContact(e.what());
1569  }
1570  });
1571 }
1572 
1583 void VuoRunner::subscribeToEventTelemetry(string compositionIdentifier)
1584 {
1585  dispatch_sync(controlQueue, ^{
1586  if (stopped || lostContact) {
1587  return;
1588  }
1589 
1590  vuoMemoryBarrier();
1591 
1592  try
1593  {
1594  zmq_msg_t messages[1];
1595  vuoInitMessageWithString(&messages[0], compositionIdentifier.c_str());
1598  }
1599  catch (VuoException &e)
1600  {
1601  stopBecauseLostContact(e.what());
1602  }
1603  });
1604 }
1605 
1618 void VuoRunner::unsubscribeFromEventTelemetry(string compositionIdentifier)
1619 {
1620  dispatch_sync(controlQueue, ^{
1621  if (stopped || lostContact) {
1622  return;
1623  }
1624 
1625  vuoMemoryBarrier();
1626 
1627  try
1628  {
1629  zmq_msg_t messages[1];
1630  vuoInitMessageWithString(&messages[0], compositionIdentifier.c_str());
1633  }
1634  catch (VuoException &e)
1635  {
1636  stopBecauseLostContact(e.what());
1637  }
1638  });
1639 }
1640 
1651 void VuoRunner::subscribeToAllTelemetry(string compositionIdentifier)
1652 {
1653  dispatch_sync(controlQueue, ^{
1654  if (stopped || lostContact) {
1655  return;
1656  }
1657 
1658  vuoMemoryBarrier();
1659 
1660  try
1661  {
1662  zmq_msg_t messages[1];
1663  vuoInitMessageWithString(&messages[0], compositionIdentifier.c_str());
1666  }
1667  catch (VuoException &e)
1668  {
1669  stopBecauseLostContact(e.what());
1670  }
1671  });
1672 }
1673 
1686 void VuoRunner::unsubscribeFromAllTelemetry(string compositionIdentifier)
1687 {
1688  dispatch_sync(controlQueue, ^{
1689  if (stopped || lostContact) {
1690  return;
1691  }
1692 
1693  vuoMemoryBarrier();
1694 
1695  try
1696  {
1697  zmq_msg_t messages[1];
1698  vuoInitMessageWithString(&messages[0], compositionIdentifier.c_str());
1701  }
1702  catch (VuoException &e)
1703  {
1704  stopBecauseLostContact(e.what());
1705  }
1706  });
1707 }
1708 
1722 void VuoRunner::setPublishedInputPortValues(map<Port *, json_object *> portsAndValuesToSet)
1723 {
1725  for (auto i : portsAndValuesToSet)
1726  {
1727  string portName = i.first->getName();
1728  if (portName == "width")
1729  p->lastWidth = json_object_get_int64(i.second);
1730  else if (portName == "height")
1731  p->lastHeight = json_object_get_int64(i.second);
1732  else if (portName == "image" || portName == "startImage")
1733  {
1734  json_object *o;
1735  if (json_object_object_get_ex(i.second, "pixelsWide", &o))
1736  p->lastWidth = json_object_get_int64(o);
1737  if (json_object_object_get_ex(i.second, "pixelsHigh", &o))
1738  p->lastHeight = json_object_get_int64(o);
1739  }
1740  }
1741 
1742  dispatch_sync(controlQueue, ^{
1743  if (stopped || lostContact) {
1744  return;
1745  }
1746 
1747  vuoMemoryBarrier();
1748 
1749  try
1750  {
1751  int messageCount = portsAndValuesToSet.size() * 2;
1752  zmq_msg_t messages[messageCount];
1753 
1754  int i = 0;
1755  for (auto &kv : portsAndValuesToSet)
1756  {
1757  vuoInitMessageWithString(&messages[i++], kv.first->getName().c_str());
1758  vuoInitMessageWithString(&messages[i++], json_object_to_json_string_ext(kv.second, JSON_C_TO_STRING_PLAIN));
1759  }
1760 
1763  }
1764  catch (VuoException &e)
1765  {
1766  stopBecauseLostContact(e.what());
1767  }
1768  });
1769 }
1770 
1779 {
1780  set<VuoRunner::Port *> portAsSet;
1781  portAsSet.insert(port);
1782  firePublishedInputPortEvent(portAsSet);
1783 }
1784 
1795 void VuoRunner::firePublishedInputPortEvent(const set<Port *> &ports)
1796 {
1797  dispatch_sync(controlQueue, ^{
1798  if (stopped || lostContact) {
1799  return;
1800  }
1801 
1802  vuoMemoryBarrier();
1803 
1804  lastFiredEventSignaled = false;
1805 
1806  try
1807  {
1808  size_t messageCount = ports.size() + 1;
1809  zmq_msg_t messages[messageCount];
1810 
1811  vuoInitMessageWithInt(&messages[0], ports.size());
1812  int i = 1;
1813  for (VuoRunner::Port *port : ports) {
1814  vuoInitMessageWithString(&messages[i++], port->getName().c_str());
1815  }
1816 
1819  }
1820  catch (VuoException &e)
1821  {
1822  stopBecauseLostContact(e.what());
1823  }
1824  });
1825 }
1826 
1852 {
1853  saturating_semaphore_wait(lastFiredEventSemaphore, &lastFiredEventSignaled);
1854 }
1855 
1867 {
1868  __block string valueAsString;
1869  dispatch_sync(controlQueue, ^{
1870  if (stopped || lostContact) {
1871  return;
1872  }
1873 
1874  vuoMemoryBarrier();
1875 
1876  try
1877  {
1878  zmq_msg_t messages[2];
1879  vuoInitMessageWithBool(&messages[0], !isInCurrentProcess());
1880  vuoInitMessageWithString(&messages[1], port->getName().c_str());
1883  valueAsString = receiveString("null");
1884  }
1885  catch (VuoException &e)
1886  {
1887  stopBecauseLostContact(e.what());
1888  }
1889  });
1890  return json_tokener_parse(valueAsString.c_str());
1891 }
1892 
1904 {
1905  __block string valueAsString;
1906  dispatch_sync(controlQueue, ^{
1907  if (stopped || lostContact) {
1908  return;
1909  }
1910 
1911  vuoMemoryBarrier();
1912 
1913  try
1914  {
1915  zmq_msg_t messages[2];
1916  vuoInitMessageWithBool(&messages[0], !isInCurrentProcess());
1917  vuoInitMessageWithString(&messages[1], port->getName().c_str());
1920  valueAsString = receiveString("null");
1921  }
1922  catch (VuoException &e)
1923  {
1924  stopBecauseLostContact(e.what());
1925  }
1926  });
1927 
1928  // https://b33p.net/kosada/node/17535
1929  json_object *js = json_tokener_parse(valueAsString.c_str());
1930  if (VuoRunner_isHostVDMX && port->getName() == "outputImage")
1931  {
1932  json_object *o;
1933  uint64_t actualWidth = 0;
1934  if (json_object_object_get_ex(js, "pixelsWide", &o))
1935  actualWidth = json_object_get_int64(o);
1936  uint64_t actualHeight = 0;
1937  if (json_object_object_get_ex(js, "pixelsHigh", &o))
1938  actualHeight = json_object_get_int64(o);
1939 
1940  if (p->lastWidth && p->lastHeight
1941  && (actualWidth != p->lastWidth || actualHeight != p->lastHeight))
1942  {
1943  call_once(p->vuoImageFunctionsInitialized, [=](){
1944  p->vuoImageMakeFromJsonWithDimensions = (Private::vuoImageMakeFromJsonWithDimensionsType)dlsym(RTLD_DEFAULT, "VuoImage_makeFromJsonWithDimensions");
1945  if (!p->vuoImageMakeFromJsonWithDimensions)
1946  {
1947  VUserLog("Error: Couldn't find VuoImage_makeFromJsonWithDimensions.");
1948  return;
1949  }
1950 
1951  p->vuoImageGetJson = (Private::vuoImageGetJsonType)dlsym(RTLD_DEFAULT, "VuoImage_getJson");
1952  if (!p->vuoImageGetJson)
1953  {
1954  VUserLog("Error: Couldn't find VuoImage_getJson.");
1955  return;
1956  }
1957  });
1958 
1960  {
1961  void *vi = p->vuoImageMakeFromJsonWithDimensions(js, p->lastWidth, p->lastHeight);
1962  return p->vuoImageGetJson(vi);
1963  }
1964  }
1965  }
1966 
1967  return js;
1968 }
1969 
1984 vector<VuoRunner::Port *> VuoRunner::getCachedPublishedPorts(bool input)
1985 {
1986  // Caching not only provides faster access (without zmq messages),
1987  // but also ensures that the VuoRunner::Port pointers passed to
1988  // VuoRunnerDelegate::receivedTelemetryPublishedOutputPortUpdated are consistent.
1989 
1990  if (input)
1991  {
1992  if (! arePublishedInputPortsCached)
1993  {
1994  publishedInputPorts = refreshPublishedPorts(true);
1995  arePublishedInputPortsCached = true;
1996  }
1997  return publishedInputPorts;
1998  }
1999  else
2000  {
2001  if (! arePublishedOutputPortsCached)
2002  {
2003  publishedOutputPorts = refreshPublishedPorts(false);
2004  arePublishedOutputPortsCached = true;
2005  }
2006  return publishedOutputPorts;
2007  }
2008 }
2009 
2021 vector<VuoRunner::Port *> VuoRunner::refreshPublishedPorts(bool input)
2022 {
2023  dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
2024  dispatch_source_t timeout = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, DISPATCH_TIMER_STRICT, queue);
2025  dispatch_source_set_timer(timeout, dispatch_time(DISPATCH_TIME_NOW, 5*NSEC_PER_SEC), NSEC_PER_SEC, NSEC_PER_SEC/10);
2026  dispatch_source_set_event_handler(timeout, ^{
2027  stopBecauseLostContact("The connection between the composition and runner timed out when trying to receive the list of published ports");
2028  dispatch_source_cancel(timeout);
2029  });
2030  dispatch_resume(timeout);
2031 
2032  vector<VuoRunner::Port *> ports;
2033 
2034  try
2035  {
2036  vuoMemoryBarrier();
2037 
2038  enum VuoControlRequest requests[4];
2039  enum VuoControlReply replies[4];
2040  if (input)
2041  {
2048  }
2049  else
2050  {
2057  }
2058 
2059  vector<string> names;
2060  vector<string> types;
2061  vector<string> details;
2062 
2063  for (int i = 0; i < 3; ++i)
2064  {
2065  vuoControlRequestSend(requests[i], NULL, 0);
2066  vuoControlReplyReceive(replies[i]);
2067  vector<string> messageStrings = receiveListOfStrings();
2068  if (i == 0)
2069  names = messageStrings;
2070  else if (i == 1)
2071  types = messageStrings;
2072  else
2073  details = messageStrings;
2074  }
2075 
2076  for (size_t i = 0; i < names.size() && i < types.size() && i < details.size(); ++i)
2077  {
2078  VuoRunner::Port *port = new Port(names[i], types[i], json_tokener_parse(details[i].c_str()));
2079  ports.push_back(port);
2080  }
2081  }
2082  catch (...)
2083  {
2084  dispatch_source_cancel(timeout);
2085  dispatch_release(timeout);
2086  throw;
2087  }
2088 
2089  dispatch_source_cancel(timeout);
2090  dispatch_release(timeout);
2091 
2092  return ports;
2093 }
2094 
2104 vector<VuoRunner::Port *> VuoRunner::getPublishedInputPorts(void)
2105 {
2106  return getCachedPublishedPorts(true);
2107 }
2108 
2118 vector<VuoRunner::Port *> VuoRunner::getPublishedOutputPorts(void)
2119 {
2120  return getCachedPublishedPorts(false);
2121 }
2122 
2133 {
2134  vector<VuoRunner::Port *> inputPorts = getPublishedInputPorts();
2135  for (vector<VuoRunner::Port *>::iterator i = inputPorts.begin(); i != inputPorts.end(); ++i)
2136  if ((*i)->getName() == name)
2137  return *i;
2138 
2139  return NULL;
2140 }
2141 
2152 {
2153  vector<VuoRunner::Port *> outputPorts = getPublishedOutputPorts();
2154  for (vector<VuoRunner::Port *>::iterator i = outputPorts.begin(); i != outputPorts.end(); ++i)
2155  if ((*i)->getName() == name)
2156  return *i;
2157 
2158  return NULL;
2159 }
2160 
2172 void VuoRunner::listen()
2173 {
2174  // Name this thread.
2175  {
2176  const char *compositionName = dylibPath.empty() ? executablePath.c_str() : dylibPath.c_str();
2177 
2178  // Trim the path, if present.
2179  if (const char *lastSlash = strrchr(compositionName, '/'))
2180  compositionName = lastSlash + 1;
2181 
2182  char threadName[MAXTHREADNAMESIZE];
2183  snprintf(threadName, MAXTHREADNAMESIZE, "org.vuo.runner.telemetry: %s", compositionName);
2184  pthread_setname_np(threadName);
2185  }
2186 
2187  ZMQSelfReceive = zmq_socket(ZMQContext, ZMQ_PAIR);
2188  VuoRunner_configureSocket(ZMQSelfReceive, -1);
2189  if (zmq_bind(ZMQSelfReceive, "inproc://vuo-runner-self") != 0)
2190  {
2191  listenError = strerror(errno);
2192  dispatch_semaphore_signal(beganListeningSemaphore);
2193  return;
2194  }
2195 
2196  ZMQSelfSend = zmq_socket(ZMQContext, ZMQ_PAIR);
2197  VuoRunner_configureSocket(ZMQSelfSend, -1);
2198  if (zmq_connect(ZMQSelfSend, "inproc://vuo-runner-self") != 0)
2199  {
2200  listenError = strerror(errno);
2201  dispatch_semaphore_signal(beganListeningSemaphore);
2202  return;
2203  }
2204 
2205  {
2206  ZMQTelemetry = zmq_socket(ZMQContext,ZMQ_SUB);
2207  VuoRunner_configureSocket(ZMQTelemetry, -1);
2208  if(zmq_connect(ZMQTelemetry,ZMQTelemetryURL.c_str()))
2209  {
2210  listenError = strerror(errno);
2211  dispatch_semaphore_signal(beganListeningSemaphore);
2212  return;
2213  }
2214  }
2215 
2216  {
2217  // subscribe to all types of telemetry
2218  char type = VuoTelemetryStats;
2219  zmq_setsockopt(ZMQTelemetry, ZMQ_SUBSCRIBE, &type, sizeof type);
2221  zmq_setsockopt(ZMQTelemetry, ZMQ_SUBSCRIBE, &type, sizeof type);
2223  zmq_setsockopt(ZMQTelemetry, ZMQ_SUBSCRIBE, &type, sizeof type);
2225  zmq_setsockopt(ZMQTelemetry, ZMQ_SUBSCRIBE, &type, sizeof type);
2227  zmq_setsockopt(ZMQTelemetry, ZMQ_SUBSCRIBE, &type, sizeof type);
2229  zmq_setsockopt(ZMQTelemetry, ZMQ_SUBSCRIBE, &type, sizeof type);
2231  zmq_setsockopt(ZMQTelemetry, ZMQ_SUBSCRIBE, &type, sizeof type);
2232  type = VuoTelemetryEventDropped;
2233  zmq_setsockopt(ZMQTelemetry, ZMQ_SUBSCRIBE, &type, sizeof type);
2234  type = VuoTelemetryError;
2235  zmq_setsockopt(ZMQTelemetry, ZMQ_SUBSCRIBE, &type, sizeof type);
2237  zmq_setsockopt(ZMQTelemetry, ZMQ_SUBSCRIBE, &type, sizeof type);
2238  }
2239 
2240  {
2241  // Wait until the connection is established, as evidenced by a heartbeat telemetry message
2242  // being received from the composition. This is necessary because the ØMQ API doesn't provide
2243  // any way to tell when a SUB socket is ready to receive messages, and if you call zmq_poll()
2244  // on it before it's ready, then it might miss messages that came in while it was still trying
2245  // to get ready. (The zmq_connect() function doesn't make any guarantees about the socket being ready.
2246  // It just starts some setup that may continue asynchronously after zmq_connect() has returned.)
2247  // To avoid missing important telemetry messages from the composition, we make sure that the
2248  // runner doesn't tell the composition to unpause until the runner has verified that it's
2249  // receiving heartbeat telemetry messages. http://zguide.zeromq.org/page:all#Node-Coordination
2250  zmq_pollitem_t items[]=
2251  {
2252  {ZMQTelemetry,0,ZMQ_POLLIN,0},
2253  };
2254  int itemCount = 1;
2255  long timeout = -1;
2256  zmq_poll(items,itemCount,timeout);
2257  }
2258 
2259  dispatch_semaphore_signal(beganListeningSemaphore);
2260 
2261  bool pendingCancel = false;
2262  while(! listenCanceled)
2263  {
2264  zmq_pollitem_t items[]=
2265  {
2266  {ZMQTelemetry,0,ZMQ_POLLIN,0},
2267  {ZMQSelfReceive,0,ZMQ_POLLIN,0},
2268  };
2269  int itemCount = 2;
2270 
2271  // Wait 1 second. If no telemetry was received in that second, we probably lost contact with the composition.
2272  long timeout = pendingCancel ? USEC_PER_SEC / 10 : USEC_PER_SEC;
2273  zmq_poll(items,itemCount,timeout);
2274  if(items[0].revents & ZMQ_POLLIN)
2275  {
2276  // Receive telemetry type.
2277  char type = vuoReceiveInt(ZMQTelemetry, NULL);
2278 
2279  // Receive telemetry arguments and forward to VuoRunnerDelegate.
2280  switch (type)
2281  {
2282  case VuoTelemetryStats:
2283  {
2284  unsigned long utime = vuoReceiveUnsignedInt64(ZMQTelemetry, NULL);
2285  unsigned long stime = vuoReceiveUnsignedInt64(ZMQTelemetry, NULL);
2286  dispatch_sync(delegateQueue, ^{
2287  if (delegate)
2288  delegate->receivedTelemetryStats(utime, stime);
2289  });
2290  break;
2291  }
2293  {
2294  char *compositionIdentifier = vuoReceiveAndCopyString(ZMQTelemetry, NULL);
2295  char *nodeIdentifier = vuoReceiveAndCopyString(ZMQTelemetry, NULL);
2296  dispatch_sync(delegateQueue, ^{
2297  if (delegate)
2298  delegate->receivedTelemetryNodeExecutionStarted(compositionIdentifier, nodeIdentifier);
2299  });
2300  free(compositionIdentifier);
2301  free(nodeIdentifier);
2302  break;
2303  }
2305  {
2306  char *compositionIdentifier = vuoReceiveAndCopyString(ZMQTelemetry, NULL);
2307  char *nodeIdentifier = vuoReceiveAndCopyString(ZMQTelemetry, NULL);
2308  dispatch_sync(delegateQueue, ^{
2309  if (delegate)
2310  delegate->receivedTelemetryNodeExecutionFinished(compositionIdentifier, nodeIdentifier);
2311  });
2312  free(compositionIdentifier);
2313  free(nodeIdentifier);
2314  break;
2315  }
2317  {
2318  while (VuoTelemetry_hasMoreToReceive(ZMQTelemetry))
2319  {
2320  char *compositionIdentifier = vuoReceiveAndCopyString(ZMQTelemetry, NULL);
2321  if (VuoTelemetry_hasMoreToReceive(ZMQTelemetry))
2322  {
2323  char *portIdentifier = vuoReceiveAndCopyString(ZMQTelemetry, NULL);
2324  if (VuoTelemetry_hasMoreToReceive(ZMQTelemetry))
2325  {
2326  bool receivedEvent = vuoReceiveBool(ZMQTelemetry, NULL);
2327  if (VuoTelemetry_hasMoreToReceive(ZMQTelemetry))
2328  {
2329  bool receivedData = vuoReceiveBool(ZMQTelemetry, NULL);
2330  if (VuoTelemetry_hasMoreToReceive(ZMQTelemetry))
2331  {
2332  string portDataSummary;
2333  char *s = vuoReceiveAndCopyString(ZMQTelemetry, NULL);
2334  if (s)
2335  {
2336  portDataSummary = s;
2337  free(s);
2338  }
2339  else
2340  portDataSummary = "";
2341 
2342  dispatch_sync(delegateQueue, ^{
2343  if (delegate)
2344  delegate->receivedTelemetryInputPortUpdated(compositionIdentifier, portIdentifier, receivedEvent, receivedData, portDataSummary);
2345  });
2346  }
2347  }
2348  }
2349  free(portIdentifier);
2350  }
2351  free(compositionIdentifier);
2352  }
2353  break;
2354  }
2356  {
2357  while (VuoTelemetry_hasMoreToReceive(ZMQTelemetry))
2358  {
2359  char *compositionIdentifier = vuoReceiveAndCopyString(ZMQTelemetry, NULL);
2360  if (VuoTelemetry_hasMoreToReceive(ZMQTelemetry))
2361  {
2362  char *portIdentifier = vuoReceiveAndCopyString(ZMQTelemetry, NULL);
2363  if (VuoTelemetry_hasMoreToReceive(ZMQTelemetry))
2364  {
2365  bool sentEvent = vuoReceiveBool(ZMQTelemetry, NULL);
2366  if (VuoTelemetry_hasMoreToReceive(ZMQTelemetry))
2367  {
2368  bool sentData = vuoReceiveBool(ZMQTelemetry, NULL);
2369  if (VuoTelemetry_hasMoreToReceive(ZMQTelemetry))
2370  {
2371  string portDataSummary;
2372  char *s = vuoReceiveAndCopyString(ZMQTelemetry, NULL);
2373  if (s)
2374  {
2375  portDataSummary = s;
2376  free(s);
2377  }
2378  else
2379  portDataSummary = "";
2380 
2381  dispatch_sync(delegateQueue, ^{
2382  if (delegate)
2383  delegate->receivedTelemetryOutputPortUpdated(compositionIdentifier, portIdentifier, sentEvent, sentData, portDataSummary);
2384  });
2385  }
2386  }
2387  }
2388  free(portIdentifier);
2389  }
2390  free(compositionIdentifier);
2391  }
2392  break;
2393  }
2395  {
2396  while (VuoTelemetry_hasMoreToReceive(ZMQTelemetry))
2397  {
2398  char *portIdentifier = vuoReceiveAndCopyString(ZMQTelemetry, NULL);
2399  if (VuoTelemetry_hasMoreToReceive(ZMQTelemetry))
2400  {
2401  bool sentData = vuoReceiveBool(ZMQTelemetry, NULL);
2402  if (VuoTelemetry_hasMoreToReceive(ZMQTelemetry))
2403  {
2404  string portDataSummary;
2405  char *s = vuoReceiveAndCopyString(ZMQTelemetry, NULL);
2406  if (s)
2407  {
2408  portDataSummary = s;
2409  free(s);
2410  }
2411  else
2412  portDataSummary = "";
2413 
2414  Port *port = getPublishedOutputPortWithName(portIdentifier);
2415 
2416  dispatch_sync(delegateQueue, ^{
2417  if (delegate)
2418  delegate->receivedTelemetryPublishedOutputPortUpdated(port, sentData, portDataSummary);
2419  });
2420  }
2421  }
2422  free(portIdentifier);
2423  }
2424  break;
2425  }
2427  {
2428  saturating_semaphore_signal(lastFiredEventSemaphore, &lastFiredEventSignaled);
2429  break;
2430  }
2432  {
2433  char *compositionIdentifier = vuoReceiveAndCopyString(ZMQTelemetry, NULL);
2434  char *portIdentifier = vuoReceiveAndCopyString(ZMQTelemetry, NULL);
2435  dispatch_sync(delegateQueue, ^{
2436  if (delegate)
2437  delegate->receivedTelemetryEventDropped(compositionIdentifier, portIdentifier);
2438  });
2439  free(compositionIdentifier);
2440  free(portIdentifier);
2441  break;
2442  }
2443  case VuoTelemetryError:
2444  {
2445  char *message = vuoReceiveAndCopyString(ZMQTelemetry, NULL);
2446  dispatch_sync(delegateQueue, ^{
2447  if (delegate)
2448  delegate->receivedTelemetryError( string(message) );
2449  });
2450  free(message);
2451  break;
2452  }
2454  {
2455  dispatch_sync(delegateQueue, ^{
2456  if (delegate)
2457  delegate->lostContactWithComposition();
2458  });
2459  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
2460  stop();
2461  });
2462  break;
2463  }
2464  default:
2465  VUserLog("Error: Unknown telemetry message type: %d", type);
2466  break;
2467  }
2468  }
2469  else if (! listenCanceled) // Either the 1-second timeout elapsed, or we got a stop-listening message from ZMQSelfSend
2470  {
2471  if (items[1].revents & ZMQ_POLLIN)
2472  {
2473  // This is a stop-listening message.
2474  vuoReceiveInt(ZMQSelfReceive, NULL);
2475 
2476  // Drain any remaining telemetry messages.
2477  pendingCancel = true;
2478  }
2479 
2480  else if (pendingCancel)
2481  listenCanceled = true;
2482 
2483  else
2484  {
2485  // Timeout.
2486  listenCanceled = true;
2487  string dir, file, ext;
2488  VuoFileUtilities::splitPath(executablePath, dir, file, ext);
2489  stopBecauseLostContact("The connection between the composition ('" + file + "') and runner timed out while listening for telemetry.");
2490  }
2491  }
2492  }
2493 
2494  zmq_close(ZMQTelemetry);
2495  ZMQTelemetry = NULL;
2496 
2497  zmq_close(ZMQSelfSend);
2498  ZMQSelfSend = NULL;
2499  zmq_close(ZMQSelfReceive);
2500  ZMQSelfReceive = NULL;
2501 
2502  dispatch_semaphore_signal(endedListeningSemaphore);
2503  return;
2504 }
2505 
2515 void VuoRunner::vuoControlRequestSend(enum VuoControlRequest request, zmq_msg_t *messages, unsigned int messageCount)
2516 {
2517  char *error = NULL;
2518  vuoSend("runner VuoControl",ZMQControl,request,messages,messageCount,false,&error);
2519 
2520  if (error)
2521  {
2522  string e(error);
2523  free(error);
2524  throw VuoException(e);
2525  }
2526 }
2527 
2537 void VuoRunner::vuoLoaderControlRequestSend(enum VuoLoaderControlRequest request, zmq_msg_t *messages, unsigned int messageCount)
2538 {
2539  char *error = NULL;
2540  vuoSend("runner VuoLoaderControl",ZMQLoaderControl,request,messages,messageCount,false,&error);
2541 
2542  if (error)
2543  {
2544  string e(error);
2545  free(error);
2546  throw VuoException(e);
2547  }
2548 }
2549 
2558 void VuoRunner::vuoControlReplyReceive(enum VuoControlReply expectedReply)
2559 {
2560  char *error = NULL;
2561  int reply = vuoReceiveInt(ZMQControl, &error);
2562 
2563  if (error)
2564  {
2565  string e(error);
2566  free(error);
2567  ostringstream oss;
2568  oss << e << " (expected " << expectedReply << ")";
2569  throw VuoException(oss.str());
2570  }
2571  else if (reply != expectedReply)
2572  {
2573  ostringstream oss;
2574  oss << "The runner received the wrong message from the composition (expected " << expectedReply << ", received " << reply << ")";
2575  throw VuoException(oss.str());
2576  }
2577 }
2578 
2587 void VuoRunner::vuoLoaderControlReplyReceive(enum VuoLoaderControlReply expectedReply)
2588 {
2589  char *error = NULL;
2590  int reply = vuoReceiveInt(ZMQLoaderControl, &error);
2591 
2592  if (error)
2593  {
2594  string e(error);
2595  free(error);
2596  ostringstream oss;
2597  oss << e << " (expected " << expectedReply << ")";
2598  throw VuoException(oss.str());
2599  }
2600  else if (reply != expectedReply)
2601  {
2602  ostringstream oss;
2603  oss << "The runner received the wrong message from the composition loader (expected " << expectedReply << ", received " << reply << ")";
2604  throw VuoException(oss.str());
2605  }
2606 }
2607 
2613 string VuoRunner::receiveString(string fallbackIfNull)
2614 {
2615  char *error = NULL;
2616  char *s = vuoReceiveAndCopyString(ZMQControl, &error);
2617 
2618  if (error)
2619  {
2620  string e(error);
2621  free(error);
2622  throw VuoException(e);
2623  }
2624 
2625  string ret;
2626  if (s)
2627  {
2628  ret = s;
2629  free(s);
2630  }
2631  else
2632  ret = fallbackIfNull;
2633 
2634  return ret;
2635 }
2636 
2640 vector<string> VuoRunner::receiveListOfStrings(void)
2641 {
2642  vector<string> messageStrings;
2644  {
2645  string s = receiveString("");
2646  messageStrings.push_back(s);
2647  }
2648  return messageStrings;
2649 }
2650 
2656 void VuoRunner::saturating_semaphore_signal(dispatch_semaphore_t dsema, bool *signaled)
2657 {
2658  if (__sync_bool_compare_and_swap(signaled, false, true))
2659  dispatch_semaphore_signal(dsema);
2660 }
2661 
2667 void VuoRunner::saturating_semaphore_wait(dispatch_semaphore_t dsema, bool *signaled)
2668 {
2669  *signaled = false;
2670  dispatch_semaphore_wait(dsema, DISPATCH_TIME_FOREVER);
2671 }
2672 
2677 {
2678  return stopped;
2679 }
2680 
2684 bool VuoRunner::isInCurrentProcess(void)
2685 {
2686  return executablePath.empty();
2687 }
2688 
2693 bool VuoRunner::isUsingCompositionLoader(void)
2694 {
2695  return ! executablePath.empty() && ! dylibPath.empty();
2696 }
2697 
2702 {
2703  dispatch_sync(delegateQueue, ^{
2704  this->delegate = delegate;
2705  });
2706 }
2707 
2711 void VuoRunner::stopBecauseLostContact(string errorMessage)
2712 {
2713  __block bool alreadyLostContact;
2714  dispatch_sync(delegateQueue, ^{
2715  alreadyLostContact = lostContact;
2716  lostContact = true;
2717  });
2718 
2719  if (alreadyLostContact)
2720  return;
2721 
2722  saturating_semaphore_signal(lastFiredEventSemaphore, &lastFiredEventSignaled);
2723 
2724  dispatch_sync(delegateQueue, ^{
2725  if (delegate)
2726  delegate->lostContactWithComposition();
2727  });
2728 
2729  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
2730  stop();
2731  });
2732 
2733  if (! isInCurrentProcess())
2734  {
2735  // Normally, stop() is responsible for terminating the ZMQ context.
2736  // But, if stopBecauseLostContact() is called, it takes the responsibility away from stop().
2737  // If there's an in-progress zmq_recv() call, stop() will get stuck waiting on controlQueue, so
2738  // the below call to terminate the ZMQ context interrupts zmq_recv() and allows stop() to proceed.
2739  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
2740  vuoMemoryBarrier();
2741 
2742  zmq_term(ZMQContext);
2743  ZMQContext = NULL;
2744  dispatch_semaphore_signal(terminatedZMQContextSemaphore);
2745  });
2746  }
2747 
2748  VUserLog("%s", errorMessage.c_str());
2749 }
2750 
2757 {
2758  return compositionPid;
2759 }
2760 
2768 VuoRunner::Port::Port(string name, string type, json_object *details)
2769 {
2770  this->name = name;
2771  this->type = type;
2772  this->details = details;
2773 }
2774 
2779 {
2780  return name;
2781 }
2782 
2787 {
2788  return type;
2789 }
2790 
2819 {
2820  return details;
2821 }
2822 
2823 VuoRunnerDelegate::~VuoRunnerDelegate() { } // Fixes "undefined symbols" error