TweetFollow Us on Twitter

Yenta
Volume Number:11
Issue Number:2
Column Tag:Appletalk

Yenta and the Appletalk Class Library

ChatterBox for the aspiring MOOSE

By Eric Rosé, cp3a@andrew.cmu.edu

Note: Source code files accompanying article are located on MacTech CD-ROM or source code disks.

A Historical Note

The infancy of my Macintosh programming life was spent writing network software and hacking Appletalk code; the scars have mostly healed. IAC and the Communication Toolbox have made network software easier to write, but there is still a burden on the programmer to manage zone and node identification, as well as actually sending the data - especially if you want to provide a different interface than the PPC Browser, or if you want to communicate invisibly (eg. an e-mail driver which should seamlessly perform the task of finding other people it can send messages to).

This first part of this article describes the Appletalk Class Library - a modular and extensible approach to Appletalk communication using Think C++. The ACL is a set of objects which encapsulate tasks like initializing Appletalk, getting lists of zones, looking up and confirming addresses, and sending data between nodes. These objects are fully asynchronous, and provide a way for you to specify completion routines which can move memory (a big plus!). The second part of this article shows how to use these classes in conjunction with a number of TCL-like but fully C++ interface classes to build “Yenta” - a “chat” application in the spirit of Ron Hafernik’s “ChatterBox” program (yay Ron!).

To Spare You The Trouble

The ACL itself consists of about 2800 lines of code, which clearly prohibits me from going into all of it in detail in this article. Much of the Appletalk code within the ACL has been seen before either in IM:IAC or DTS programs such as GetZoneList or DMZ. My general approach will be to give the declaration for each class I discuss and describe how it can be used, only showing method definitions when they illustrate some interesting or important concepts and programming strategies. The interface classes which I use to build Yenta are also of my own devising and consist of about 9000 lines of code. Since the focus of this article is on the ACL, I will not discuss them except when absolutely necessary. All the code for the ACL and the Yenta application is available from Xplain.

Appletalk Review

Anyone who wants the real lowdown on Appletalk network topology and protocols should read the second edition of “Inside Appletalk” or Michael Peirce’s book “Programming with Appletalk”. These details are not important for the development of the ACL, so this review will talk primarily about what kind of information we have to keep track of in order to communicate on an Appletalk network.

Every Appletalk network consists of zero or more zones; each zone is represented by a unique name. If there is no zone at all, the name “*” is used as a default. Once you have the zone name you can perform lookups, confirmations, etc. Every ‘visible’ entity within a zone has a unique name which is specified by an object name, a type, and the zone name; the commonly used format is name:type@zone. All network entities also have a numerical address made up of a node number (the ID of the computer they are on), a socket number (a number between 1 and 255 which specifies a socket on the computer) and a zone number. There may be multiple zone numbers assigned to each zone name. Some things you can do with network addresses are 1) formally register them on the network (making them visible); 2) deregister them (making them invisible); 3) look for addresses matching a certain name or pattern; and 4) confirm an address which you obtained earlier.

The ACL uses the PPC Toolbox (IM:IAC, ch 11) to do its data transmission. The PPC Toolbox extends the ‘address’ of a network entity by adding to the “name:type@zone” identifier a theoretically infinite number of ports - each described by a unique “pname:ptype” pair. For the purpose of simplicity, the ACL uses the same ‘name’ for both the location name and the port name, although you can still specify different location and port types if you want.

Asynchronicity is a vitally important aspect of Appletalk communication. Ideally all communication will be asynchronous so that we can be doing node lookups and confirmations while we are sending large amounts of data. To determine when such a task completes, you either have to poll a flag in the task record, or assign a completion routine which is limited in power because it can’t move memory; generally they simply set global variables which are polled by the application. The ACL provides an object (CPPPeriodicTask) which acts as a wrapper for periodic tasks, and a second class (CPPPeriodicTaskManager) which performs all of the polling operations for you. All you have to do is specify a method or routine to be run when the task completes, hand it to a CPPPeriodicTaskManager object and let things take care of themselves. Because your method is not called at interrupt time, as would a traditional completion routine, you can feel free to move as much memory as you want to.

OOP(s)!

Enough talk of what has to be done; let’s talk about how to do it! All of the nifto features we’ve discussed above are implemented in the ACL in three abstract base classes and nine concrete classes. To build on a firm foundation, we’ll look at the abstract classes first.

Laying the Foundation

CPPObject

All of the objects in the ACL have a single abstract base class called CPPObject (the declaration for which is shown below). Besides providing a common ancestor, this class gives every object the potential for making an exact copy of it (Clone) or returning its name as a c-string to anyone who asks (ClassName). We will see the utility of this feature later.

class CPPObject {
public:
 CPPObject (void);
 virtual~CPPObject (void);
 
 virtualchar*ClassName (void);
 virtualCPPObject*Clone (void);    
};

Asynchronicity

Directly descended from CPPObject is CPPPeriodicTask - an object which encapsulates the data and methods needed to maintain an asynchronous task. Associated with CPPPeriodicTask is CPPTaskManager, which maintains a list of all periodic tasks and is responsible for making sure that each task is called at least as often as it needs to be. Here are declarations for these classes. I elaborate on them below.

typedef void (*CompletionProc)(CPPObject *TheTask);

class CPPPeriodicTask : public CPPObject {
public:
 BooleanisRunning;
 BooleanhasCompleted;
 BooleandeleteWhenDone;

 CPPPeriodicTask (CPPTaskManager *TaskManager, 
 long minPeriod = 120,
 Boolean deleteWhenCompleted = TRUE);
 ~CPPPeriodicTask (void);
 virtualchar*ClassName (void);

    // Setter and accessor methods
 long   GetPeriod (void);
 long   GetTimesCalled (void);
 void   SetPeriod (long newPeriod);
 OSErr  TaskError (void);
 void   SetCompletionProc (CompletionProc NewProc);

    // user-specific methods
 virtualBoolean  NeedsToRun (void);
 virtualvoidDoPeriodicAction (void);
 virtualvoidDoCompletedAction (void);
protected:
 OSErr  callResult;
 CPPTaskManager  *ourManager;
 CompletionProc  completion;
private:
 long   minimumPeriod;
 long   lastCalled;
 long   timesCalled;
};

class CPPTaskManager : public CPPObjectList {
public:
 CPPTaskManager (void);
 ~CPPTaskManager (void);
 virtualchar   *ClassName (void);

 Boolean AddPeriodicTask (CPPPeriodicTask *TheTask);
 Boolean RemovePeriodicTask (CPPPeriodicTask *TheTask,
  Boolean disposeOfTask);
 long HowManyTasksOfType (char *ClassName);
 void   RunPeriodicTasks (void);
 short  HowManyPeriodicTasks (void);
};

CPPTaskManager (PTM) is subclassed off of an object which maintains a list of CPPObjects. Since CPPObject is the base class of the entire ACL, this list can hold any object in the ACL; in this case the list only contains CPPPeriodicTask objects, or any object descended from them.

The AddPeriodicTask and RemovePeriodicTask methods are semantically identical to append and delete operations on lists. One thing to note is that the ‘Remove’ method gives you the option of disposing of the task or merely removing it from the list. The two ‘HowMany???’ methods let you count the total number of objects in the queue, or the number of objects belonging to a specific class. The code for HowManyTasksOfType is shown below, and demonstrates the use of the ClassName method.

