NeXT Programming Tools: Dial On Demand

Some time ago, I saw a news post where the author was proposing a simple solution to an annoying problem:

Newsgroups: comp.sys.next.software
Organization: Antigone Press gateway, San Francisco
From: 476TJL@ptsmail.netcom.com (TIMOTHY LUOMA @ TEMPLE * LUOMA,TIMOTHY)
Cc: nextppp@chinx1.thoughtport.com
Subject: Dial-on-demand software
Date: 06 Oct 95 15:28:00 EDT


I've heard a lot of talk recently about "dial-in demand" (ie, connections 
for PPP/Slip going up when needed, ie for NewsGrazer or OmniWeb).

Joe Freeman (I think) has written a great preference Module which plays 
sounds when something specific happens (ie a disk is loaded or unloaded, 
etc).  Something to do with Workspace response or update.... I don't know 
for sure.

It would seem simple enough (at least, to me, who is admittedly not a 
programmer) that it might be possible to write a little something which 
would watch for OmniWeb/NewsGrazer and the like to be launched, and then 
automatically start up the link.  (Obviously one would want to be able to 
select what apps would trigger this response.)  It would also seem possible 
to watch for the apps which require the 'net to be QUIT and thus bring the 
link down (after checking to see if any other apps are using it).

There seems to be a lot of people who are using their NeXTs via PPP these 
days.  A real need seems to out there.

This is not the best solution, I'll admit.  However, it does seem one which 
would be possible given our current NS technology.

TjL

This all seems very reasonable, so I thought I would write a small app to do this. There are a number of techniques to be used, some more common than others.

One of the commonest techniques used by every single "industrial strength" NeXTSTEP application is to save preference settings in the Defaults database. This is quite simple to do and to manage, and certainly beats using #define statements in include files (which I have often seen used). Along the way, I'll also use the NXTypedStream as an easy way to support persistence for larger objects.

One aspect of object oriented programming that is much talked about, but rarely practiced, is code reuse. This is often taken to mean some abstract skill for designing classes that can be used in other projects; but in the real world, it more often means attempting to knock into shape a class that was never developed with reuse in mind. As a compromise between Scylla and Charybdis, I'll take the more mundane example of taking a non-NeXT supplied class and incorporating that into our project. This reflects true life mcuh better.

Another, much rarer but still useful technique, is access to the Workspace object, and Workspace notification in particular. This will let us be notified every time that an application is launched.

Take a look at this app ported to OpenStep.

Defaults

Every user has a defaults database, which should be used by applications to store values unique to that user. Some values can be set in common between all applications that use the same kind of defaults. There is a small set of function calls that can be used to access the defaults database.

All of these good intentions are sort of broken, because NeXT doesn't publish a list of these commonly used values, and because there is no centralised administration for defaults. The mechanism also changes considerably with OpenStep.

The basic routing is to create a set of defaults that will be using in your appliation, with default values for each setting. This is done by:

   static NXDefaultsVector defaults = {
	{"Command", ""},
	{NULL}
    };

Once this table is created (you can have as many values as you like, so long as the list is terminated by a NULL), you have to register the table; I normally do this in awakeFromNib:

    NXRegisterDefaults("DialOnDemand", defaults);

These defaults can be accessed from the command line if you need to change them outside the scope of the program, possibly in a clean-up script of something of that order:

nagshead> dread -o DialOnDemand
DialOnDemand Command ""
nagshead> dwrite DialOnDemand Command "ls -l /tmp/test.tar.Z"
nagshead> dread -o DialOnDemand
DialOnDemand Command ""
nagshead> 

To set a default value, you have to put the values into a string, possibly using sprintf, and then:

    NXWriteDefault("DialOnDemand", "Command", [command stringValue]);

And to read in a value:

    [command setStringValue:NXGetDefaultValue("DialOnDemand", "Command")];

This shows a commonly used pair of lines of code used to match a TextField toa defaults value. There are a number of different ways to set a default value; NXWriteDefault is reasonable for setting an individual value, but there are better functions available if you need to manipulate a larger number of settings at once.

In addition to direct manipulation of custom default values, it is commonly desired to allow the user to set the size and position of a window, and for the application to retain these settings between executions of the program. The Window class directly supports this:

    [[command window] setFrameAutosaveName:"PrefPanel"];

Again, this is appropriate to put into awakeFromNib.

Reuse: StringList

