diff --git a/ChangeLog b/ChangeLog index 6d5df8f..3772198 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,11 @@ +2014-11-02 Richard Frith-Macdonald + + * EcProcess.h: + * EcProcess.m: + Add method to register a user default / configuration key to have + updates for a default automatically trigger a method to handle it, + and to provide 'help' documentation for command line arguments. + 2014-11-01 Richard Frith-Macdonald * EcProcess.m: Check for descriptor leaks at 1 minute intervals, diff --git a/EcProcess.h b/EcProcess.h index d365457..9b037fb 100644 --- a/EcProcess.h +++ b/EcProcess.h @@ -430,6 +430,31 @@ extern NSString* cmdVersion(NSString *ver); */ + (NSMutableDictionary*) ecInitialDefaults; +/** Registers an NSUserDefaults key that the receiver understands.
+ * This is primarily intended for user defaults which can reasonably + * be supplied at the command line when a process is started (and for + * which the process should therefore supply help information)
+ * The type text must be a a short string saying what kind of value + * must be provided (eg 'YES/NO') for the default, or nil if no help + * is to be provided for the default.
+ * The help text should be a description of what the default does, + * or nil if no help is to be provided for the default.
+ * The action may either be NULL or a selector for a message to be sent + * to the EcProc instance with a single argument (the new default value) + * when the value of the user default changes.
+ * If the same default name is registered more than once, the values + * from the last registration are used, except for the case where the + * cmd argument is NULL, in that case the previous selector is kept + * in the new rfegistration.
+ * This method should be called in your +initialize method, so that all + * supported defaults are already registered by the time your process + * tries to respond to being started with a --help command line argument. + */ ++ (void) ecRegisterDefault: (NSString*)name + withTypeText: (NSString*)type + andHelpText: (NSString*)help + action: (SEL)cmd; + /** Convenience method to create the singleton EcProcess instance * using the initial configuration provided by the +ecInitialDefaults * method.
@@ -592,18 +617,17 @@ extern NSString* cmdVersion(NSString *ver); - (void) cmdDebug: (NSString*)fmt, ... NS_FORMAT_FUNCTION(1,2); /** Called whenever the user defaults are updated (which may be due to a - * central configuration in additions to other defaults system changes).
+ * central configuration in additions to local defaults system changes).
* This is automatically called by -cmdUpdate: (even if the user defaults - * database has not actually changed), in this case the notification - * argument is nil.
- * If you override this to handle configuration changes, don't forget + * database has not actually changed, in this case the notification + * argument is nil).
+ * This method deals with the updates for any defaults registered using + * the +ecRegisterDefault:withTypeText:andHelpText:action: method, so + * if you override this to handle configuration changes, don't forget * to call the superclass implementation.
- * This method is provided to allow subclasses to control the order - * in which defaults changes are handled by them and their superclasses.
- * Generally, this method is for use handling changes in the local - * NSUserDefaults database; to handle explict configuration changes from - * the central configuration in the Control server, you should usually - * override the -cmdUpdated method instead. + * If you wish to manage updates from the central database in a specific + * order, you may wish to override the -cmdUpdate: and -cmdUpdated methods + * directly. */ - (void) cmdDefaultsChanged: (NSNotification*)n; @@ -700,7 +724,7 @@ extern NSString* cmdVersion(NSString *ver); */ - (int) cmdSignalled; -/** Used to tell your application about configuration changes.
+/** Used to tell your application about central configuration changes.
* This is called before the NSUserDefaults system is updated with the * changes, so you may use it to update internal state in the knowledge * that code watching for user defaults change notifications will not @@ -708,30 +732,38 @@ extern NSString* cmdVersion(NSString *ver); * The base class implementation is responsible for updating the user * defaults system ... so be sure that your implementation calls the * superclass implementation (unless you wish to suppress the configuration - * update).
+ * update) after performing any pre-update operations.
* You may alter the info dictionary prior to passing it to the superclass * implementation if you wish to adjust the new configuration before it * takes effect.
* The order of execution of a configuration update is therefore as follows: * - * Any subclass implementation of -cmdUpdate: is entered. - * - * The base implementation of -cmdUpdate: is entered, the stored - * configuration is changed as necessary, the user defaults database is - * updated. - * - * The -cmdDefaultsChanged: method is called (either as a result of - * a user defaults update, or directly by the base -cmdUpdate: method. + * Any subclass implementation of -cmdUpdate: is entered. * - * The base implementation of the -cmdDefaults: method ends. + * The base implementation of -cmdUpdate: is entered, the stored + * configuration is changed as necessary, the user defaults database is + * updated. * - * Any subclass implementation of the -cmdDefaults: method ends. + * Any subclass of the -cmdDefaultsChanged: method is entere + * (either as a result of the user defaults update, + * or directly by the base -cmdUpdate: method). + * + * The base implementation of the -cmdDefaultsChanged: method is + * entered, and any messages registered using the + * +ecRegisterDefault:withTypeText:andHelpText:action: method are + * sent if the corresponding default value has changed. + * + * The base implementation of the -cmdDefaultsChanged: method ends. + * + * Any subclass implementation of the -cmdDefaultsChanged: method ends. * * The -cmdUpdated method is called. * * - * You should usually override the -cmdUpdated method to handle configuration - * changes, using this method only when you want to check/override changes + * You should usually either register your own methods to handle changes + * to particular defaults values, or override the -cmdDefaultsChanged: + * method to handle general configuration changes.
+ * Use this method only when you want to check/override changes * before they take effect. */ - (void) cmdUpdate: (NSMutableDictionary*)info; @@ -755,6 +787,9 @@ extern NSString* cmdVersion(NSString *ver); * calls the superclass implementation, and if that returns a non-nil * result, you should pass that on as the return value from your own * implementation. + * Use this method only for handling config changes which must be done + * after any code which is watching NSUserDefaultsDidChangNotification + * has run, or for situations where a config error may be fatal. */ - (NSString*) cmdUpdated; @@ -867,7 +902,7 @@ extern NSString* cmdVersion(NSString *ver); * If 'EcHomeDirectory' is not present in the defaults system (or is * an empty string) then no directory change is done.
* Please note, that the base implementation of this method may - * cause other methods (eg -cmdUpdated and -cmdDefaultsChaned) to be called, + * cause other methods (eg -cmdUpdated and -cmdDefaultsChanged:) to be called, * so you must take care that when you override those methods, your own * implementations do not depend on initialisation having completed. * It's therefore recommended that you use 'lazy' initialisation of subclass diff --git a/EcProcess.m b/EcProcess.m index 3f1dc75..cf05b85 100644 --- a/EcProcess.m +++ b/EcProcess.m @@ -98,6 +98,23 @@ #define EC_EFFECTIVE_USER nil #endif + +@interface EcDefaultRegistration : NSObject +{ + NSString *name; // The name/key of the default (without prefix) + NSString *type; // The type text for the default + NSString *help; // The help text for the default + SEL cmd; // method to update when default values change + id obj; // The latest value of the default +} ++ (void) defaultsChanged: (NSUserDefaults*)defs; ++ (void) registerDefault: (NSString*)name + withTypeText: (NSString*)type + andHelpText: (NSString*)help + action: (SEL)cmd; ++ (void) showHelp; +@end + /* Lock for controlling access to per-process singleton instance. */ static NSRecursiveLock *ecLock = nil; @@ -958,6 +975,12 @@ findMode(NSDictionary* d, NSString* s) @end +@interface EcProcess (Defaults) +- (void) _defMemory: (id)val; +- (void) _defRelease: (id)val; +- (void) _defTesting: (id)val; +@end + @interface EcProcess (Private) - (void) cmdMesgrelease: (NSArray*)msg; - (void) cmdMesgtesting: (NSArray*)msg; @@ -1030,6 +1053,17 @@ findMode(NSDictionary* d, NSString* s) count: 2]; } ++ (void) ecRegisterDefault: (NSString*)name + withTypeText: (NSString*)type + andHelpText: (NSString*)help + action: (SEL)cmd +{ + [EcDefaultRegistration registerDefault: name + withTypeText: type + andHelpText: help + action: cmd]; +} + + (void) ecSetup { if (nil != EcProc) @@ -1171,6 +1205,8 @@ static NSString *noFiles = @"No log files to archive"; NSString *str; int i; + [EcDefaultRegistration defaultsChanged: cmdDefs]; + enumerator = [cmdDebugKnown keyEnumerator]; while (nil != (mode = [enumerator nextObject])) { @@ -1195,10 +1231,6 @@ static NSString *noFiles = @"No log files to archive"; [ecLock unlock]; } - GSDebugAllocationActive([cmdDefs boolForKey: @"Memory"]); - [NSObject enableDoubleReleaseCheck: [cmdDefs boolForKey: @"Release"]]; - cmdFlagTesting = [cmdDefs boolForKey: @"Testing"]; - if ((str = [cmdDefs stringForKey: @"CmdInterval"]) != nil) { [self setCmdInterval: [str floatValue]]; @@ -1641,6 +1673,18 @@ NSLog(@"Ignored attempt to set timer interval to %g ... using 10.0", interval); [cmdDebugModes addObject: cmdDefaultDbg]; + [self ecRegisterDefault: @"Memory" + withTypeText: @"YES/NO" + andHelpText: @"Enable memory allocation checks" + action: @selector(_defMemory:)]; + [self ecRegisterDefault: @"Release" + withTypeText: @"YES/NO" + andHelpText: @"Turn on double release checks (debug)" + action: @selector(_defRelease:)]; + [self ecRegisterDefault: @"Testing" + withTypeText: @"YES/NO" + andHelpText: @"Run in test mode (if supported)" + action: @selector(_defTesting:)]; /* * Set the timeouts for the default connection so that * they will be inherited by other connections. @@ -3479,6 +3523,7 @@ NSLog(@"Ignored attempt to set timer interval to %g ... using 10.0", interval); else { NSProcessInfo *pinfo; + NSArray *args; NSFileManager *mgr; NSEnumerator *enumerator; NSString *str; @@ -3492,6 +3537,7 @@ NSLog(@"Ignored attempt to set timer interval to %g ... using 10.0", interval); started = RETAIN([dateClass date]); pinfo = [NSProcessInfo processInfo]; + args = [pinfo arguments]; mgr = [NSFileManager defaultManager]; prf = EC_DEFAULTS_PREFIX; if (nil == prf) @@ -3507,28 +3553,69 @@ NSLog(@"Ignored attempt to set timer interval to %g ... using 10.0", interval); [cmdDefs registerDefaults: defs]; } - if ([[pinfo arguments] containsObject: @"--help"]) + if ([args containsObject: @"--help"] || [args containsObject: @"-H"]) { - NSLog(@"Standard command-line arguments ...\n\n" - @"-%@CommandHost [aHost] Host of command server to use.\n" - @"-%@CommandName [aName] Name of command server to use.\n" - @"-%@Daemon [YES/NO] Fork process to run in background?\n" - @"-%@EffectiveUser [aName] User to run as\n" + GSPrintf(stderr, @"Standard command-line arguments ...\n\n"); + + if ([self isKindOfClass: NSClassFromString(@"EcControl")]) + { + GSPrintf(stderr, +@"-%@Daemon NO Run process in the foreground.\n", + prf); + } + else if ([self isKindOfClass: NSClassFromString(@"EcConsole")]) + { + GSPrintf(stderr, +@"-%@ControlHost [aHost] Host of the Control server to use.\n" +@"-%@ControlName [aName] Name of the Control server to use.\n" +@"-%@Daemon [YES/NO] Fork process to run in background?\n", + prf, prf, prf); + } + else if ([self isKindOfClass: NSClassFromString(@"EcCommand")]) + { + GSPrintf(stderr, +@"-%@ControlHost [aHost] Host of the Control server to use.\n" +@"-%@ControlName [aName] Name of the Control server to use.\n" +@"-%@Daemon NO Run process in in the foreground.\n", + prf, prf, prf); + } + else + { + GSPrintf(stderr, +@"-%@CommandHost [aHost] Host of the Command server to use.\n" +@"-%@CommandName [aName] Name of the Command server to use.\n" +@"-%@Daemon [YES/NO] Fork process to run in background?\n", +@"-%@Transient [YES/NO] Expect this process be short-lived?\n", + prf, prf, prf, prf); + } + + GSPrintf(stderr, @"\n"); + GSPrintf(stderr, + @"-%@CoreSize [MB] Maximum core dump size\n" + @" 0 = no dumps, -1 = unlimited\n" + @"-%@DescriptorsMaximum [N]\n" + @" Set maximum file descriptors to use\n" + @"-%@Debug-name [YES/NO] Turn on/off the named type of debug\n" + @"-%@EffectiveUser [aName] User to run this process as\n" @"-%@HomeDirectory [relDir] Relative home within user directory\n" @"-%@UserDirectory [dir] Override home directory for user\n" @"-%@Instance [aNumber] Instance number for multiple copies\n" - @"-%@Memory [YES/NO] Enable memory allocation checks?\n" + @"-%@MemoryAllowed [MB] Expected memory usage (before alerts)\n" + @"-%@MemoryIncrement [KB] Absolute increase in alert threshold\n" + @"-%@MemoryMaximum [MB] Maximum memory usage (before restart)\n" + @"-%@MemoryPercentage [N] Percent increase in alert threshold\n" @"-%@ProgramName [aName] Name to use for this program\n" - @"-%@Testing [YES/NO] Run in test mode (if supported)\n" @"\n--version to get version information and quit\n\n", - prf, prf, prf, prf, prf, prf, prf, prf, prf, prf - ); + prf, prf, prf, prf, prf, prf, prf, prf, prf, prf, prf, prf); + + [EcDefaultRegistration showHelp]; + RELEASE(self); [ecLock unlock]; return nil; } - if ([[pinfo arguments] containsObject: @"--version"]) + if ([args containsObject: @"--version"]) { NSLog(@"%@ %@", [self ecCopyright], cmdVersion(nil)); RELEASE(self); @@ -4519,3 +4606,202 @@ NSLog(@"Ignored attempt to set timer interval to %g ... using 10.0", interval); @end +@implementation EcProcess (Defaults) +- (void) _defMemory: (id)val +{ + GSDebugAllocationActive([val boolValue]); +} +- (void) _defRelease: (id)val +{ + [NSObject enableDoubleReleaseCheck: [val boolValue]]; +} +- (void) _defTesting: (id)val +{ + cmdFlagTesting = [val boolValue]; +} +@end + +@implementation EcDefaultRegistration + +static NSMutableDictionary *regDefs = nil; + ++ (void) defaultsChanged: (NSUserDefaults*)defs +{ + NSEnumerator *e; + NSString *n; + + [ecLock lock]; + e = [[regDefs allKeys] objectEnumerator]; + [ecLock unlock]; + while (nil != (n = [e nextObject])) + { + EcDefaultRegistration *d; + id o = nil; + SEL c = NULL; + + [ecLock lock]; + d = [regDefs objectForKey: n]; + if (nil != d) + { + o = [defs objectForKey: n]; + if (o != d->obj && NO == [o isEqual: d->obj]) + { + ASSIGNCOPY(d->obj, o); + o = d->obj; + c = d->cmd; + } + } + [ecLock unlock]; + if (NULL != c && [EcProc respondsToSelector: c]) + { + [EcProc performSelector: c withObject: o]; + } + } +} + ++ (void) initialize +{ + regDefs = [NSMutableDictionary new]; +} + ++ (void) registerDefault: (NSString*)name + withTypeText: (NSString*)type + andHelpText: (NSString*)help + action: (SEL)cmd +{ + static NSCharacterSet *w = nil; + EcDefaultRegistration *d; + + if (nil == w) + { + w = RETAIN([NSCharacterSet whitespaceAndNewlineCharacterSet]); + } + if ([type length] > 0) + { + type = [type stringByTrimmingSpaces]; + if ([type length] == 0) + { + type = nil; + } + else + { + NSUInteger length = [type length]; + NSMutableString *m = nil; + + while (length-- > 0) + { + unichar u = [type characterAtIndex: length]; + + if (u != ' ' && [w characterIsMember: u]) + { + if (nil == m) + { + m = AUTORELEASE([type mutableCopy]); + type = m; + } + [m replaceCharactersInRange: NSMakeRange(length, 1) + withString: @" "]; + } + } + } + } + if ([help length] > 0) + { + help = [help stringByTrimmingSpaces]; + if ([help length] == 0) + { + help = nil; + } + } + + [ecLock lock]; + d = [regDefs objectForKey: name]; + if (nil == d) + { + d = [EcDefaultRegistration new]; + ASSIGNCOPY(d->name, name); + [regDefs setObject: d forKey: d->name]; + RELEASE(d); + } + ASSIGNCOPY(d->type, type); + ASSIGNCOPY(d->help, help); + if (0 != cmd) + { + d->cmd = cmd; + } + [ecLock unlock]; +} + ++ (void) showHelp +{ + NSArray *keys; + NSString *prf; + NSEnumerator *e; + NSString *k; + NSUInteger max = 0; + + prf = EC_DEFAULTS_PREFIX; + if (nil == prf) + { + prf = @""; + } + + keys = [regDefs allKeys]; + e = [keys objectEnumerator]; + while (nil != (k = [e nextObject])) + { + EcDefaultRegistration *d = [regDefs objectForKey: k]; + + if (nil != d->type && nil != d->help) + { + NSUInteger length = [prf length] + 5; + + length += [k length] + [d->type length]; + if (length > max) + { + max = length; + } + } + } + + keys = [keys sortedArrayUsingSelector: @selector(compare:)]; + e = [keys objectEnumerator]; + while (nil != (k = [e nextObject])) + { + EcDefaultRegistration *d = [regDefs objectForKey: k]; + + if (nil != d->type && nil != d->help) + { + /* If the help text is short enough, put it all on one line. + */ + if ([d->help length] + max < 80) + { + NSMutableString *m; + + m = [NSMutableString stringWithFormat: @"-%@%@ [%@] ", + prf, k, d->type]; + while ([m length] < max) + { + [m appendString: @" "]; + } + GSPrintf(stderr, @"%@%@\n", m, d->help); + } + else + { + GSPrintf(stderr, @"-%@%@ [%@]\n %@\n", prf, k, d->type, d->help); + } + } + } +} + +- (void) dealloc +{ + RELEASE(name); + RELEASE(type); + RELEASE(help); + RELEASE(obj); + [super dealloc]; +} + +@end +