long CPPTaskManager::HowManyTasksOfType (char *ClassName)
/* returns how many objects of type “ClassName” are in the queue */
{
 long i = 1;
 long Count = 0;
 
 while (i <= numItems)
   {
 if (strcmp (((*this)[i])->ClassName(), ClassName) == 0)
   Count ++;
 i++;
   }
 return Count;
}

The key method in the PTM class is RunPeriodicTasks, which iterates through all the tasks which it manages, executing those whose periods have expired and removing those which have completed. The code for this method is shown below:

void CPPTaskManager::RunPeriodicTasks (void)
/* Give all of the periodic tasks we manage a chance to execute */
{
 long i = 1;
 CPPPeriodicTask *TheTask;
 
 while (i <= numItems)
   {
   if (TheTask = (CPPPeriodicTask *)((*this)[i]))
     {
                  // execute the task if it needs to run
   if (TheTask->NeedsToRun())
     {
      TheTask->isRunning = TRUE;
      BroadcastMessage (kTaskActivated, (void *)i);
       TheTask->DoPeriodicAction();
       TheTask->isRunning = FALSE;
      BroadcastMessage (kTaskDeactivated, (void *)i);
     }
   
                  // if it has completed remove it from the queue
   if (TheTask->hasCompleted)
     {
       CPPList::DeleteItem(i, FALSE);// Dequeue w/o deleting
       TheTask->DoCompletedAction(); // Perform final operation
       if (TheTask->deleteWhenDone)// Dispose of the task if 
         delete TheTask;  // requested
     }
   else // advance to the next item if we don't delete this one
     i++;
     }
   }
}

On each call to RunPeriodicTasks, each task in the queue is asked to determine whether or not it needs to run; if it does, the PTM calls DoPeriodicAction (the contents of which will be explained in a minute). If during execution, the periodic task determines that it is done, the PTM removes the task from the queue, calls DoCompletedAction to let the task perform any special final actions, and then deletes it, if requested to. Ideally RunPeriodicTasks should be called once during every iteration of the application’s main event loop in order to give enough processing time to the tasks in the PTM.

CPPPeriodicTask is an abstract base class whose only job is to maintain all the bookkeeping information which the PTM needs to service it. Each task has associated with it a minimum period which indicates (in ticks) how often it is to run. Unlike the Time Manager, this is not a guaranteed rate of service - it merely indicates that the task will be run no more often than once every n ticks. While this is an acceptable strategy for polling periodic lookup/read/write tasks, I wouldn’t advise it for controlling vital processes.

The key functional methods in CPPPeriodicTask are NeedsToRun, DoPeriodicAction and DoCompletedAction. The basic NeedsToRun method subtracts the last called time of the task from the current time, and returns TRUE if that amount exceeds minimumPeriod. If you want you could override it to take the state of the task into account, as well as the period.

The basic DoPeriodicAction method merely increments the timesCalled variable and stores the current time in lastCalled. Any override of DoPeriodicAction should call the inherited method so that this bookkeeping information is maintained. An important job which must be done by the user’s DoPeriodicAction method is setting the public hasCompleted flag, which the PTM checks to see whether the task should be dequeued.

DoCompletedAction allows the user to customize the task’s final behavior in two ways. Here is the code for the basic method:

 
void CPPPeriodicTask::DoCompletedAction (void)
/* execute the completion routine, if there is one */
{
 if (this->completion)
   (*this->completion)((CPPObject *)this);
}

One way to customize DoCompletedAction would be to overload it to perform any necessary data copying, notification, etc. The other way is to use SetCompletionProc to specify a routine to be called explicitly. Why do we need to be able to do it both ways? Glad you asked! As an example, most of the ACL’s lookup tasks allocate goodly-sized chunks of memory which are only used when a lookup is underway. Principles of information hiding make it more logical to dispose of this memory in an overridden method than an external routine.

On the other hand, many times when we do a lookup we would like the results to be displayed somewhere. Principles of information hiding argue that this kind of ‘display-specific’ code should not be placed inside the task itself. In addition, it would become tedious and wasteful to have to create several subclasses of the same kind of lookup task which differ only in their completion routines. This all becomes much clearer when we discuss the construction of the Yenta application, so don’t worry if this doesn’t quite make sense yet.

Poring (over) The Concrete

Now that we have discussed the abstract classes, let’s move on to the ones which do the gruntwork.

MaBell

In order to use Appletalk, you need a set of maintenance routines which let you do things like find out driver numbers, turn self-send capability on and off, initialize the PPC Toolbox, etc. To save the programmer worry (and time spent flipping through IM) I encapsulated these routines in a class called CPPMaBell, whose declaration is shown below

class CPPMaBell : public CPPObject {
public:
 CPPMaBell (Boolean AllowSelfSend = TRUE);
 ~CPPMaBell (void);
 
 virtualchar   *ClassName (void);

    // General Appletalk Utilities
 OSErr  OpenATalkDriver (short whichDrivers);
 short  GetDriverNumber(short whichDriver);
 short  GetAtalkVersion (void);
 BooleanHasPhase2ATalk (void);
 BooleanEnableSelfSend (void);
 BooleanDisableSelfSend (void);
 BooleanSelfSendEnabled (void);

    // PPC Toolbox routines
 OSErr  InitPPCToolbox (void);
 OSErr  OpenCommunicationPort (StringPtr OurName,
        StringPtr PortType,
        PPCPortRefNum *newRefNum);
 OSErr  CloseCommunicationPort (PPCPortRefNum portRefNum);
 OSErr  EndSession (PPCSessRefNum  sessionID);
private:
 short  XPPRefNum,
 MPPRefNum,
 ADSPRefNum;
 short  AtalkVersionNumber;
 BooleanSelfSendState;
};

The majority of these routines are unspectacular, but necessary. One routine of interest is OpenCommunicationPort, which shows how to open a PPC Toolbox Port. This code modifies the example code in IM:IAC, p. 11-21, by setting the connection’s full address to “OurName:PPCCommPort” at location “?’s Macintosh: OurName•PortType@ zone”.

OSErr CPPMaBell::OpenCommunicationPort (StringPtr OurName,
   StringPtr PortType,
   PPCPortRefNum *newRefNum)
{
 PPCOpenPBRec    OpenRec;
 PPCPortRec PortRec;
 LocationNameRec LocationRec;
 EntityName TheName;
 OSErr  ErrCode;
 
    // set up the port specification record
 PortRec.nameScript = smRoman;
 PortRec.portKindSelector = ppcByString;
 CopyString (OurName, PortRec.name);
 CopyString ("\pPPCCommPort", PortRec.u.portTypeStr);
 
    // set up the record which describes who we are
 LocationRec.locationKindSelector = ppcNBPTypeLocation;
 PStrCat(32, &LocationRec.u.nbpType[0], 3, OurName, 
  "\p•", PortType);
 // set up the 'open port' record
 OpenRec.serviceType = ppcServiceRealTime;
 OpenRec.resFlag = 0;
 OpenRec.portName = &PortRec;
 OpenRec.locationName = &LocationRec;
 OpenRec.networkVisible = TRUE;
 
    // make the call and, if no errors, get the port number
 if ((ErrCode = PPCOpen (&OpenRec, FALSE)) == noErr)
   *newRefNum = OpenRec.portRefNum;
 else
   *newRefNum = -1;
 return ErrCode;
}