NeXT provide a MiniExample called StringList (#1254), which includes a couple of classes for use to hold strings of various types. These are reasonably well written, and moderately generic in application. You even get a NeXT style couple of pages of documentation, and the objects included on a palette for convenience. StringList actually does much the same as NXStringTable, with some minor exceptions, but ones that are significant for this example. NXStringTable doesn't include the Browser support, and it is Hashtable based rather than Storage based.

I want to use StringList to hold a list of application names, built up by the user, and stored between executions of the application. This will be used for the list of partial application names to be compared against each application that is launched. If the application matches on of these strings, then we should do something to initialise the network connection. My first decision was that, attractive as the name was, I couldn't use FileList for this purpose, but would have to use StringList.

The rest becomes very simple. Read the documentation, and see how to use it. Because StringList includes a loadColumnZero mthod, it can be used as the delegate of a browser relatively easily.

My first step was to include a StringList object from the palette, and then connect it as the delegate of a browser, and write some support code:

    [fileScroller setTarget:self];
    [fileScroller setAction:@selector(clickScroller:)];
    [fileScroller loadColumnZero];

and:

- clickScroller:sender;
{
    if ([fileScroller selectedCell]) {
	[fileName setStringValue:[[fileScroller selectedCell] stringValue]];
    }
    return self;
}

This is enough to load data from our StringList object; the single click target takes the string from a clicked on cell into a TextField for editing. The TextField in turn allows us to add and delete entries from the list:

- addFileName:sender;
{
    if (strlen([fileName stringValue]) > 0)
	[fileList addStringIfAbsent:[fileName stringValue]];
    [fileScroller loadColumnZero];
...
    [fileName selectText:self];
    return self;
}

- delFileName:sender;
{
    [fileList removeString:[fileName stringValue]];
    [fileScroller loadColumnZero];
...
    [fileName selectText:self];
    return self;
}

- cancelFileOp:sender;
{
    [fileName setStringValue:NXUniqueString("")];
    [fileName selectText:self];
    return self;
}

I'll fill in the gaps shortly. The rest of the code updates the browser display, and puts the cursor into the fileName field (which must be in a Panel, not a Window, to work) for further editing.

File Streams

All objects should support the read: and write: methods, according to NeXT. The underlying reason is that objects can be archived to the file system via an NXTypedStream. I chose to put the file inside the app wraper; this isn't a great location for it, but anyhing else that is flexible is also much more complex to write, and this method will work fine for individual users.

To set up the path to the file:

    char filePath[MAXPATHLEN+1];
...
    strcpy(filePath, [[NXBundle mainBundle] directory]);
    strcat(filePath, "/");
    strcat(filePath, "fileList");

Having done this, we can now set up the stream:

    fileStream = NXOpenTypedStreamForFile(filePath, NX_READONLY);
    if (fileStream == NULL) { 
	fileStream = NXOpenTypedStreamForFile(filePath, NX_WRITEONLY);
	[fileList write:fileStream];
	NXCloseTypedStream(fileStream);
	fileStream = NXOpenTypedStreamForFile(filePath, NX_READONLY);
	if (fileStream == NULL) {
	    NXRunAlertPanel(NULL, "Can't create file list inside app wrapper",
		NULL, NULL, NULL);
	    [NXApp terminate:self];
	}
    }
    [fileList read:fileStream];
    NXCloseTypedStream(fileStream);

This creates the file if it doesn't initially exist, and the loads the StringList object from the file.

The lines of code dropped from the add and delete methods shown above are:

    fileStream = NXOpenTypedStreamForFile(filePath, NX_WRITEONLY);
    [fileList write:fileStream];
    NXCloseTypedStream(fileStream);

and

    fileStream = NXOpenTypedStreamForFile(filePath, NX_WRITEONLY);
    [fileList write:fileStream];
    NXCloseTypedStream(fileStream);

This all works because StringList supports the read: and write: methods properly.

Workspace Notification

If you are updating files on the file system, it is often a good idea to make sure that the Workspace is notified that you have made changes, so that it can update the file viewer appropriately. The Workspace is also able to notify an interested application of a number of useful events. One of those that concerns us is notification on launch of any application. This works in a very similar way to delegation, and is the basis of the changes made with OpenStep towards using "Notification".

You find the Workspace, and tell it of your interests:

    [[Application workspace] beginListeningForApplicationStatusChanges];

All that remains is to write the appropriate methods corresponding to the events you wish to be notified of. In our case, this will involve scanning against the StringList object for all application launches, and running a command in the event that a match is found:

-app:sender applicationWillLaunch:(const char *)appName;
{
    int i;
    
    for (i = 0; i < [fileList count]; i++) {
	if (strstr(appName, [fileList stringAt:i]) != NULL) {
	    system(NXGetDefaultValue("DialOnDemand", "Command"));
	}
    }
    return self;
}

If you would like a copy of the fully formed app:DialOnDemand.app, or you can build from source: DialOnDemand.

Paul_Lynch@plsys.co.uk

Designed by Paul Lynch

using WebPages 1.7

Last updated: Dec 08, 1995