Vuo  2.3.2
VuoEventLoop.m
Go to the documentation of this file.
1 
10 #include "VuoEventLoop.h"
11 #include "VuoLog.h"
12 #include "VuoCompositionState.h"
13 
14 #import <AppKit/AppKit.h>
15 #import <IOKit/pwr_mgt/IOPMLib.h>
16 #import <IOKit/pwr_mgt/IOPM.h>
17 
18 #include <objc/objc-runtime.h>
19 
20 #include <dlfcn.h>
21 
23 
27 static bool VuoEventLoop_isMainThread(void)
28 {
29  static void **mainThread;
30  static dispatch_once_t once = 0;
31  dispatch_once(&once, ^{
32  mainThread = (void **)dlsym(RTLD_SELF, "VuoApp_mainThread");
33  if (!mainThread)
34  mainThread = (void **)dlsym(RTLD_DEFAULT, "VuoApp_mainThread");
35 
36  if (!mainThread)
37  {
38  VUserLog("Error: Couldn't find VuoApp_mainThread.");
40  exit(1);
41  }
42 
43  if (!*mainThread)
44  {
45  VUserLog("Error: VuoApp_mainThread isn't set.");
47  exit(1);
48  }
49  });
50 
51  return *mainThread == (void *)pthread_self();
52 }
53 
72 {
74  {
75  VUserLog("Error: VuoEventLoop_processEvent must be called from the main thread.");
77  exit(1);
78  }
79 
80  NSAutoreleasePool *pool = [NSAutoreleasePool new];
81 
82  // Can't just use `NSApp` directly, since the composition might not link to AppKit.
83  id *nsAppGlobal = (id *)dlsym(RTLD_DEFAULT, "NSApp");
84 
85  // Support running either with or without an NSApplication.
86  if (nsAppGlobal && *nsAppGlobal)
87  {
88  // http://www.cocoawithlove.com/2009/01/demystifying-nsapplication-by.html
89  // https://stackoverflow.com/questions/6732400/cocoa-integrate-nsapplication-into-an-existing-c-mainloop
90 
91  // When the composition is ready to stop, it posts a killswitch NSEvent,
92  // to ensure that this function returns immediately.
93  NSEvent *event = [*nsAppGlobal nextEventMatchingMask:NSEventMaskAny
94  untilDate:(mode == VuoEventLoop_WaitIndefinitely ? [NSDate distantFuture] : [NSDate distantPast])
95  inMode:NSDefaultRunLoopMode
96  dequeue:YES];
97  [*nsAppGlobal sendEvent:event];
98  [*nsAppGlobal updateWindows];
99  }
100  else
101  {
102  // This blocks until CFRunLoopStop() is called.
103  // When the composition is ready to stop (or switch into NSApplication mode), it calls CFRunLoopStop().
104  if (mode == VuoEventLoop_WaitIndefinitely)
105  CFRunLoopRun();
106  else
107  CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false);
108  }
109 
110  [pool drain];
111 }
112 
119 {
120  // Can't just use `NSApp` directly, since the composition might not link to AppKit.
121  id *nsAppGlobal = (id *)dlsym(RTLD_DEFAULT, "NSApp");
122 
123  if (nsAppGlobal && *nsAppGlobal)
124  {
125  // Send an event, to ensure VuoEventLoop_processEvent()'s call to `nextEventMatchingMask:…` returns immediately.
126  // -[NSApplication postEvent:atStart:] must be called from the main thread.
127  dispatch_async(dispatch_get_main_queue(), ^{
128  NSEvent *killswitch = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
129  location:NSMakePoint(0,0)
130  modifierFlags:0
131  timestamp:0
132  windowNumber:0
133  context:nil
134  subtype:0
135  data1:0
136  data2:0];
137  [*nsAppGlobal postEvent:killswitch atStart:NO];
138  });
139  }
140  else
141  // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW26
142  // says this can be called from any thread.
143  CFRunLoopStop(CFRunLoopGetMain());
144 }
145 
152 {
153  // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW26
154  // says this can be called from any thread.
155  CFRunLoopStop(CFRunLoopGetMain());
156 }
157 
164 {
166  {
167  VUserLog("Error: VuoEventLoop_mayBeTerminated must be called from the main thread.");
169  exit(1);
170  }
171 
172  // Can't just use `NSApp` directly, since the composition might not link to AppKit.
173  id *nsAppGlobal = (id *)dlsym(RTLD_DEFAULT, "NSApp");
174  if (!nsAppGlobal || !*nsAppGlobal)
175  return true;
176 
177  if ([*nsAppGlobal modalWindow])
178  return false;
179 
180  for (NSWindow *window in [*nsAppGlobal windows])
181  if ([window attachedSheet])
182  return false;
183 
184  return true;
185 }
186 
196 {
197  static dispatch_queue_attr_t attr = 0;
198  static dispatch_once_t once = 0;
199  dispatch_once(&once, ^{
200  attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0);
201  });
202  return attr;
203 }
204 
211 {
212  // Increase the open-files limit (macOS defaults to 256).
213  struct rlimit rl = {OPEN_MAX, OPEN_MAX};
214  getrlimit(RLIMIT_NOFILE, &rl);
215  rl.rlim_cur = MIN(OPEN_MAX, rl.rlim_max);
216  if (setrlimit(RLIMIT_NOFILE, &rl))
217  VUserLog("Warning: Couldn't set open-files limit: %s", strerror(errno));
218 }
219 
226 {
227  typedef void (*vuoStopCompositionType)(struct VuoCompositionState *);
228  vuoStopCompositionType vuoStopComposition = (vuoStopCompositionType)dlsym(RTLD_SELF, "vuoStopComposition");
229  if (!vuoStopComposition)
230  vuoStopComposition = (vuoStopCompositionType)dlsym(RTLD_DEFAULT, "vuoStopComposition");
231  if (!vuoStopComposition)
232  {
233  VUserLog("Warning: Couldn't find vuoStopComposition symbol; not installing clean-shutdown signal handlers.");
234  return;
235  }
236 
237  // Disable default signal handlers so libdispatch can catch them.
238  signal(SIGINT, SIG_IGN);
239  signal(SIGQUIT, SIG_IGN);
240  signal(SIGTERM, SIG_IGN);
241 
242  // Use libdispatch to handle signals instead of `signal`/`sigaction`
243  // since `vuoStopComposition()` uses non-signal-safe functions such as `malloc`.
244  void (^stop)(void) = ^{
245  vuoStopComposition(NULL);
246  };
247  dispatch_source_t sigintSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGINT, 0, dispatch_get_main_queue());
248  dispatch_source_t sigquitSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGQUIT, 0, dispatch_get_main_queue());
249  dispatch_source_t sigtermSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGTERM, 0, dispatch_get_main_queue());
250  dispatch_source_set_event_handler(sigintSource, stop);
251  dispatch_source_set_event_handler(sigquitSource, stop);
252  dispatch_source_set_event_handler(sigtermSource, stop);
253  dispatch_resume(sigintSource);
254  dispatch_resume(sigquitSource);
255  dispatch_resume(sigtermSource);
256 }
257 
261 static void VuoShowSystemPowerEvent(void *refcon, io_service_t root_domain, natural_t messageType, void *messageArgument)
262 {
263  if (messageType != kIOPMMessageSystemPowerEventOccurred)
264  return;
265 
266  CFDictionaryRef d;
267  IOPMCopyCPUPowerStatus(&d);
268  CFNumberRef cpuSpeedLimitCF = CFDictionaryGetValue(d, CFSTR(kIOPMCPUPowerLimitProcessorSpeedKey));
269  int cpuSpeedLimit = -1;
270  CFNumberGetValue(cpuSpeedLimitCF, kCFNumberIntType, &cpuSpeedLimit);
271  CFRelease(d);
272 
273  if (cpuSpeedLimit >= 0)
274  VDebugLog("The system changed the CPU speed limit to %d%%.", cpuSpeedLimit);
275 }
276 
281 static void VuoThermalState(void)
282 {
283  void (^logThermalState)(int thermalState) = ^(int thermalState){
284  if (thermalState == 0)
285  VDebugLog("thermalState = nominal");
286  else if (thermalState == 1)
287  VDebugLog("thermalState = fair (\"fans audible\")");
288  else if (thermalState == 2)
289  VDebugLog("thermalState = serious (\"fans at maximum speed\")");
290  else if (thermalState == 3)
291  VDebugLog("thermalState = critical (\"system needs to cool down\")");
292  };
293  if ([NSProcessInfo.processInfo respondsToSelector:@selector(thermalState)])
294  {
295  logThermalState(((int (*)(id, SEL))objc_msgSend)(NSProcessInfo.processInfo, @selector(thermalState)));
296  [NSNotificationCenter.defaultCenter addObserverForName:@"NSProcessInfoThermalStateDidChangeNotification" object:nil queue:nil usingBlock:^(NSNotification *note){
297  logThermalState(((int (*)(id, SEL))objc_msgSend)(note.object, @selector(thermalState)));
298  }];
299  }
300 
301 
302  // Start listening for system power events.
303  io_service_t rootDomain = IORegistryEntryFromPath(kIOMasterPortDefault, kIOPowerPlane ":/IOPowerConnection/IOPMrootDomain");
304  IONotificationPortRef notePort = IONotificationPortCreate(MACH_PORT_NULL);
305  if (rootDomain && notePort)
306  {
307  io_object_t notification_object = MACH_PORT_NULL;
308  IOReturn ret = IOServiceAddInterestNotification(notePort, rootDomain, kIOGeneralInterest, VuoShowSystemPowerEvent, NULL, &notification_object);
309  if (ret == kIOReturnSuccess)
310  {
311  CFRunLoopSourceRef runLoopSrc = IONotificationPortGetRunLoopSource(notePort);
312  if (runLoopSrc)
313  CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSrc, kCFRunLoopDefaultMode);
314  }
315  }
316 }
317 
322 static void VuoMemoryPressure(void)
323 {
324  dispatch_source_t memoryPressureWatcher = dispatch_source_create(DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, 0, DISPATCH_MEMORYPRESSURE_NORMAL|DISPATCH_MEMORYPRESSURE_WARN|DISPATCH_MEMORYPRESSURE_CRITICAL, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
325  dispatch_source_set_event_handler(memoryPressureWatcher, ^{
326  int pressure = dispatch_source_get_data(memoryPressureWatcher);
327  if (pressure == DISPATCH_MEMORYPRESSURE_NORMAL)
328  VDebugLog("memoryPressure = normal");
329  else if (pressure == DISPATCH_MEMORYPRESSURE_WARN)
330  VDebugLog("memoryPressure = warning");
331  else if (pressure == DISPATCH_MEMORYPRESSURE_CRITICAL)
332  VDebugLog("memoryPressure = critical");
333  });
334  dispatch_resume(memoryPressureWatcher);
335 }
336 
340 static void VuoWorkspaceState(void)
341 {
342  [NSWorkspace.sharedWorkspace.notificationCenter addObserverForName:NSWorkspaceWillSleepNotification object:nil queue:nil usingBlock:^(NSNotification *note){
343  VDebugLog("The system is going to sleep.");
344  VuoEventLoop_systemAsleep = true;
345  }];
346  [NSWorkspace.sharedWorkspace.notificationCenter addObserverForName:NSWorkspaceDidWakeNotification object:nil queue:nil usingBlock:^(NSNotification *note){
347  VDebugLog("The system is waking up.");
348  VuoEventLoop_systemAsleep = false;
349  }];
350  [NSWorkspace.sharedWorkspace.notificationCenter addObserverForName:NSWorkspaceWillPowerOffNotification object:nil queue:nil usingBlock:^(NSNotification *note){
351  VDebugLog("The system is powering off");
352  }];
353  [NSWorkspace.sharedWorkspace.notificationCenter addObserverForName:NSWorkspaceScreensDidSleepNotification object:nil queue:nil usingBlock:^(NSNotification *note){
354  VDebugLog("The screens are going to sleep.");
355  }];
356  [NSWorkspace.sharedWorkspace.notificationCenter addObserverForName:NSWorkspaceScreensDidWakeNotification object:nil queue:nil usingBlock:^(NSNotification *note){
357  VDebugLog("The screens are waking up.");
358  }];
359  [NSWorkspace.sharedWorkspace.notificationCenter addObserverForName:NSWorkspaceSessionDidBecomeActiveNotification object:nil queue:nil usingBlock:^(NSNotification *note){
360  VDebugLog("The system is switching back to this user account.");
361  }];
362  [NSWorkspace.sharedWorkspace.notificationCenter addObserverForName:NSWorkspaceSessionDidResignActiveNotification object:nil queue:nil usingBlock:^(NSNotification *note){
363  VDebugLog("The system is switching to another user account.");
364  }];
365  [NSNotificationCenter.defaultCenter addObserverForName:NSSystemClockDidChangeNotification object:nil queue:nil usingBlock:^(NSNotification *note){
366  VDebugLog("The system's clock changed to %s", NSDate.date.description.UTF8String);
367  }];
368 }
369 
375 {
377 }
378 
388 {
389  VuoThermalState();
392 }
393 
402 {
403  id activityToken = [NSProcessInfo.processInfo beginActivityWithOptions:NSActivityUserInitiated & ~NSActivitySuddenTerminationDisabled
404  reason: @"Many Vuo compositions need to process input and send output even when the app's window is not visible."];
405 
406  [activityToken retain];
407 }