CPPNodeInfo

As mentioned in the Appletalk Review, network entities can be identified using three strings and/or three numbers. The CPPNodeInfo object whose declaration is shown below is the ACL’s common denominator for representing network entities. All six pieces of address data are freely settable and accessible, placing no restrictions on the protocol you use to communicate with (eg. DDP uses only the numbers, PPC only the names).

class CPPNodeInfo : CPPObject {
public:
 CPPNodeInfo (void);
 ~CPPNodeInfo (void);
 virtualchar*ClassName (void);

 void   SetNodeName (StringPtr ObjectStr, StringPtr TypeStr, 
 StringPtr ZoneStr);
 void   SetNodeAddress (short SocketNum, short NodeNum, 
 short ZoneNum);
 void   GetNodeName(StringPtr *ObjectStr, StringPtr *TypeStr, 
 StringPtr *ZoneStr);
 void   GetNodeAddress (short *SocketNum, short *NodeNum, 
 short *ZoneNum);
 StringPtrReturnNameString (void);
 StringPtrReturnShortNameString (void);

 BooleanRegisterNodeAddress (OSErr *ErrCode);
 BooleanDeregisterNodeAddress (OSErr *ErrCode);
 BooleanConfirmNodeAddress (OSErr *ErrCode);

 BooleanEquals (CPPNodeInfo *TestNA);
 CPPObject*Clone (void);
 Ptr    InfoToStream (void);
private:
 NamesTableEntry *NTE;  // only defined if registered
 StringPtrobjectString,
 typeString,
 zoneString;
 unsigned char socketNumber,
 nodeNumber;
 short  zoneNumber;
 BooleanisRegistered;
};
CPPNodeInfo *StreamToInfo (Ptr Buffer, Ptr *BufferEnd);

In addition to managing the network address and name data, CPPNodeInfo lets you register and deregister address on the network, so that any CPPNodeInfo entity can be made visible to the outside world. For convenience, a synchronous routine, ConfirmNodeAddress, has been provided to let you check to see if the entity’s address has changed behind your back.

InfoToStream and StreamToInfo provide the ability to write the node’s address out to a block of memory and then reconstruct a CPPNodeInfo record from such a block of memory. This is provided so that you can send addresses back and forth between machines without any loss of data.

Finally, CPPNodeInfo provides a Clone method which creates a CPPNodeInfo object which refers to the same object as the original, and an Equals method, which tests a passed-in object to see if it refers to the same object as the one which makes the method call.

411 - CPPPeriodicTask Returns

In the Appletalk Review, we talked about two different kinds of lookup we want to be able to do - lookup of zone names (including the zone we are in) and lookup of all network entities whose names match a predefined pattern. In the ACL, these two jobs are performed by subclasses of CPPPeriodicTask, affectionately known as CPPZone411 and CPPNode411. Here are their declarations (for brevity, I’ve omitted their private variable and method declarations).

class CPPZone411 : public CPPPeriodicTask {
public:
 CPPZone411 (CPPTaskManager *TaskManager,
     CPPMaBell *MaBell, long minPeriod = 120, 
     Boolean deleteWhenDone = TRUE);
 ~CPPZone411 (void);
 virtualchar   *ClassName (void);

 virtual void  DoPeriodicAction (void);
 virtual void  DoCompletedAction (void);
 BooleanNthZone (long whichItem, Boolean getCopy, 
   StringPtr *ZoneName);
 long   NumZonesFound (Boolean *isDone);
 
 void   StartZoneLookup (CompletionProc DoProc);
 
 StringPtrGetOurZoneName (OSErr *ErrCode);
 CPPStringList *GetFoundList (void);
 
protected:
 CPPStringList *FoundList;
private:
...
};

class CPPNode411 : public CPPPeriodicTask {
public:
 CPPNode411 (CPPTaskManager *TaskManager, 
 long minPeriod = 120, 
 Boolean deleteWhenDone = TRUE);
 ~CPPNode411 (void);
 virtual char *ClassName (void);

 virtual void  DoPeriodicAction (void);
 virtual void  DoCompletedAction (void);
 
 CPPNodeInfo   *NthNode (long whichItem, Boolean getCopy);
 long   NumNodesFound (Boolean *isDone);
 void   StartNodeLookup (StringPtr ObjectName,
  StringPtr TypeName, 
  StringPtr ZoneName,
  short maxNumResponses,
  CompletionProc DoProc);
 CPPObjectList *GetFoundList (void);
protected:
 CPPObjectList *FoundList;
private:
...
}

Each class maintains a list of names or nodes which it has found, and provides a similar interface (Nth???, Num???Found, and GetFoundList) for accessing them. CPPZone411 provides an additional routine called GetOurZoneName which returns the name of the network our machine resides on. The details of how to perform zone and node lookup have been discussed many times in IM and programs like DMZ and GetZoneList from Apple DTS, so I won’t go into those here. It is, however, worth looking at the DoPeriodicAction, DoCompletedAction, and Start???Lookup methods to see how you can use the CPPPeriodicTask abstract class to do something useful. We’ll look at CPPNode411, since node lookup is considerably more straightforward.

To begin a node lookup, one calls the StartNodeLookup method, passing it the name, type, and zone names to match against, along with the maximum number of responses you want, and the address of a routine to call on completion. Here are the guts of StartNodeLookup.

void CPPNode411::StartNodeLookup (StringPtr ObjectName,
  StringPtr TypeName, 
  StringPtr ZoneName,
  short maxNumResponses,
  CompletionProc DoProc)
{
 if ((!this->hasCompleted) || (!this->ourManager))
   return;// exit if doing a lookup or bogus params
 
    // otherwise, set up the call
 this->SetCompletionProc(DoProc);
 this->hasCompleted = FALSE;// note we are not done
 this->FoundList->DeleteItems(TRUE); // get rid of old results
 
 SetupNBPCall(ObjectName, TypeName, ZoneName, maxNumResponses);
 
 if (this->callResult == noErr)
   this->ourManager->AddPeriodicTask(this);  // now add the task
}

The first thing the task does is check its own hasCompleted flag to see if it is currently doing a lookup. Once it determines that it is not already busy, it sets the flag to FALSE, so that the PTM will not toss it out as soon as it is enqueued. It then clears any nodes which may have been found earlier from its list of ‘found’ nodes. SetupNBPCall is a private method which allocates the necessary storage, fills in the paramBlock for an NBP call, and makes the PLookupName call - storing its result in the class’ callResult variable. If the call is made successfully, the only thing left to do is add the task to the PTM’s list of serviceable tasks. All subsequent activity is managed by the DoPeriodic/CompletedTask methods.

Here’s CPPNode411’s DoPeriodicAction method:

