This is TRHCrashTrap.m in view mode; [Download] [Up]
/*+++ * title: TRHCrashTrap.m * abstract: Implementation of of TRHCrashTrap class. * author: various * created: Dec 1998, by Tom Hageman <tom@basil.icce.rug.nl> * modified: Jan 1999 * copyleft: (see below) * description: * * [adapted for use in Mail.app by means of MailCrashTrap plugin] * * XXX TODO: * - make thread-safe; * v add delegate to handle user-interaction; * v gracefully handle nested crash; * v throw & catch dedicated exception instead of abusing abortModal; * - watch out for character buffer overflows; * - port stack backtrace to hppa, sparc; * - autodoc. * * Based on: // // HKCrashTrap version 1.0 // // Based on ObjectError // Original class by: // Bill Bumgarner, Andrew Stone, Mike Morton, and Julie Zelenski // // Modifications by: // Ivo Rothschild (ivo@hasc.ca) // // A class that [poses as object and] does crash-reporting. // *** This version does _not_ pose as Object anymore, it turns out not // *** to be necessary. // Catches terminating signals (ie seg faults, bus errors) // and fatal Objective-C runtime errors and writes a backtrace // out to the console using some shady hacks. This could be modified // to write backtrace to a file, mail message, etc... if desired. // // // You may freely copy, distribute, and reuse the code in this class. // // NO WARRANTY: // ANY IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE // IS HEREBY DISCLAIMED. IN NO EVENT WILL THE AFOREMENTIONED PARTIES BE LIABLE // FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL // DAMAGES ARISING OUT OF THE USE OF OR INABILITY TO USE THIS CODE. // *---*/ static const char * const RCSid = ((void)&RCSid, "@(#)TRHCrashTrap.m,v 1.15 1999/01/10 17:05:20 tom Exp"); #define RCS_TRHCrashTrap_ID #import "TRHCrashTrap.h" #import "objcTypeDecode.h" #import <appkit/appkit.h> #import <string.h> #import <stdio.h> #import <signal.h> #import <ctype.h> #import <objc/objc-runtime.h> #import <defaults/defaults.h> #ifdef POSE_AS_OBJECT /* According to the docs, -[Object error] calls _error, which we already handle so this poser should not be necessary. And indeed the docs are right. */ @interface _TRHCrashTrapObjectPoser : Object + (void)_crashTrapSetup; - error:(const char *)aString, ...; + error:(const char *)aString, ...; @end #endif static const char *signal_descriptions[NSIG]; static BOOL should_handle_signals[NSIG]; static void init_signal_tables(void); /* fills in the tables above. */ static void (*org_signal_handlers[NSIG])(); static BOOL ignoreCrashes; static BOOL continueAfterError; static int selfOffset, _cmdOffset; static Class crashTrapHandlerClass; static id delegate; #define FREED_OBJECT_NAME "<FreedObject>" // Factory default values. #define MAX_FRAMES 50 // Print at most this number of stack frames. #define MAX_FUNCTION_ARGS 4 // Print at most this number of args to a function. #define MAX_ARG_VAL_LENGTH 256 // limit length of printed argument values. // User default names. #define MAX_FRAMES_DEFAULT_NAME "CrashTrap_MaxFrameCount" #define MAX_FUNCTION_ARGS_DEFAULT_NAME "CrashTrap_FunctionArgCount" #define MAX_ARG_VAL_LENGTH_DEFAULT_NAME "CrashTrap_MaxArgDescriptionLength" #define DUMP_BACKTRACE_DEFAULT_NAME "CrashTrap_DumpBacktrace" static unsigned maxFrames = MAX_FRAMES; static unsigned maxFunctionArgs = MAX_FUNCTION_ARGS; static unsigned maxArgValLength = MAX_ARG_VAL_LENGTH; static BOOL dumpBacktrace = YES; // XXX Should define this elsewhere -- indirection to force macro expansion. #define STRINGIZE(X) _STRINGIZE(X) #define _STRINGIZE(X) #X // ASSUMPTION: The layout of a stack frame for a method invocation is: // fp+0 bytes: calling frame // fp+4 bytes: calling pc // fp+8 bytes: self // fp+12 bytes: selector for method invoked // fp+16 bytes: first argument // // ASSUMPTION: The layout of a stack frame for a function invocation is: // fp+0 bytes: calling frame // fp+4 bytes: calling pc // fp+8 bytes: first argument // // Clearly these are shady assumptions, however we're already // in the process of crashing, so what harm can be done? // // XXX These assumptions are only valid for m68k and i386; // XXX hppa, sparc and ppc are _not_ yet supported. #if (__m68k__ || __i386__) # define FRAME_ARG_OFFSET 8 #endif #if 0 // NOTYET #if (__hppa__) # define FRAME_ARG_OFFSET 0 // really? # define INITIAL_FRAME(first_arg_address) __asm__("%%r4") XXX Does not work with 3.3 cc (gcc 2.5.8) # define ARG_DIRECTION (-1) #endif #if (__sparc__) # define FRAME_ARG_OFFSET 8 // really? # define INITIAL_FRAME(first_arg_address) __asm__("%%fp") XXX see above #endif #endif // NOTYET #ifndef FRAME_ARG_OFFSET # warning XXX stack backtrace not yet implemented for this architecture. # define FRAME_ARG_OFFSET 0 # define INITIAL_FRAME(first_arg_addr) = NULL #endif #ifndef FRAME_NEXT_OFFSET # define FRAME_NEXT_OFFSET 0 #endif #ifndef FRAME_RETURN_OFFSET # define FRAME_RETURN_OFFSET 4 #endif #ifndef ARG_DIRECTION # define ARG_DIRECTION (+1) #endif #ifndef INITIAL_FRAME # define INITIAL_FRAME(first_arg_address) \ = ((void*)((char*)(first_arg_address) - FRAME_ARG_OFFSET)) #endif #ifndef NEXT_FRAME # define NEXT_FRAME(fp) \ (*(void**)((char*)(fp) + FRAME_NEXT_OFFSET)) #endif #ifndef FRAME_ARG_START # define FRAME_ARG_START(fp) \ ((void *)((char *)(fp) + FRAME_ARG_OFFSET)) #endif #ifndef FRAME_RETURN_ADDRESS # define FRAME_RETURN_ADDRESS(fp) \ (*(void**)((char*)(fp) + FRAME_RETURN_OFFSET)) #endif // in OPENSTEP 4.1 and later we can use NSFrameAddress() instead. static void *FramePointer(unsigned frame) { register void *fp INITIAL_FRAME(&frame); // Unwind fp the requested number of frames // (+ 1 additional to compensate for self) do { if (fp == NULL) break; fp = NEXT_FRAME(fp); } while (frame-- > 0); return fp; } typedef void (*ObjcErrorHandler)(Object *, const char *, va_list); static ObjcErrorHandler _originalError; static ObjcErrorHandler SetObjcErrorHandler(ObjcErrorHandler newHandler) { ObjcErrorHandler oldHandler = _error; _error = newHandler; return oldHandler; } /* error/signal handler callback functions. */ static void handle_objc_error(Object *anObject, const char *format, va_list ap) { [crashTrapHandlerClass handleObjcError:anObject format:format args:ap]; } static void handle_signal(int signal) { [crashTrapHandlerClass handleSignal:signal]; } static void error_reporter(NXHandler *errorState) { switch (errorState->code) { case CrashTrap_Crash: // Already reported the error, so nothing to do here... break; case CrashTrap_Continue: #if DEBUG NXLogError("CrashTrap: ...continuing."); #endif break; case CrashTrap_RecursiveCrash: NXLogError("CrashTrap: INTERNAL ERROR: uncaught recursive crash."); break; default: NXLogError("CrashTrap: unknown error code %ld\n", errorState->code); } } // User defaults helper functions. static unsigned getUnsignedDefaultValue(const char *owner, const char *name, unsigned defaultValue) { const char *stringValue; unsigned result = defaultValue; NXUpdateDefault(owner, name); // Make sure value is up-to-date. stringValue = NXGetDefaultValue(owner, name); if (stringValue && isdigit(*stringValue)) { sscanf(stringValue, "%u", &result); } return result; } static BOOL getBoolDefaultValue(const char *owner, const char *name, BOOL defaultValue) { const char *stringValue; BOOL result = defaultValue; NXUpdateDefault(owner, name); // Make sure value is up-to-date. stringValue = NXGetDefaultValue(owner, name); if (stringValue) { if (strcasecmp("YES", stringValue) == 0) result = YES; else if (strcasecmp("NO", stringValue) == 0) result = NO; } return result; } static const char *appName() { const char *result = NULL; if (NXApp) result = [NXApp appName]; if (result == NULL) { /* shady hack in case NXApp is not yet initialized. */ result = strrchr(NXArgv[0], '/'); if (result) ++result; } return result; } @implementation TRHCrashTrap + initialize { if (crashTrapHandlerClass == Nil) { // Determine offsets of hidden self, _cmd arguments. Method m = class_getClassMethod(self, _cmd); char *typeDummy; method_getArgumentInfo(m, 0, &typeDummy, &selfOffset); method_getArgumentInfo(m, 1, &typeDummy, &_cmdOffset); init_signal_tables(); NXRegisterErrorReporter(CRASHTRAP_ERROR_BASE, CRASHTRAP_ERROR_BASE + CRASHTRAP_ERROR_RANGE -1, error_reporter); [self setHandlerClass:[TRHCrashTrap class]]; } return self; } + (void)setup { [self setHandlerClass:[self class]]; _originalError = SetObjcErrorHandler(handle_objc_error); [self setContinueAfterError:NO]; [self setDefaultValues]; [self resumeHandlingCrashes]; #ifdef POSE_AS_OBJECT [_TRHCrashTrapObjectPoser _crashTrapSetup]; #endif } + (void)setDefaultValues { static const NXDefaultsVector defaults = { { MAX_FRAMES_DEFAULT_NAME, STRINGIZE(MAX_FRAMES) }, { MAX_FUNCTION_ARGS_DEFAULT_NAME, STRINGIZE(MAX_FUNCTION_ARGS) }, { MAX_ARG_VAL_LENGTH_DEFAULT_NAME, STRINGIZE(MAX_ARG_VAL_LENGTH) }, { DUMP_BACKTRACE_DEFAULT_NAME, "YES" }, { NULL } }; static BOOL initialized; const char *owner = appName(); if (!initialized) { initialized = YES; NXRegisterDefaults(owner, defaults); } // Obtain default values. maxFrames = getUnsignedDefaultValue(owner, MAX_FRAMES_DEFAULT_NAME, MAX_FRAMES); maxFunctionArgs = getUnsignedDefaultValue(owner, MAX_FUNCTION_ARGS_DEFAULT_NAME, MAX_FUNCTION_ARGS); maxArgValLength = getUnsignedDefaultValue(owner, MAX_ARG_VAL_LENGTH_DEFAULT_NAME, MAX_ARG_VAL_LENGTH); dumpBacktrace = getBoolDefaultValue(owner, DUMP_BACKTRACE_DEFAULT_NAME, YES); } + (void)setContinueAfterError:(BOOL)flag; { continueAfterError = flag; } + (BOOL)continueAfterError; { return continueAfterError; } + (void)setDelegate:aDelegate; { delegate = aDelegate; if (delegate && [delegate respondsTo:@selector(crashTrap:shouldContinueAfterError:)]) { [self setContinueAfterError:YES]; } } + delegate { return delegate; } + (Class)handlerClass { return crashTrapHandlerClass; } + (void)setHandlerClass:(Class)aClass { crashTrapHandlerClass = aClass; // XXX should only allow self or subclasses of self? } + (void)_setHandler:(void (*)())aHandler forSignal:(int)aSignal { // XXX probably should use sigvec instead to preserve signal flags as well. void (*org_handler)() = signal(aSignal, ((aHandler == SIG_DFL) ? org_signal_handlers[aSignal] : aHandler)); if (aHandler != SIG_DFL && org_handler != aHandler) { org_signal_handlers[aSignal] = org_handler; } } + (BOOL)isHandlingSignal:(int)aSignal { if (aSignal <= 0 || aSignal >= NSIG) return NO; return should_handle_signals[aSignal]; } + (void)setSignal:(int)aSignal handle:(BOOL)aValue { if (aSignal <= 0 || aSignal >= NSIG) return; if (aValue) aValue = YES; // normalize value. if (should_handle_signals[aSignal] != aValue) { should_handle_signals[aSignal] = aValue; if (!ignoreCrashes) { [self _setHandler:(aValue ? handle_signal : SIG_DFL) forSignal:aSignal]; } } } + (void)setSignalHandler:(void (*)())handler { int sig; /* [TRH] semantics of setSignalHandler:SIG_DFL has changed slightly: now it restores the situation that was in effect just before the CrashTrap handler was installed. */ for (sig = 1; sig < NSIG; sig++) { if (should_handle_signals[sig]) { [self _setHandler:handler forSignal:sig]; } } } + (void)resumeHandlingCrashes { [self setSignalHandler:handle_signal]; ignoreCrashes = NO; } + (void)stopHandlingCrashes { [self setSignalHandler:SIG_DFL]; ignoreCrashes = YES; } #define BACKTRACE_PREFIX_FORMAT "#%-2d 0x%08lx in " + (void)printFunctionFromFP:(void *)framePointer at:(void *)address index:(int)index { char line[1024]; char *buffer = line; unsigned long *argStart; long argNum; // Index into arguments; sprintf(buffer, BACKTRACE_PREFIX_FORMAT, index, (long)address); buffer += strlen(buffer); sprintf(buffer, "function ("); buffer += strlen(buffer); argStart = FRAME_ARG_START(framePointer); for (argNum = 0; argNum < maxFunctionArgs; argNum++) { sprintf(buffer, "%s0x%08lx", ((argNum != 0) ? ", " : ""), marg_getValue(argStart, selfOffset + argNum * sizeof(long) * ARG_DIRECTION, long)); buffer += strlen(buffer); } strcpy(buffer, ")"); [self logMessage:line]; } + (void)printMethodFromFP:(void *)framePointer at:(void *)address index:(int)index { char line[BUFSIZ]; char *buffer = line; SEL selector; const char *selName; id object, class, classIsa; char *argStart; Method m; BOOL isClassMethod; NX_DURING sprintf(buffer, BACKTRACE_PREFIX_FORMAT, index, (long)address); buffer += strlen(buffer); argStart = FRAME_ARG_START(framePointer); object = marg_getValue(argStart, selfOffset, id); selector = marg_getValue(argStart, _cmdOffset, SEL); selName = sel_getName(selector); if (selName == NULL) selName = "<invalid selector>"; class = nil; classIsa = nil; object_getInstanceVariable(object, "isa", (void **)&class); if (class) { object_getInstanceVariable(class, "isa", (void **)&classIsa); } if (!classIsa) { // Freed object. // (in practice classes are never freed, so assume instance method.) sprintf(buffer, "%c[%s0x%06lx %s]", '-', FREED_OBJECT_NAME, (long)object, selName); } else { isClassMethod = IS_CLASS(object); m = (isClassMethod ? class_getClassMethod(class, selector) : class_getInstanceMethod(class, selector)); if (m) { int argNum, numArgs, offset; char *type; *buffer++ = (isClassMethod?'+':'-'); /* Get return type -- XXX assumption: Method types description starts with return type. */ *buffer++ = '('; decode_objc_encoded_type(buffer, sizeof(line) - (buffer - line), m->method_types, 0); buffer += strlen(buffer); *buffer++ = ')'; *buffer++ = '['; decode_objc_encoded_value_for_type(buffer, sizeof(line) - (buffer - line), @encode(id), &object, DECODE_OBJC_VERBOSE); buffer += strlen(buffer); numArgs = method_getNumberOfArguments(m); argNum = 2; // Skip the first two args which are self and _cmd if (numArgs <= argNum) { sprintf(buffer, " %s", selName); } else { while (argNum < numArgs) { int argVerbosity = DECODE_OBJC_VERBOSE; const char *se = strchr(selName, ':'); // intermix selector name parts and arguments. if (se == NULL) { sprintf(buffer, (*selName) ? " %s " : ", ", selName); selName += strlen(selName); } else { ++se; sprintf(buffer, " %.*s", (int)(se - selName), selName); selName = se; } buffer += strlen(buffer); // argument type. method_getArgumentInfo(m, argNum, &type, &offset); *buffer++ = '('; decode_objc_encoded_type(buffer, sizeof(line) - (buffer - line), type, 0); buffer += strlen(buffer); *buffer++ = ')'; // argument value. while (argVerbosity >= 0) { // Retry with less verbosity if recursive crash. NX_DURING decode_objc_encoded_value_for_type(buffer, maxArgValLength, type, marg_getRef(argStart, offset, void), argVerbosity); argVerbosity = -1; NX_HANDLER // If recursive crash, retry with less verbosity. if (argVerbosity & DECODE_OBJC_VERBOSE) { argVerbosity &= ~DECODE_OBJC_VERBOSE; } else { strcpy(buffer, "???"); argVerbosity = -1; } NX_ENDHANDLER } buffer += strlen(buffer); argNum++; } } strcat(buffer, "]"); } else { *buffer++ = (isClassMethod?'+':'-'); *buffer++ = '['; decode_objc_encoded_value_for_type(buffer, sizeof(line) - (buffer - line), @encode(id), &object, DECODE_OBJC_VERBOSE); buffer += strlen(buffer); sprintf(buffer, " %s] <unknown method>", selName); } } NX_HANDLER static const char recursiveCrashMessage[] = " *** recursive crash ***"; if (strlen(line) <= sizeof(line) - sizeof(recursiveCrashMessage)) { strcat(line, recursiveCrashMessage); } NX_ENDHANDLER [self logMessage:line]; } + (void)dumpBacktrace { return [self dumpBacktraceAtFrame:1]; } + (void)dumpBacktraceAtFrame:(unsigned)startFrameNumber { unsigned int frameCount = 0; // counter for number of frames printed void *returnAddress; void *framePointer; if (maxFrames <= 0) return; framePointer = FramePointer(startFrameNumber); if (framePointer == NULL) { if (FramePointer(0) == NULL) { [self logMessage:"*** stack frame Backtrace not yet supported for architecture "__ARCHITECTURE__", sorry..."]; } else { [self logMessage:"*** stack frame Backtrace -- sorry, I'm lost!"]; } return; } [self logMessage:"*** stack frame Backtrace follows:"]; // Get initial return address. returnAddress = FRAME_RETURN_ADDRESS(framePointer); // Assume that a whole lotta frames means either (a) we're trashed or // (b) we're in a recursive deathtrap. In the latter case, we have // probably got enough info to see a whole cycle. // initial NEXT_FRAME to skip self. while (frameCount < maxFrames && (framePointer = NEXT_FRAME(framePointer))) { void *argStart = FRAME_ARG_START(framePointer); SEL sel = marg_getValue(argStart, _cmdOffset, SEL); // If this frame is a method call we will have a valid // selector as second argument. if (sel && sel_isMapped(sel)) { [self printMethodFromFP:framePointer at:returnAddress index:frameCount]; } else { [self printFunctionFromFP:framePointer at:returnAddress index:frameCount]; } ++frameCount; returnAddress = FRAME_RETURN_ADDRESS(framePointer); } if (framePointer) [self logMessage:"... (more frames follow, not listed)"]; } + (BOOL)handleError:(const char *)message { return [self handleError:message atFrame:1]; } + (BOOL)handleError:(const char *)message atFrame:(unsigned)startFrameNumber { if (!ignoreCrashes) { static BOOL crashing; if (!crashing) { crashing = YES; [self logStart]; [self logMessage:message]; if (dumpBacktrace) [self dumpBacktraceAtFrame:(startFrameNumber + 1)]; [self logStop]; if (continueAfterError && [self shouldContinue:message]) { #if DEBUG NXLogError("CrashTrap: trying to continue..."); #endif crashing = NO; NX_RAISE(CrashTrap_Continue, NULL, NULL); return YES; } [self stopHandlingCrashes]; crashing = NO; } else { #if DEBUG fprintf(stderr, "CrashTrap: recursive crash \"%s\"\n", message); #endif NX_RAISE(CrashTrap_RecursiveCrash, NULL, NULL); return YES; } } return NO; } + (BOOL)shouldContinue:(const char *)message { /* XXX this introduces AppKit dependencies. A better solution might be to exlusively let the delegate handle it. */ if (delegate && [delegate respondsTo:@selector(crashTrap:shouldContinueAfterError:)]) { return [delegate crashTrap:self shouldContinueAfterError:message]; } // XXX Localize? // XXX should preallocate this alert panel to be more crashproof? if (NXRunAlertPanel("Error Alert!", "%s\n\nAn internal error has occurred." " Try to save work," " then quit and restart the %s application.\n" "(see the console for details)", "Continue", "CRASH!", NULL, message, appName()) != NX_ALERTDEFAULT) { return NO; } return YES; } + (void)handleObjcError:object format:(const char *)format args:(va_list)args { char buf[BUFSIZ]; sprintf(buf, "objc: <%s> ", object_getClassName(object)); vsprintf(buf + strlen(buf), format, args); if ([self handleError:buf atFrame:3]) return; // skip self, invoking error handler function, and -error:. /* invoke original error handler. */ _originalError(object, format, args); } + (void)handleSignal:(int)sig { const char *desc = NULL; char buf[256]; // unblock this signal. unsigned long mask = sigblock(0) & ~sigmask(sig); sigsetmask(mask); if (0 < sig && sig < NSIG) desc = signal_descriptions[sig]; if (desc == NULL) desc = "Unrecognized signal"; sprintf(buf, "Caught signal #%d (%s)", sig, desc); if ([self handleError:buf atFrame:2]) return; // skip self, and signal handler. /* Here the original signal handler is in effect (or should be, at least.) Re-raise signal to get original intended handling (or lack thereof). */ kill(getpid(), sig); } // Logging. + (void)logStart {} + (void)logMessage:(const char *)message { NXLogError("%s\n", message); } + (void)logStop {} @end // TRHCrashTrap #ifdef POSE_AS_CLASS @implementation _TRHCrashTrapObjectPoser + (void)_crashTrapSetup { [self poseAs:[self superclass]]; } - error:(const char *)aFormat, ... { va_list ap; char buffer[BUFSIZ]; va_start(ap, aFormat); vsprintf(buffer, aFormat, ap); va_end(ap); if ([[TRHCrashTrap handlerClass] handleError:buffer]) return self; return [super error:"%s", buffer]; } + error:(const char *)aFormat, ... { va_list ap; char buffer[BUFSIZ]; va_start(ap, aFormat); vsprintf(buffer, aFormat, ap); va_end(ap); if ([[TRHCrashTrap handlerClass] handleError:buffer]) return self; return [super error:"%s", buffer]; } @end // _TRHCrashTrapObjectPoser #endif typedef struct _sig { short number; BOOL isOn; const char *message; } SignalItem; #define ON 1 #define OFF 0 static const SignalItem signals[] = { {SIGHUP, OFF, "Hangup"}, {SIGINT, OFF, "Interrupt"}, {SIGQUIT, ON, "Quit"}, {SIGILL, ON, "Illegal instruction"}, {SIGTRAP, ON, "Trace trap"}, {SIGIOT, ON, "IOT instruction"}, {SIGEMT, ON, "EMT instruction"}, {SIGFPE, ON, "Floating point exception"}, {SIGKILL, OFF, "Kill"}, {SIGBUS, ON, "Bus error"}, {SIGSEGV, ON, "Segmentation violation"}, {SIGSYS, ON, "Bad argument to system call"}, {SIGPIPE, OFF, "Write on a pipe with no one to read it"}, {SIGALRM, OFF, "Alarm clock"}, {SIGTERM, OFF, "Software termination"}, {SIGURG, OFF, "Urgent condition present on socket"}, {SIGSTOP, OFF, "Stop"}, {SIGTSTP, OFF, "Stop signal generated from keyboard"}, {SIGCONT, OFF, "Continue after stop"}, {SIGCHLD, OFF, "Child status changed"}, {SIGTTIN, OFF, "Background read attempted from control terminal"}, {SIGTTOU, OFF, "Background write attempted to control terminal"}, {SIGIO, OFF, "I/O is possible on a descriptor"}, {SIGXCPU, OFF, "CPU time limit is exceeded"}, {SIGXFSZ, OFF, "File size limit exceeded"}, {SIGVTALRM, OFF, "Virtual timer alarm"}, {SIGPROF, OFF, "Profiling timer alarm"}, {SIGWINCH, OFF, "Window size change"}, {SIGUSR1, OFF, "User defined signal 1"}, {SIGUSR2, OFF, "User defined signal 2"}, {0}, }; static void init_signal_tables() { const SignalItem *cur; for (cur = signals; cur->number; cur++) { signal_descriptions[cur->number] = cur->message; should_handle_signals[cur->number] = cur->isOn; } }
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.