make command line argument handling and documentation easier

git-svn-id: svn+ssh://svn.gna.org/svn/gnustep/libs/ec/trunk@38148 72102866-910b-0410-8b05-ffd578937521
This commit is contained in:
Richard Frith-MacDonald 2014-11-02 14:30:24 +00:00
parent 27911d0938
commit 6db4067e56
3 changed files with 369 additions and 40 deletions

View file

@ -1,3 +1,11 @@
2014-11-02 Richard Frith-Macdonald <rfm@gnu.org>
* 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 <rfm@gnu.org>
* EcProcess.m: Check for descriptor leaks at 1 minute intervals,

View file

@ -430,6 +430,31 @@ extern NSString* cmdVersion(NSString *ver);
*/
+ (NSMutableDictionary*) ecInitialDefaults;
/** Registers an NSUserDefaults key that the receiver understands.<br />
* 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)<br />
* 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.<br />
* The help text should be a description of what the default does,
* or nil if no help is to be provided for the default.<br />
* 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.<br />
* 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.<br />
* 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.<br />
@ -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).<br />
* central configuration in additions to local defaults system changes).<br />
* This is automatically called by -cmdUpdate: (even if the user defaults
* database has not actually changed), in this case the notification
* argument is nil.<br />
* If you override this to handle configuration changes, don't forget
* database has not actually changed, in this case the notification
* argument is nil).<br />
* 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.<br />
* This method is provided to allow subclasses to control the order
* in which defaults changes are handled by them and their superclasses.<br />
* 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.<br />
/** Used to tell your application about central configuration changes.<br />
* 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).<br />
* update) after performing any pre-update operations.<br />
* 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.<br />
* The order of execution of a configuration update is therefore as follows:
* <list>
* <item>Any subclass implementation of -cmdUpdate: is entered.
* </item>
* <item>The base implementation of -cmdUpdate: is entered, the stored
* configuration is changed as necessary, the user defaults database is
* updated.
* </item>
* <item>The -cmdDefaultsChanged: method is called (either as a result of
* a user defaults update, or directly by the base -cmdUpdate: method.
* <item>Any subclass implementation of -cmdUpdate: is entered.
* </item>
* <item>The base implementation of the -cmdDefaults: method ends.
* <item>The base implementation of -cmdUpdate: is entered, the stored
* configuration is changed as necessary, the user defaults database is
* updated.
* </item>
* <item>Any subclass implementation of the -cmdDefaults: method ends.
* <item>Any subclass of the -cmdDefaultsChanged: method is entere
* (either as a result of the user defaults update,
* or directly by the base -cmdUpdate: method).
* </item>
* <item>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.
* </item>
* <item>The base implementation of the -cmdDefaultsChanged: method ends.
* </item>
* <item>Any subclass implementation of the -cmdDefaultsChanged: method ends.
* </item>
* <item>The -cmdUpdated method is called.
* </item>
* </list>
* 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.<br />
* 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.<br />
* 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

View file

@ -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