void CPPNode411::DoPeriodicAction (void)
{
    // call the inherited method to update frequency count
 CPPPeriodicTask::DoPeriodicAction();
 
 switch (this->lookupRec->MPPioResult) {
 case noErr :  // the call has completed
 this->ProcessNodes(lookupRec->NBPnumGotten);
 this->hasCompleted = TRUE;
 this->callResult = noErr;
 break;
 
 case 1  :// still busy getting names
 break;
 
 default: // an error occurred
 this->callResult = lookupRec->MPPioResult;
 hasCompleted = TRUE;
 break;
 }
}

As one might expect, the periodic task checks the ioResult parameter of the paramBlock which was used to make the PLookupName call and responds appropriately. If it has completed successfully, it calls ProcessNodes which extracts all of the nodes from the lookup buffer, then sets the hasCompleted flag so the PTM will remove it from the queue. If the call has completed with an error, the task also sets the hasCompleted flag, but records the error so that the programmer can use the TaskError method to find out what went wrong.

When RunPeriodicTasks sees that the hasCompleted flag is set, it will call CPPNode411’s DoCompletedAction method:

 void CPPNode411::DoCompletedAction (void)
 {
 NukePtr(this->returnBuffer);
 NukePtr(this->lookupRec);
 inherited::DoCompletedAction();
 }

As mentioned earlier, this method is used exclusively to free up memory allocated in StartNodeLookup before calling the inherited method which will call the completion routine the user passed to StartNodeLookup. Here’s an example of such a completion routine:

void NodeLookupCompleted  (CPPObject *TheTask)
/* add each 'found' node to a list of users */
{
 CPPNode411 *LookupTask = (CPPNode411 *)TheTask;
 BooleanBTemp;
 long   numFound;
 
 numFound = LookupTask->NumNodesFound(&BTemp);
 if (numFound && BTemp)
   {
 for (long i = 1; i <= numFound; i++)
     UserList->AddNode(LookupTask->NthNode (i, FALSE));
   }
}

Though the guts of the Start, DoPeriodic, and DoCompleted code are different in CPPZone411 and every other periodic task, the basic strategy remains the same: allocate memory for the call in Start, check the ioResult flag in DoPeriodicAction, and deallocate the memory in DoCompletedAction.

A third task which falls partially under the pervue of 411 is CPPConfirmTask, which takes a CPPNodeInfo object and a completion routine, and sets a flag to indicate whether the node still exists on the network. The unique parts of its public declaration are shown below:

class CPPConfirmTask : public CPPPeriodicTask {
public:
 CPPNodeInfo*NodeToConfirm;
 CPPConfirmTask (CPPTaskManager *TaskManager,
 long minPeriod = 120,
      Boolean deleteWhenDone = TRUE);
 ~CPPConfirmTask (void);
...
 BooleanNodeExists (void);
 void   StartNodeConfirm (CPPNodeInfo *TheNode,
   CompletionProc DoProc);
private:
...
};

The Three R’s and the Other Guy

When using the PPC Toolbox to exchange data, the strategy is to open a connection on your computer, wait for someone to start a session with you, perform all necessary reads and write, then close the session. We will, therefore, need four more tasks to let us exchange data over the network - ‘read’, ‘write’, and ‘respond to connection requests’. Oh yes, and ‘initiate connection requests’.

Responding

Listening for a connection is done by posting a PPCInform call; connections can be accepted or rejected based on authentication information which the caller provides. In the ACL, CPPMaBell’s OpenCommunicationPort is used to open a port on your machine which someone can connect to. A subclass of CPPPeriodicTask called CPPListenTask does the ‘waiting’ by posting and polling a PPCInform call. In its current form, it automatically accepts all connections which people try to form with it. If you wanted to provide authentication, you could simply subclass CPPListenTask, set it to not automatically accept, then have its DoCompletedAction method perform the needed verification. The unique parts of the declaration for CPPListenTask are shown below:

class CPPListenTask : public CPPPeriodicTask {
public:
 CPPListenTask (CPPTaskManager *TaskManager,
 long minPeriod = 120,
 Boolean deleteWhenDone = TRUE);
 ~CPPListenTask (void); 
...
 PPCSessRefNum GetNewSessionID (void);
 void   GetConnectedToInfo (Boolean *hasConnected,
 LocationNamePtr *Location,
 PPCPortPtr *PortRec);
...
 void   StartListenTask (PPCPortRefNum PortID, 
 CompletionProc DoProc);
protected:
 PPCSessRefNum   sessionID;
private:
...
};

You start the connection task by passing it the reference number of the port opened by CPPMaBell, and a completion routine. When the task completes, you can get the reference number for the session, and information about the person you are connected to using the GetNewSessionID and GetConnectedToInfo methods. The guts of StartListenTask are merely a modification of the code in IM:IAC, p. 11-36, and so are not repeated here.

Reading

Once a connection is open, a PPCRead task should be posted asynchronously to receive any data coming in on the connection. The ACL task which lets you do this is called CPPReadTask; the unique parts of its declaration are shown below.

class CPPReadTask : public CPPPeriodicTask {
public:
 CPPReadTask (CPPTaskManager *TaskManager,
  long minPeriod = 120,
  Boolean deleteWhenDone = TRUE);
 ~CPPReadTask (void);
...
 long   GetDataSize (void);
 Handle GetData (Boolean BecomeOwner, 
 Boolean *AmITheOwner);
 void   StartReadTask (PPCSessRefNum ConnectionID, 
  short blockSize,
    CompletionProc DoProc);
protected:
 PPCSessRefNum sessionID;
private:
...
};

You start a CPPReadTask by specifying a session number (obtained from CPPListenTask) and the size of the blocks you want to read. CPPReadTask starts out with an empty data handle, and a buffer of size blockSize in which data is temporarily stored. As data is sent over to it from the other end of the connection, it is accumulated into the data handle in blockSize sized chunks. The ‘optimal’ block size to use will depend heavily on the application; for transmitting typed messages, 128 bytes is probably fine; for doing file-transfers, you probably want to go to 1024 or 2048 bytes per block.

When the task is completed, GetDataSize tells you how much was read, and GetData returns a handle to the data and lets you establish ownership of it. The distinction of who owns the actual data handle becomes important when more than one object calls GetData on the same CPPReadTask. It is also important to determine whether the data will remain after the CPPReadTask is deleted. When you call GetData, you use BecomeOwner to tell it whether or not you will take responsibility for disposing of the handle. When you get the handle back, the AmITheOwner variable is set to indicate whether you succeeded in getting ownership of the handle, or whether someone else already owns it.

Note that a good time to post the first CPPReadTask on an open connection is in the completion task of the CPPListenTask which received the connection in the first place. I did not do this in CPPListenTask both to make it more flexible and to reduce the coupling between classes in the ACL, but the CPPYListenTask subclass used by the Yenta application uses this strategy most effectively.

Writing

On the other end of the connection from PCCInform and PPCRead, a PPCWrite task must be posted asynchronously to dump a chunk of data across the network. This duty is carried out by CPPWriteTask class (declaration below).

class CPPWriteTask : CPPPeriodicTask {
public:
 CPPWriteTask (CPPTaskManager *TaskManager, 
 long minPeriod = 120, 
 Boolean deleteWhenDone = TRUE);
 ~CPPWriteTask (void);
...
 void StartWriteTask (Ptr DataToWrite, Boolean OwnsData,
 PPCSessRefNum ConnectionID,
 CompletionProc DoProc,
 OSType DataType = "????",
 OSType DataCreator = "????");
 void   StartWriteTask (PPCPortRefNum SourcePortRefNum,
 CPPNodeInfo *SendTo,
 Ptr DataToWrite, Boolean OwnsData,
 CompletionProc DoProc,
 OSType DataType = "????",
 OSType DataCreator = "????");
private:
...
};

To provide flexibility, this task may be started in one of two ways. The first is to pass it a CPPNodeInfo object corresponding to the network entity you want to communicate with, the data, the ownership flag, and the block’s type and creator. In this case the write task synchronously establishes the session for you, making the assumption that the person you are trying to talk to has followed the naming conventions for ACL objects (see the description of CPPMaBell). A drawback of using synchronous connect is that you cannot connect to another port on your own computer.

The second way to start CPPWriteTask is to pass it the reference number of an established connection (which requires that you establish the connection yourself) along with the data you want to send, a boolean flag indicating whether it is allowed to dispose of the data on completion, and the type and creator of the data block.

“But how do I establish a connection?” Funny you should ask. The ACL provides a class which lets you asynchronously establish a connection with another network entity and return the reference number of the established connection. The declaration for this class (CPPConnectTask) is shown below.

class CPPConnectTask : CPPPeriodicTask {
public:
 CPPConnectTask (CPPTaskManager *TaskManager, 
     long minPeriod = 120, 
     Boolean deleteWhenDone = TRUE);
 ~CPPConnectTask (void);
...
 PPCSessRefNum GetSessionID (Boolean *isDone);
 void   StartConnectTask (
  PPCPortRefNum SourcePortRefNum,
      CPPNodeInfo *ConnectTo,
      CompletionProc DoProc);
protected:
 PPCSessRefNum sessionID;    
private:
...
};

As with the second StartWriteTask call, all you have to do is pass it the reference number of the port on your computer, and the network address of an entity which you wish to talk to and has followed the naming conventions for ACL network objects. When the task completes, you can use the GetSessionID call to get the reference number to pass to the second StartWriteTask call.

And Closing?

When you have finished reading and writing data across a session, you can call CPPMaBell::CloseSession to close the connection between the two computers. Something to be wary of when you are figuring out where to make the call to CloseSession, is that closing from the writer’s end can sometimes interrupt the read task. It is much better to have the end which does the reading close the session when it completes so that you can be sure that it actually receives all the data it was sent. Ideally CloseSession would be called by CPPReadTask’s completion method, which could then post another CPPListenTask (sort of like having CPPListenTask’s completion method posting a CPPYReadTask). The ACL’s CPPReadTask does not do this, since it would create restricting dependencies on the CPPReadTask and CPPListenTask, but you are encouraged to implement this behavior in a subclass of CPPReadTask.

That’s All Folks

Believe it or not, we’ve covered the entire Appletalk Class Library. As you’ve noticed, the only data transmission protocol I use is that provided by the PPC Toolbox. With the information provided by the CPPNodeInfo class, you should be able to subclass CPPPeriodicTask to provide support for ATP, DDP, ADSP, or any other protocol you care to use.

Yenta

Yenta, as I mentioned before, is an application built entirely in C++ which uses the ACL to do its communications, and a set of home-rolled TCL-like classes to provide the interface. The main window of the application is shown in Figure 1.

Figure 1. The Yenta “Send” Window

On the left is a list of all known zones. The magnifying glass button will re-load the list of zones which is automatically loaded when you run the application. Double-clicking on an item in the zone list will cause the program to scan that zone for other Yenta applications. If any are found, they are placed in the list on the right side, which tracks the names and addresses of all users who you know about on all of the zones listed on the left.

Near the bottom is a scrollable text area in which you can type up to 32K of text (can we say TEHandle? I knew you could). To send the text to another user(s), you can either double-click on a single name in the user list, or shift-click on several names and press the send button. If the ‘echo’ checkbox is on, your message is placed in a scrolling text-edit window (shown in figure 2) along with a log of all your incoming messages.

Figure 2. The Yenta “Message Log” Window

In case you leave your computer for any length of time, a feature called AutoReply in the file menu lets you type in a string which is echoed back to any other Yenta application which sends you a message when AutoReply is active.

Also available from the File menu is a Preferences dialog (shown in figure 3) which lets you give Yenta explicit instructions on how to maintain the user list. By default, users are added to the list when they send you messages or whenever you ask Yenta to scan a zone for new users. In the Preferences dialog you can specify a rate at which the application should 1) scan all known zones for new users, and 2) confirm each of the addresses in the right-hand list. Scan causes new users to be added to the list automatically, and confirm causes them to be removed if they are no longer visible on the network. You can also use the Preferences dialog to specify a sound to be played when messages are received (very useful!) and when a user logs on or logs off (marginally useful).

Figure 3. The Yenta “Preferences” Dialog

Requirements

Yenta is set to run in a 500 K partition, and should function properly on any Macintosh running System 7 with program linking turned on.

What’s Involved

Not much, actually. In order to provide the basic communications features of Yenta, I only had to subclass two classes in the ACL - CPPReadTask and CPPListenTask. Implementing the auto-scan/confirm features required the construction of five more descendants of CPPPeriodicTask. The interface required about 40 other classes, but I’m not going into those in detail here. The approach I will take in going over all this is to talk about the two new classes and how they work, then show how all of the basic communications classes are used in the application. After that I will discuss the periodic task classes which implement the scan and confirm features. Figure 4 presents an overview of the tasks used by the Yenta application, which may help you to sort out who is doing what to whom.

Figure 4. Yenta’s Task Generation Map

Customizing the ACL

Back when I was discussing listening, reading, and writing, I noted that it could be beneficial to create links between the CPPListenTask and CPPReadTask classes so that when a listen completed it would spawn a read, and vice versa. That is essentially what the two new classes do. As a result, the only method we override in each class is DoCompletedAction. Both overloaded methods are listed below.

void  CPPYListenTask::DoCompletedAction (void)
{
 CPPYReadTask  *DoRead = NULL;
 
 CPPListenTask::DoCompletedAction();
 if (this->callResult = noErr)
   {
   DoRead = new CPPYReadTask (this->ourManager, 15, TRUE);
   DoRead->StartReadTask(this->sessionID, 100, NULL);
   }
}

void  CPPYReadTask::DoCompletedAction (void)
{
 PPCEndPBRecEndRec;
 BooleanAmITheOwner;
 CPPYListenTask  *LTask = NULL;
 Handle TempHandle = this->GetData (FALSE, &AmITheOwner);
 
 CPPReadTask::DoCompletedAction();
 gTalkText->AppendItem(Hand2Ptr(TempHandle));
 
 EndRec.sessRefNum = this->sessionID; // close connection
 this->callResult = PPCEnd (&EndRec, FALSE);
 
   LTask = new CPPYListenTask (this->ourManager, 60, TRUE);
   LTask->StartListenTask(gOurPort, NULL);
}

The details of CPPYListenTask’s completion routine are fairly intuitive; if the listen task completes successfully it creates a new CPPYReadTask and starts it running on that session, using a period of 100 ticks.

The details of CPPYReadTask’s completion routine requires a bit more explaining, as it involves classes in other parts of the program. gTalkText is a queue which holds pointers to blocks of data - in this case messages received by the application. DoCompletedAction copies the received data into a pointer using Hand2Ptr, then adds it to the ‘received message’ queue. It then closes the connection and starts a new CPPYListenTask with a 60 tick period to wait for someone else to talk to us.

How It All Fits:

Listening and Reading

The nice thing about this tight link between the read and listen tasks is that once we have opened the communications port and posted the first listen task, everything else is done automatically; without our ever having to tell it to, the port is always engaged in reading or listening. The code which starts the whole thing off is part of the s method, and is shown below.

CPPMaBell *gMaBell;
CPPTaskManager *gSlaveDriver;
...
gMaBell = new CPPMaBell (TRUE);
if ((ErrCode = gMaBell->InitPPCToolbox()) != noErr)
  {
 ErrorAlert (ErrCode, NULL);
 ExitToShell();
  }
...
// create the lookup/read/write task manager
gSlaveDriver = new CPPTaskManager();
...
// open the port we will use to communicate through
if ((ErrCode = gMaBell->OpenCommunicationPort (ObjString, 
   gAppName, &gOurPort)) != noErr)
  {
 ErrorAlert (ErrCode, "\pCan't open a port to communicate with.");
 ExitToShell();
  }
...
// Set up a ConnectionTask to handle communications 
LTask = new CPPYListenTask(gSlaveDriver, 60, TRUE);
LTask->StartListenTask(gOurPort, NULL);

When the application is shutting down, we simply delete gSlaveDriver - which causes any outstanding tasks to be aborted and deleted, closes gOurPort, and deletes gMaBell to shut down Appletalk.

Writing

Writing data to other users is also a fairly simple process. All writing is done from the ‘Send’ window, so I created a method within it called SendToUser, the details of which are shown below.

void  CPPSendWindow::SendToUser(Ptr Text, Boolean ownsData, 
 CPPNodeInfo *SendTo)
{
 CPPWriteTask  *TheTask;
 
    // have the write task open the connection
 TheTask = new CPPWriteTask (gSlaveDriver, 25, TRUE);
 TheTask->StartWriteTask (gOurPort, SendTo, Text, ownsData, 
 NULL, (OSType)'TEXT', (OSType)'YntA');      
}

SendToUser simply creates a CPPWriteTask and uses the ‘automatic connect’ version of StartWriteTask to send the data to the specified user.

SendToUser is used by two other methods within CPPSendWindow - the method which responds to the user pressing the send button, and the method which sends the AutoReply string back to someone who sends us a message. Looking at these two methods will complete our coverage of how writing is done within Yenta. Let’s look at the AutoReply method first:

void  CPPSendWindow::GotNewMessage (CPPNodeInfo *FromWhom)
{
 Ptr    TempPtr, OurIDStream;
 OSErr  ErrCode;

    // send autoreply message if feature is on
   if (gReplyString)
     {      
    // Create a string with our name and address in it
 OurIDStream = gOurIdentity->InfoToStream();
 
    // create a pointer to hold our address & the reply string
 TempPtr = NewPtr (GetPtrSize(OurIDStream) 
 + gReplyString[0]);
 if ((ErrCode = MemError()) == noErr)
   {      
   BlockMove (OurIDStream, TempPtr,GetPtrSize(OurIDStream));
   BlockMove (gReplyString+1,
 TempPtr+GetPtrSize(OurIDStream),
   *gReplyString);
 DisposPtr (OurIDStream);
 
    // send the autoreply to the user who sent us the message
 SendToUser (TempPtr, TRUE, FromWhom);
     }
   }
   
    // Add the user to the list
 if (this->UserList->AddNewUser(FromWhom))
   if (gPrefsInfo.playLogon)
     gLogonSound->PlaySound(TRUE);
}

This method is called every time a message is taken from the message queue. The first part checks a StringPtr called gReplyString; if AutoReply is turned on, gReplyString points to the AutoReply message, otherwise it is NULL. Because the standard format for Yenta messages requires that the address of the sender be included with the message, we first use CPPNodeInfo’s InfoToStream method to convert our address to a pointer, copy the string and the address into another pointer, then ask SendToUser to deliver the message. Note that the write task is given the responsibility for deleting the pointer when it completes.

The method which responds to the user pressing the send button is named (rather predictably) DoSendButton. The first part of the method collects information about the text to be sent, then constructs the message, storing it in a variable called TempPtr. It also stores the total number of selected users in the variable NumToSendTo, then enters the following loop:

 
    // send text to each hilighted user
 while (this->UserList->NextSelectedCell(&whichUser))
   {
   UserData = (CPPNodeInfo *)((*this->UserList)[whichUser]);
   if (UserData && !(UserData->Equals(gOurIdentity)))
     SendToUser (TempPtr, NumToSendTo == 1, UserData);
   }

Each selected user’s CPPNodeInfo object is extracted from the user list, and the data sent to each of them using the SendToUser method. Note that the write task is only given permission to dispose of the data if there is one user selected.

When you send to more than one user, the matter of disposing of the write data becomes a bit more complicated, because there are no hard-and-fast rules for determining which write task should actually be given permission to dispose of the data. You can’t give permission to any one task in particular, since you have no guarantee that any particular task will complete after all of the others. Similarly, you can’t let the application dispose of the memory directly after the loop, since not all the tasks may have completed by then. You could create a copy of the data for each user and give each task ownership of its copy, but consider the problem of sending 10K of data to 50 users; you tend to run out of memory rather quickly.

The solution which I came up with was to create a subclass of CPPPeriodicTask which could hold on to the data pointer, and wait for all of the write tasks to complete before disposing of it. This class, called CPPWatchWriteTasks, accomplishes this task by calling its PTM’s HowManyTasksOfType method with the name “CPPWriteTask” until the count reaches zero, then completing and deleting the pointer it was given. The following fragment comes directly after the ‘send’ loop shown above:

    // keep track of 'write' data until all tasks complete
 if (NumToSendTo != 1)
 {
   WatchTask = new CPPWatchWriteTasks (gSlaveDriver, 60);
   WatchTask->StartWatchTask(TempPtr);
 }

The Final Pieces

The last two features to discuss are the program’s ability to automatically scan known zones for new users and to confirm the presence of known users. Each of these features required the creation of two subclasses of CPPPeriodicTask: one which does the actual work, and the other which simply triggers it every n minutes. Let’s look at the details of the trigger classes first.

There are two trigger classes - CPPSpawnZoneTask and CPPSpawnConfirmTask. Each class has three elements in common: 1) a private variable of the type task it triggers (CPPScanZones or CPPConfirmUsers respectively), 2) a Start??? method which initializes the private variable and enqueues the task, and 3) a DoPeriodicTask method which, when called, calls Start??? on that private variable. Below is the code for one of their DoPeriodicTask methods; the other is identical in style.

void  CPPSpawnZoneTask::DoPeriodicAction (void)
/* if the Scan task has completed, ask it to scan again; */
{
    // call the inherited method to update frequency count
 CPPPeriodicTask::DoPeriodicAction();
 if (scanTask->hasCompleted)
   scanTask->StartScanZones (NULL);
}

A key feature of both trigger tasks is that they never complete. (What, never? No, never!) Both of them remain in the queue until they are either explicitly removed or until the application shuts down. Boring, but useful. Time to move on to the gruntwork classes - CPPScanZones and CPPConfirmUsers.

CPPScanZones

CPPScanZones is a fairly unassuming descendant of CPPPeriodicTask. It has a single unique method - StartScanZones, and three private variables, shown below:

class CPPScanZones : public CPPPeriodicTask {
public:
...
 void   StartScanZones (CompletionProc DoProc);
private:
 CPPStringList *zoneList;
 CPPNode411 *lookupTask;
 long   whichZone; 
};

When StartScanZones is called, it copies the list of zones in the Send window into the zoneList variable, allocates the lookupTask object, and sets whichZone to 1. Its DoPeriodicTask method looks like this:

void  CPPScanZones::DoPeriodicAction (void)
/* if the lookup has completed, advance to the next zone and start another lookup */
{
 StringPtrZoneName;
 Str32  TypeStr;
 Str255 STemp;
 
    // call the inherited method to update frequency count
 CPPPeriodicTask::DoPeriodicAction();

 if (lookupTask->hasCompleted)
   {
   this->whichZone++;
   if (this->whichZone <= this->zoneList->GetNumItems())
     {
   ZoneName = (*zoneList)[this->whichZone];
 PStrCat (32, TypeStr, 2, "\p •", gAppName);
 lookupTask->StartNodeLookup("\p=", TypeStr, 
 ZoneName, 50, ProcessNodeLookupResults);
 PStrCat (255, STemp, 3, "\pScanning zone '", 
 ZoneName, "\p' for new users.");
      SetStatusMessage (STemp, TRUE);
     }
   else
     {
      this->hasCompleted = TRUE;
      this->callResult = noErr;
     }
   }
}

At each iteration, it gets the name of the next zone in the list and starts the CPPNode411 task looking for all objects in that zone which match the ACL naming convention, using the application name as the PortType (see the discussion of CPPMaBell’s OpenCommunicationPort method). The global routine ProcessNodeLookupResults takes each node in the ‘found’ list and passes it to the user list, which then determines whether the node is already in the list or not.

CPPConfirmUsers

CPPConfirmUsers is also a fairly unassuming task, with a declaration similar to that of CPPScanZones:

class CPPConfirmUsers : public CPPPeriodicTask {
public:
...
 void   StartConfirmUsers (CompletionProc DoProc);
private:
 CPPObjectList   *nodeList;
 CPPConfirmTask  *confirmTask;
 long   whichNode;
};

StartConfirmUsers copies the list of known addresses from the Send window into the nodeList variable, allocates the confirmTask object, and sets whichNode to 1. It’s DoPeriodicTask method follows:

void  CPPConfirmUsers::DoPeriodicAction (void)
{
 CPPNodeInfo*TheNode = NULL;
 Str255 STemp;
 StringPtrUsersName;
 
 CPPPeriodicTask::DoPeriodicAction();

 if (confirmTask->hasCompleted)
   {
   this->whichNode++;
   if (this->whichNode <= gUserList->GetNumItems())
     {
      TheNode = (CPPNodeInfo *)((*gUserList)[whichNode]);
      
                  // tell the user we are confirming this connection
      UsersName = ShortName (TheNode);
 PStrCat (255, STemp, 2, "\pSearching for ", UsersName);
      SetStatusMessage (STemp, TRUE);
      NukePtr(UsersName);
      
 confirmTask->StartNodeConfirm(TheNode,
  ConfirmCompletionProc);
     }
   else
     {
      this->hasCompleted = TRUE;
      this->callResult = noErr;
     }
   }
}

In a manner similar to CPPScanZones, it iterates through its list, extracting each user address in turn and activating the confirm task. Note that it assigns a completion routine to CPPConfirmTask. This is done primarily so that we don’t have to create a specific subclass of CPPConfirmTask. The completion routine simply asks the user list to delete the specified user if the NodeExists method returns FALSE (indicating that the confirm task failed to find the user on the network).

In Conclusion

Believe it or not, we’re done. As promised, I haven’t discussed any of the interface classes/application framework which I used to put Yenta together. If you are comfortable with the TCL, you probably needn’t bother, unless you want to work entirely in C++. If, like me, you find the TCL a bit baroque and unintuitive, you might want to look over the classes I’ve constructed. They don’t provide as many fancy features as the TCL (embedded scrolling panes, etc.) but most of the classes map pretty directly onto the Macintosh user interface, which I think makes it easier to use. If there is enough interest, I may discuss parts of it at a later date. If anyone finds any bugs (bound to be in there, somewhere!), or comes up with any neat classes which add functionality to either the ACL or my interface class library, please snail/e-mail me and let me know. Feel free to modify, subclass, and experiment like mad. Good hacking to you all!

References

Books

Inside AppleTalk, Second Edition. Addison-Wesley Publishing Company. Good overview of network topology and communication protocols.

Pierce, Michael. Programming with Appletalk. Addison-Wesley Publishing Company. Great overview of the nitty-gritty details; lots of ‘how to’ code.

Technical Manuals

Inside Macintosh, Vol II, chapter 10

Inside Macintosh, Vol V, chapter 28

Inside Macintosh, Vol VI, chapters 7 and 32

Inside Macintosh:Interapplication Communication, chapter 11

Software (available from Apple DTS)

Network Watch (DMZ)

Neighborhood Watch

SC011.GetZoneList

 
AAPL
$98.77
Apple Inc.
-0.26
MSFT
$44.00
Microsoft Corpora
+0.03
GOOG
$589.11
Google Inc.
-1.49

MacTech Search:
Community Search:

Software Updates via MacUpdate

OS X Yosemite Wallpaper 1.0 - Desktop im...
OS X Yosemite Wallpaper is the gorgeous new background image for Apple's upcoming OS X 10.10 Yosemite. This wallpaper is available for all screen resolutions with a source file that measures 5,418... Read more
Acorn 4.4 - Bitmap image editor. (Demo)
Acorn is a new image editor built with one goal in mind - simplicity. Fast, easy, and fluid, Acorn provides the options you'll need without any overhead. Acorn feels right, and won't drain your bank... Read more
Bartender 1.2.20 - Organize your menu ba...
Bartender lets you organize your menu bar apps. Features: Lets you tidy your menu bar apps how you want. See your menu bar apps when you want. Hide the apps you need to run, but do not need to... Read more
TotalFinder 1.6.2 - Adds tabs, hotkeys,...
TotalFinder is a universally acclaimed navigational companion for your Mac. Enhance your Mac's Finder with features so smart and convenient, you won't believe you ever lived without them. Tab-based... Read more
Vienna 3.0.0 RC 2 :be5265e: - RSS and At...
Vienna is a freeware and Open-Source RSS/Atom newsreader with article storage and management via a SQLite database, written in Objective-C and Cocoa, for the OS X operating system. It provides... Read more
VLC Media Player 2.1.5 - Popular multime...
VLC Media Player is a highly portable multimedia player for various audio and video formats (MPEG-1, MPEG-2, MPEG-4, DivX, MP3, OGG, ...) as well as DVDs, VCDs, and various streaming protocols. It... Read more
Default Folder X 4.6.7 - Enhances Open a...
Default Folder X attaches a toolbar to the right side of the Open and Save dialogs in any OS X-native application. The toolbar gives you fast access to various folders and commands. You just click... Read more
TinkerTool 5.3 - Expanded preference set...
TinkerTool is an application that gives you access to additional preference settings Apple has built into Mac OS X. This allows to activate hidden features in the operating system and in some of the... Read more
Audio Hijack Pro 2.11.0 - Record and enh...
Audio Hijack Pro drastically changes the way you use audio on your computer, giving you the freedom to listen to audio when you want and how you want. Record and enhance any audio with Audio Hijack... Read more
Intermission 1.1.1 - Pause and rewind li...
Intermission allows you to pause and rewind live audio from any application on your Mac. Intermission will buffer up to 3 hours of audio, allowing users to skip through any assortment of audio... Read more

Latest Forum Discussions

See All

Puzzix Review
Puzzix Review By Jennifer Allen on July 29th, 2014 Our Rating: :: NICE IDEAUniversal App - Designed for iPhone and iPad A little like Tetris, Puzzix is all about piecing together blocks and watching them vanish. It could do with... | Read more »
Cannonball eMail is Now Live – Works Wit...
Cannonball eMail is Now Live – Works With Gmail, Yahoo, Outlook, Hotmail, and AOL Posted by Jessica Fisher on July 29th, 2014 [ permalink ] | Read more »
To The End Review
To The End Review By Lee Hamlet on July 29th, 2014 Our Rating: :: A VICIOUS CYCLEUniversal App - Designed for iPhone and iPad To The End will test players’ patience, timing, and dedication as they try to navigate all 13 levels in... | Read more »
Kairobotica (Games)
Kairobotica 1.0.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0.0 (iTunes) Description: In a galaxy not so far away, miscreants and monsters are wreaking havoc, and it's up to everyone's favorite mechanical mascot... | Read more »
Traps n’ Gemstones Review
Traps n’ Gemstones Review By Campbell Bird on July 28th, 2014 Our Rating: :: CASTLEVANIA JONESUniversal App - Designed for iPhone and iPad Fight mummies, dig tunnels, and ride a runaway minecart to discover ancient secrets in this... | Read more »
The Phantom PI Mission Apparition Review
The Phantom PI Mission Apparition Review By Jordan Minor on July 28th, 2014 Our Rating: :: GHOSTS BUSTEDUniversal App - Designed for iPhone and iPad The Phantom PI is an exceedingly clever and well-crafted adventure game.   | Read more »
More Stubies Are Coming Your Way in a Ne...
More Stubies Are Coming Your Way in a New Update Posted by Jessica Fisher on July 28th, 2014 [ permalink ] Universal App - Designed for iPhone and iPad | Read more »
The Great Prank War Review
The Great Prank War Review By Nadia Oxford on July 28th, 2014 Our Rating: :: PRANKING IS SERIOUS BUSINESSUniversal App - Designed for iPhone and iPad Though short, The Great Prank War offers an interesting and fun mix of action and... | Read more »
Marvel Contest of Champions Announced at...
Marvel Contest of Champions Announced at Comic-Con Posted by Jennifer Allen on July 28th, 2014 [ permalink ] Announced over the weekend at San Diego Comic-Con was the fairly exciting looking Marvel Contest of Champions. | Read more »
Teenage Mutant Ninja Turtles Review
Teenage Mutant Ninja Turtles Review By Jennifer Allen on July 28th, 2014 Our Rating: :: DULL SWIPINGUniversal App - Designed for iPhone and iPad The pizza power is weak when it comes to this Teenage Mutant Ninja Turtles game.   | Read more »

Price Scanner via MacPrices.net

Updated MacBook Pro Price Trackers
We’ve updated our MacBook Pro Price Trackers with the latest information on prices, bundles, and availability on the new 2014 models from Apple’s authorized internet/catalog resellers as well as... Read more
Apple updates MacBook Pros with slightly fast...
Apple updated 13″ and 15″ Retina MacBook Pros today with slightly faster Haswell processors. 13″ models now ship with 8GB of RAM standard, while 15″ MacBook Pros ship with 16GB across the board. Most... Read more
Apple drops price on 13″ 2.5GHz MacBook Pro b...
The Apple Store has dropped their price for the 13″ 2.5GHz MacBook Pro by $100 to $1099 including free shipping. Read more
Apple drops prices on refurbished 2013 MacBoo...
The Apple Store has dropped prices on Apple Certified Refurbished 13″ and 15″ 2013 MacBook Pros, with model now available starting at $929. Apple’s one-year warranty is standard, and shipping is free... Read more
iOS 8 and OS X 10.10 To Support DuckDuckGo As...
Writing for Quartz, Dan Frommer reports that Apple’s forthcoming iOS 8 and OS X 10.10 operating systems version updates will allow users to select DuckDuckGo as their default search engine. He notes... Read more
U.K. Hospital Using iPods and iPads To Record...
British news journal GazetteLive’s. Ian McNeal notes that the old “an apple a day keeps the doctor away” proverb is being turned on its head at http://southtees.nhs.uk/hospitals/james-cook/ James... Read more
13-inch 2.5GHz MacBook Pro on sale for $1099,...
Best Buy has the 13″ 2.5GHz MacBook Pro available for $1099.99 on their online store. Choose free shipping or free instant local store pickup (if available). Their price is $100 off MSRP. Price is... Read more
Roundup of Apple refurbished MacBook Pros, th...
The Apple Store has Apple Certified Refurbished 13″ and 15″ MacBook Pros available for up to $400 off the cost of new models. Apple’s one-year warranty is standard, and shipping is free. Their prices... Read more
Record Mac Shipments In Q2/14 Confound Analys...
A Seeking Alpha Trefis commentary notes that Apple’s fiscal Q3 2014 results released July 22, beat market predictions on earnings, although revenues were slightly lower than anticipated. Apple’s Mac’... Read more
Intel To Launch Core M Silicon For Use In Not...
Digitimes’ Monica Chen and Joseph Tsai, report that Intel will launch 14nm-based Core M series processors specifically for use in fanless notebook/tablet 2-in-1 models in Q4 2014, with many models to... Read more

Jobs Board

Sr Software Lead Engineer, *Apple* Online S...
Sr Software Lead Engineer, Apple Online Store Publishing Systems Keywords: Company: Apple Job Code: E3PCAK8MgYYkw Location (City or ZIP): Santa Clara Status: Full Read more
*Apple* Solutions Consultant (ASC) - Apple (...
**Job Summary** The ASC is an Apple employee who serves as an Apple brand ambassador and influencer in a Reseller's store. The ASC's role is to grow Apple Read more
Sr. Product Leader, *Apple* Store Apps - Ap...
**Job Summary** Imagine what you could do here. At Apple , great ideas have a way of becoming great products, services, and customer experiences very quickly. Bring Read more
*Apple* Solutions Consultant (ASC) - Apple (...
**Job Summary** The ASC is an Apple employee who serves as an Apple brand ambassador and influencer in a Reseller's store. The ASC's role is to grow Apple Read more
*Apple* Solutions Consultant (ASC) - Apple (...
**Job Summary** The ASC is an Apple employee who serves as an Apple brand ambassador and influencer in a Reseller's store. The ASC's role is to grow Apple Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.