
Open Directory allows Mac OS X applications to locate and update information
about users, machines, and resources on a network. Applications that make use of
this directory service may provide transparent access to resources with a
minimum of user intervention. For example, your application may use Open
Directory in order to participate in a network-wide sign-on mechanism shared
with Windows and UNIX users. Or your application may allow users to update their
personal information in a company-wide directory of employee data. Adding Open
Directory support to your application can create a lot of opportunities for you
in the enterprise, and even in smaller network environments.
This article discusses the
implementation of an Open Directory plug-in, which responds to requests for
information about resources. This article also discusses an Open Directory
client application that calls the plug-in. Note: you should have a second boot disk or CD available while testing. Since
an Open Directory plug-in loads during system startup, any errors encountered
may prevent the machine from fully booting, in which case you may not be able to
login and remove the plug-in. Booting from another disk will allow you to remove
the plug-in and then restart.
What Is Open Directory?
Originally NetInfo, a NeXT technology, provided the implementation for
directory services on Mac OS X v10.0. NetInfo is still used for looking up
directory information on a local machine, but other cross platform technologies
have taken precedence in Apple's network directory system. Open Directory
supports Lightweight Directory Access protocol (LDAP) versions 2 and 3, Apple's
Bonjour protocol, and Microsoft's Active Directory, which allows Mac OS X
machines to fit into a Microsoft network environment. Other supported legacy
protocols include AppleTalk, BSD flat files, NIS, and the Service Location
Protocol (SLP).
In this article you will see references to both Directory Services and Open
Directory. While Open Directory provides the architecture for the directory
system, Directory Services is the name of the physical framework
(DirectoryService.framework) provided by Apple. Plus, the sample code uses a
"DS" prefix for projects, files, and classes.
Plug-in Functions
Each protocol supported by Open Directory is implemented as a Core Foundation
plug-in (type CFPlugIn). Three groups of functions are needed for a
plug-in: those declared by the IUnknown interface, the Open
Directory plug-in entry points, and the Open Directory callbacks. Apple provides
the DSPlugIn Sample Code as part of the Directory Services SDK that
includes an Xcode project and source files. The discussion of files in this
section refers to the files in the C version of that project. You may elect to
use the C++ version instead. The concepts discussed below remain the same.
The first set of functions is declared by the IUnknown
interface. IUnknown is part of Microsoft's Component Object Model
(COM). CFPlugin uses COM as the basis for function discovery and
dispatch. IUnknown serves as the root interface for
CFBundle's discovery and dispatching mechanism.
IUnknown does not actually refer to functions that are unknown;
instead it is used to perform a lookup of named functions in objects. (The book
Inside COM, listed in the Additional Information section below, will
tell you much more about COM internals.) IUnknown declares the
functions QueryInterface, AddRef, and
Release. An IUnknown implementation, typically defined
in a structure, will include a reserved pointer (for use by the operating
system) at the beginning followed by the three functions.
ServerModule.c contains implementations of these functions, though
with slightly different names (such as _COMQueryInterface). The
names do not have to match because function dispatching or calling is handled
through a function table. This table contains an array of pointers to function
implementations (addresses). When a function gets called by name, that name is
first translated to an index in the array, and the corresponding function
pointer or address is then invoked.
The Open Directory plug-in interface declares the second set of functions,
the entry points, or functions in your plug-in that may be called by Open
Directory: Validate, Initialize,
ProcessRequest, SetPlugInState,
PeriodicTask, Shutdown, and Configure.
These will be invoked to handle plug-in startup, execution of requests, and
shutdown. The document Open Directory Plug-ins contains the API reference
information.
The third set of functions consists of the Open Directory callbacks. Your
plug-in calls these in order to interact with Open Directory. The callbacks
include: DSRegisterNode, DSUnregisterNode, and
DSDebugLog. The first two allow your plug-in to add a node to or
remove a node from the set of global Open Directory entries. The last provides a
way to add a message to the Open Directory log file, which may be viewed using
Console.app. These callbacks are declared in ServerModuleLib.h and
implemented in SerModuleLib.c.
The function implementations dispatch through to the corresponding function
table implementations. The callbacks are located near the end of the function
table, following the IUnknown functions and the plug-in entry
points. Listing 1 contains the function table declaration from
ServerModule.h in the sample code. The macro
IUNKNOWN_C_GUTS expands to the three function declarations from
IUnknown.
Listing 1: The Function Table Declaration in ServerModule.h
//-----------------------------------------------------------------------------
// * Plugin Module Function Table representation for CFPlugin
//-----------------------------------------------------------------------------
// Function table for the com.apple.DSServer.ModuleInterface
typedef struct tagModuleInterfaceFtbl
{
/**** Required COM header info. ****/
IUNKNOWN_C_GUTS;
/**** Instance methods. ****/
long (*validate) ( void *thisp, const char *inVersionStr,
const unsigned long inSignature );
long (*initialize) ( void *thisp );
long (*configure) ( void *thisp );
long (*processRequest) ( void *thisp, void *inData );
long (*setPluginState) ( void *thisp, const unsigned long inState );
long (*periodicTask) ( void *thisp );
long (*shutdown) ( void *thisp );
void (*linkLibFtbl) ( void *thisp, SvrLibFtbl *inLinkBack );
unsigned long mRefCount;
} ModuleFtbl;
The Plug-in Shell
Apple provides sample projects in the Directory Services SDK that
simplifies development for both Open Directory plug-ins and clients. Figure 1
shows the plug-in shell Xcode project.
Figure 1: DSPlugIn Xcode Project
The recommended deployment directory for your plug-in is
/Library/DirectoryServices/PlugIns/. After you build the
DSPlugIn target, drag or copy the updated
DSPlugIn.dsplug bundle from the project's /build
folder to /Library/DirectoryServices/PlugIns/. You can test
subsequent plug-in changes by deleting the existing
/Library/DirectoryServices/PlugIns/DSPlugIn.dsplug, copying in the
new bundle, then stopping the DirectoryService daemon using Activity Monitor.
The next invocation of the client application (the sample project named
DSTestTool) will restart the daemon, which will load the updated plug-in bundle.
This sequence is easier than rebooting your machine each time you need to test
the plug-in.
PlugInShell.c contains the plug-in entry points:
Validate, Initialize, ProcessRequest,
SetPlugInState, PeriodicTask, Shutdown,
and Configure. This file is where your plug-in functionality goes.
The other files provide support for interacting with the CFPlugIn
architecture.
You should leave most of the property list entries as-is, though you may want
to rev the CFBundleVersion as you add functionality.
Listing 2 includes the syslog header file, and declares several variables at
the top of PlugInShell.c. These are used to keep track of the
plug-in state, node name, and so on. They are referenced in the other plug-in
functions discussed here.
Listing 2: Additional Include File and Global Variables Declared in PlugInShell.c
#include <syslog.h>
static ePluginState gPluginState = kUnknownState;
static unsigned long gSignature = 0;
static tDataListPtr gNodeList = NULL;
static tDirReference gDSRef = NULL;
const char * nodePath = "/DSPlugIn/local";
Listing 3 shows the plug-in's Initialize function. It calls CreateNode and AddTestRecords. Note the inclusion of syslog messages; this will be discussed in more detail in the section on debug messages.
Listing 3: The Initialize Function
long PlugInShell_Initialize ( void )
{
LogIt( 0x0F, "DSPlugInStub %s Initialize Called %s\n", "method", "now" );
syslog( LOG_INFO, "PlugInShell_Initialize" );
// Need to register nodes in some loop or other logic
AddTestRecords();
long status = CreateNode();
return( eDSNoErr );
} // Initialize
Listing 4 shows the structure, global variables, and function supporting
simple test records for this plug-in. This makes it easy to test interaction
with the client application later.
Listing 4: Creating Sample Records
typedef struct {
char recordName[ 32 ];
char attrName[ 32 ];
char attrValue[ 32 ];
} DSPlugInRecordStructure;
static DSPlugInRecordStructure gRecords[ 10 ];
static unsigned long gRecordCount = 0;
void AddTestRecords ( void )
{
gRecordCount = 0;
strcpy( gRecords[ gRecordCount ].recordName, "test" );
strcpy( gRecords[ gRecordCount ].attrName, "message" );
strcpy( gRecords[ gRecordCount ].attrValue, "Hello, world." );
gRecordCount++;
strcpy( gRecords[ gRecordCount ].recordName, "another record" );
strcpy( gRecords[ gRecordCount ].attrName, "message" );
strcpy( gRecords[ gRecordCount ].attrValue, "Hello, world (again)." );
gRecordCount++;
syslog( LOG_INFO, "PlugInShell.AddTestRecord" );
}
Initialize also handles node registration, discussed next.
Adding and Removing a Node
Clients lookup services by node, rather than by requesting a specific
plug-in. Listing 5 illustrates how to add or register a node. Once the node has
been registered, clients that know to look for it (by name) can start making
requests. Open Directory will locate the appropriate plug-in based on the
requested node; clients do not need to know the name of the plug-in.
Listing 5: Adding a Node
long CreateNode ( void )
{
long status = eDSNoErr;
syslog( LOG_INFO, "PlugInShell.CreateNode" );
if ( gDSRef == NULL )
status = dsOpenDirService( &gDSRef );
syslog( LOG_INFO, "PlugInShell.CreateNode: dsOpenDirService returned %ld", status );
if ( gDSRef != NULL )
{
syslog( LOG_INFO, "PlugInShell.CreateNode: gDSRef != NULL" );
if ( gNodeList == NULL )
{
gNodeList = dsDataListAllocate( gDSRef );
status = dsBuildListFromPathAlloc ( gDSRef, gNodeList, nodePath, "/" );
syslog( LOG_INFO, "PlugInShell.CreateNode: gNodeList = %p", gNodeList );
syslog( LOG_INFO, "PlugInShell.CreateNode: dsBuildListFromPathAlloc returned = %d", status );
if ( gNodeList != NULL )
{
status = DSRegisterNode( gSignature, gNodeList, kDirNodeType );
syslog( LOG_INFO, "PlugInShell.CreateNode: DSRegisterNode returned %ld", status );
}
else
{
syslog( LOG_INFO, "\t*** PlugInShell.CreateNode: gNodeList == NULL" );
}
}
}
return status;
}
Listing 6 illustrates node removal. This does not unload the plug-in, it
simply makes the node inaccessible to clients. This function is called from
Shutdown.
Listing 6: Removing a Node
long RemoveNode ( void )
{
long status = eDSNoErr;
syslog( LOG_INFO, "PlugInShell.RemoveNode" );
if ( gNodeList != NULL )
{
status = DSUnregisterNode( gSignature, gNodeList );
}
return status;
}
Processing Requests
Plug-ins must respond to a variety of requests, as outlined in the Open
Directory Plug-Ins manual. These include dsOpenDirNode,
dsCreateRecord, and so on. The plug-in entry point for these
requests is the ProcessRequest function. The client invokes a
particular function, and the parameters provided by the caller are packaged into
an appropriate structure type. The sHeader type, shown in Listing
7, contains two fields that are common to the other structure types. This allows
the plug-in to cast the incoming data to an sHeader, determine the
requested operation using the fType field, and then dispatch to an
appropriate block of code to handle the request. Inside the handler block, the
plug-in casts the incoming data to a structure type that is specific to that
operation. For a dsGetRecordList request, the appropriate type is
sGetRecordList.
Listing 7: The Generic Header and Record List Structures
typedef struct {
unsigned long fType;
long fResult;
} sHeader;
typedef struct {
unsigned long fType;
long fResult;
tDirNodeReference fInNodeRef;
tDataBufferPtr fInDataBuff;
tDataListPtr fInRecNameList;
tDirPatternMatch fInPatternMatch;
tDataListPtr fInRecTypeList;
tDataListPtr fInAttribTypeList;
dsBool fInAttribInfoOnly;
unsigned long fOutRecEntryCount;
tContextData fIOContinueData;
} sGetRecordList;
Once the plug-in casts the function parameter to an
sGetRecordList pointer, it fills-in the response field,
fInDataBuff. Listing 8 implements dsGetRecordList.
Note that this plug-in ignores the Boolean field fInAttribInfoOnly,
which specifies whether the plug-in should return attribute names and values (a
value of false) or names only (true). This plug-in
returns both names and attributes.
The client is responsible for allocating memory for several of the fields in
the sRecordList, while the plug-in allocates others. In either
case, deallocation is the client's responsibility. Check the API documentation
for details.
The record list format may be of two predefined types, StdA or
StdB, or you may define your own type. This example uses
StdA. The formats are defined in the Open
Directory Plug-ins manual. The advantage of using one of the predefined types is
that the system will automatically attempt to interpret the record list: a
request for a specific record out of the tDataBufferPtr will not
call your plug-in to parse the record list. This is an example of Client Side
Buffer Parsing, which significantly improves performance. If you use a custom
format for the returned data, the message traffic between the client and your
plug-in increases, and your plug-in must respond to additional operations, such
as dsGetRecordEntry.
Listing 8: Building the Record List in ProcessRequest
long PlugInShell_ProcessRequest ( void *inData )
{
long siresult = 0;
sHeader *pMsgHdr = nil;
tDataBufferPtr theDataBufferPtr = nil;
unsigned long offset = 0;
syslog( LOG_INFO, "PlugInShell_ProcessRequest" );
if ( inData == nil )
{
return( -4460 );
}
pMsgHdr = ( sHeader * )inData;
syslog( LOG_INFO, "PlugInShell_ProcessRequest: pMsgHdr->fType = %d", pMsgHdr->fType );
switch ( pMsgHdr->fType )
{
// snip...
case kGetRecordList:
syslog( LOG_INFO, "PlugInShell_ProcessRequest: kGetRecordList" );
sGetRecordList *getRecordListData = ( sGetRecordList * )inData;
theDataBufferPtr = ( tDataBufferPtr )getRecordListData->fInDataBuff;
// Used to memcpy numeric values.
unsigned long scratch = 0;
unsigned short shortScratch = 0;
// Current location in buffer.
offset = 0;
// General scheme for building the buffer, repeated continuously:
// 1. Write next value.
// a. Use memcpy for ulong or ushort values.
// b. Use strcpy for strings. Add 1 to all strlen results for terminator.
// 2. Update the buffer location.
scratch = 'StdA';
memcpy( theDataBufferPtr->fBufferData + offset, &scratch, sizeof( scratch ) );
offset += 4;
scratch = gRecordCount;
memcpy( theDataBufferPtr->fBufferData + offset, &scratch, sizeof( scratch ) );
offset += 4;
// Skip past record offset values for now.
// Fill them in as each record gets processed.
offset += ( 4 * gRecordCount );
scratch = 'EndT';
memcpy( theDataBufferPtr->fBufferData + offset, &scratch, sizeof( scratch ) );
offset += 4;
// Count down. Greatest index will be first after the StdA data block.
unsigned long i = gRecordCount - 1;
unsigned long recordOffset = offset;
while ( i >= 0 )
{
// Set length of record block.
// Use a unique type because this is a non-standard record format.
// Note addition of 1 for each strlen call.
// The 10 is the number of bytes needed for the fixed-length fields.
scratch = strlen( "dsRecTypeNative:MyType" ) + 1 +
strlen( gRecords[ i ].recordName ) + 1 + 10;
memcpy( theDataBufferPtr->fBufferData + offset, &scratch, sizeof( scratch ) );
offset += 4;
// Set record type length.
shortScratch = strlen( "dsRecTypeNative:MyType" ) + 1;
memcpy( theDataBufferPtr->fBufferData + offset, &shortScratch, sizeof( shortScratch ) );
offset += 2;
// Set record type.
strcpy( theDataBufferPtr->fBufferData + offset, "dsRecTypeNative:MyType" );
offset += strlen( "dsRecTypeNative:MyType" ) + 1;
// Set record name length.
shortScratch = strlen( gRecords[ i ].recordName ) + 1;
memcpy( theDataBufferPtr->fBufferData + offset, &shortScratch, sizeof( shortScratch ) );
offset += 2;
// Set record name.
strcpy( theDataBufferPtr->fBufferData + offset, gRecords[ i ].recordName );
offset += strlen( gRecords[ i ].recordName ) + 1;
// Set number of attributes.
shortScratch = 1;
memcpy( theDataBufferPtr->fBufferData + offset, &shortScratch, sizeof( shortScratch ) );
offset += 2;
// Set attribute length: name plus value lengths, plus size of fixed-length fields.
scratch = strlen( gRecords[ i ].attrName ) + 1 + strlen( gRecords[ i ].attrValue ) + 1 + 8;
memcpy( theDataBufferPtr->fBufferData + offset, &scratch, sizeof( scratch ) );
offset += 4;
// Set attribute name length.
shortScratch = strlen( gRecords[ i ].attrName ) + 1;
memcpy( theDataBufferPtr->fBufferData + offset, &shortScratch, sizeof( shortScratch ) );
offset += 2;
// Set attribute name.
strcpy( theDataBufferPtr->fBufferData + offset, gRecords[ i ].attrName );
offset += strlen( gRecords[ i ].attrName ) + 1;
// Set number of attribute values.
shortScratch = 1;
memcpy( theDataBufferPtr->fBufferData + offset, &shortScratch, sizeof( shortScratch ) );
offset += 2;
// Set attribute value length.
scratch = strlen( gRecords[ i ].attrValue ) + 1;
memcpy( theDataBufferPtr->fBufferData + offset, &scratch, sizeof( scratch ) );
offset += 4;
// Set attribute value.
strcpy( theDataBufferPtr->fBufferData + offset, gRecords[ i ].attrValue );
offset += strlen( gRecords[ i ].attrValue ) + 1;
// Adjust length of entire structure.
theDataBufferPtr->fBufferLength = offset;
// Set offset to this record, way back near the beginning of the StdA structure.
scratch = recordOffset;
memcpy( theDataBufferPtr->fBufferData + 8 + ( 4 * i ), &scratch, sizeof( scratch ) );
// Set length of this record, at the beginning of the record.
scratch = offset - recordOffset;
memcpy( theDataBufferPtr->fBufferData + recordOffset, &scratch, sizeof( scratch ) );
// Starting location for next record.
recordOffset = offset;
// Adjust record count.
getRecordListData->fOutRecEntryCount++;
// Ignore this because we know we have enough space for returning everything.
// Should check total length and make sure everything fit, else set this field
// and break.
getRecordListData->fIOContinueData = NULL;
// Handle countdown for unsigned value.
if ( i > 0 )
i--;
else
break;
}
siResult = eDSNoErr;//eNotYetImplemented;
syslog( LOG_INFO, "PlugInShell_ProcessRequest: kGetRecordList: returning..." );
break;
Custom Calls
A plug-in may respond to custom requests that are not defined by Open
Directory. Custom requests are also handled in ProcessRequest. The
data structure passed for a dsDoPlugInCustomCall request is shown
in Listing 9.
Listing 9: Data Structure for dsDoPlugInCustomCall in PluginData.h
typedef struct {
unsigned long fType;
long fResult;
tDirNodeReference fInNodeRef;
unsigned long fInRequestCode;
tDataBufferPtr fInRequestData;
tDataBufferPtr fOutRequestResponse;
} sDoPlugInCustomCall;
Listing 10 shows custom call handling in ProcessRequest. A message id of 0 returns a simple greeting, and a 2 changes the greeting string using data passed by the client.
Listing 10: Handling a Custom Call in ProcessRequest
long PlugInShell_ProcessRequest( void *inData )
{
long siResult = 0;
sHeader *pMsgHdr = nil;
sDoPlugInCustomCall *customCallData = nil;
if ( inData == nil )
{
return( -4460 );
}
pMsgHdr = (sHeader *)inData;
switch ( pMsgHdr->fType )
{
// snip...
case kDoPlugInCustomCall:
customCallData = ( sDoPlugInCustomCall * )inData;
if ( customCallData->fInRequestCode == 0 )
{
// Greeting.
dsDataNodeSetLength( customCallData->fOutRequestResponse, strlen( gMessage ) );
strcpy( customCallData->fOutRequestResponse->fBufferData, gMessage );
}
else if ( customCallData->fInRequestCode == 2 )
{
strcpy( gMessage, customCallData->fInRequestData->fBufferData );
}
siResult = eDSNoErr;
break;
default:
siResult = eNotHandledByThisNode;
break;
}
pMsgHdr->fResult = siResult;
return( siResult );
}
Debug Messages
Debugging a plug-in during development can be difficult. The code in this
article uses syslog to send messages to the console. It works
reliably while Open Directory is starting. The callback DSDebugLog
may cause the system to hang, and is not used in this plug-in.
Figure 2 shows the Console application's server log. These are high-level messages from Open Directory itself, primarily regarding the success or failure of each plug-in during startup.
Figure 2: Open Directory Server Log
Figure 3 shows the Console application's system log, including numerous
debugging messages generated by DSPlugIn using the syslog facility.
One advantage of this approach is that Console.app retains logs between
sessions. The syslog syntax is:
syslog( <severity_level>, "<message>" );
The severity constants and the function are declared in syslog.h. The following example logs an informational message from DSPlugIn's Shutdown method. The timestamp, host, and process ID are automatically generated by the syslog facility.
syslog( LOG_INFO, "PlugInShell_Shutdown" );
Figure 3: Debugging using syslog
Occasionally things go wrong during development. Figure 4 shows several thread call stacks from the Console application's crash log for Open Directory. The thread that crashed was executing dsBuildListFromStrings from within its Initialize function. Something bad happened while determining the node name string length.
Figure 4: Crash Log
The Client Application
The client application DSTestTool calls DSPlugIn
and other Open Directory plug-ins. DSTestTool accepts option flags
on the command-line. The main function parses the options, creates
an instance of the DSTestTool class, and dispatches to other
routines. In Listing 11, most of main has been removed except for
initialization and flags that specify requests to DSPlugIn.
Listing 11: DSTestTool.main()
int main ( int argc, char * const *argv )
{
int i = 0;
int nextParam = 2;
char *p = nil;
char *pNodeName = nil;
sInt32 siStatus = eDSNoErr;
DSTestTool myClass;
if ( argc > 1 )
{
p = strstr( argv[1], "-" );
if ( p != nil )
{
// snip...
siStatus = myClass.Initialize();
if ( siStatus == noErr )
{
while ( *p != '\0' )
{
switch ( *p )
{
// snip...
case 'u':
myClass.SetNodeCustomMessage();
break;
case 'w':
myClass.GetNodeCustomMessage();
break;
case 'z':
myClass.GetRecordList();
break;
}
p++;
}
myClass.Deinitialize();
}
}
else
{
DoHelp( stderr, argv[0] );
exit( 1 );
}
}
else
{
DoHelp( stderr, argv[0] );
exit( 1 );
}
} // main
Listing 12 shows DSTestTool::GetNodeCustomMessage. This function
calls dsDoPlugInCustomCall, passing a reference to the node, a
request code, and in and out data buffers. The node reference allows Open
Directory to call the correct plug-in. The request code is a programmer-defined
unsigned long that allows you to specify, for example, what type of
custom processing the plug-in should perform. The input buffer allows you to
pass data to the plug-in, and the output buffer contains the results from the
plug-in. In this case there is no input to send. The plug-in simply fills the
output buffer.
Listing 12: Calling dsDoPlugInCustomCall from DSTestTool
sInt32 DSTestTool::GetNodeCustomMessage( void )
{
sInt32 status = eDSNoErr;
char *nodePath = "/DSPlugIn/local";
tDirNodeReference nodeRef = nil;
tDataListPtr inRequest = nil;
status = OpenDirNode( nodePath, &nodeRef );
tDataBufferPtr outData = dsDataBufferAllocate( fDSRef, 1024 );
tDataBufferPtr inData = dsDataBufferAllocate( fDSRef, 1024 );
status = dsDoPlugInCustomCall( nodeRef, 0, inData, outData );
fprintf( stdout, "dsDoPlugInCustomCall returned %s of length %ld\n",
outData->fBufferData, outData->fBufferLength );
status = dsDataBufferDeAllocate( fDSRef, inData );
status = dsDataBufferDeAllocate( fDSRef, outData );
status = CloseDirectoryNode( nodeRef );
return( status );
}
Figure 5 shows the results of invoking DSTestTool with the
-w and -z flags. The -w flag requests the
plug-in's message string, and -z requests the
DSPlugIn's records.
Figure 5: Invoking DSTestTool
$ ./dstesttool -w
dsDoPlugInCustomCall returned Hello from DSPlugIn! of length 20
$ ./dstesttool -z
Record Name = test
1 - 1: (message) Hello, world.
Record Name = another record
1 - 1: (message) Hello, world (again).
recCount = 2
$
Configuring a Plug-In
Figure 6 shows the Directory Access utility and loaded plug-ins. The Checkbox
indicates whether a plug-in has a configuration application available, as
defined by the CFBundleConfigAvail entry in the plug-in's property
list (Info.plist). If you set this property incorrectly or do not
make the configuration application available in the correct location, Directory
Access will display an error dialog when you select the plug-in and click
Configure. But it will not crash. If you want to create a configuration app that
executes in the context of Directory Access, contact Apple Developer Relations
for details.
Figure 6: Available Plug-ins in Directory Access
Alternatively, a standalone configuration application is the simplest way to
configure a plug-in. Once the app locates the plug-in, it can make a custom
request, as discussed earlier in this article. Listing 13 contains a function in
DSTestTool that invokes dsDoPlugInCustomCall in the plug-in and
passes a (hardcoded) new greeting string. You can add a user interface or
command-line switch that allows the user or administrator to specify a custom
value for the greeting, as well as other parameters.
Listing 13: Changing the Plug-In Greeting from DSTestTool
sInt32 DSTestTool::GetNodeCustomMessage( void )
{
sInt32 status = eDSNoErr;
char *nodePath = "/DSPlugIn/local";
tDirNodeReference nodeRef = nil;
status = OpenDirNode( nodePath, &nodeRef );
tDataBufferPtr outData = dsDataBufferAllocate( fDSRef, 1024 );
tDataBufferPtr inData = dsDataBufferAllocate( fDSRef, 1024 );
strcpy( inData->fBufferData, "New message!" );
dsDataNodeSetLength( inData, strlen( "New message!" ) );
status = dsDoPlugInCustomCall( nodeRef, 2, inData, outData );
status = dsDataBufferDeAllocate( fDSRef, inData );
status = dsDataBufferDeAllocate( fDSRef, outData );
status = CloseDirectoryNode( nodeRef );
return( status );
}
Read This Before Deploying Your Plug-In
The plug-in discussed in this article is not a complete implementation. It
fills-in some blanks in the sample code, but you need to implement the remaining
functions or at least make sure they return eNotYetImplemented
while still under development. A more useful plug-in will need to access
resources in the system or on the network. This leads to two critical issues
that a developer needs to resolve before sending a plug-in to users:
- Your plug-in runs as a trusted system daemon, so make sure it is bulletproof. Although Open Directory is resilient, and will automatically restart after a crash, bad code can continue to bring it down and kill system performance.
- Your plug-in MUST perform authorization before accepting requests to write shared data. You might also consider authorizing requests for data reads. Use
dsDoDirNodeAuth to perform authorization. Authorization protects the system from rogue requests to change critical data, such as user records. If your plug-in allows write access to system and user data, but does not check the credentials of the user making the request, that plug-in represents a serious security risk.
For More Information
Using the techniques discussed in this article, you can create Open Directory
plug-ins that respond to requests for information from Open Directory clients.
The references below contain additional details on these topics.
- Join or search the darwin development mailing list.
- The document Open Directory overview discusses how to work with nodes and records, and other client tasks. It also contains Open Directory reference material.
- The document Open Directory plug-in overview discusses the Open Directory runtime architecture, required plug-in entry points, callbacks into the Open Directory service, and how to configure a plug-in. It also contains reference material for the plug-in API.
- The
CFPlugin internals are based on Microsoft's Component Object Model (COM), and the book Inside COM by Dale Rogers, published by Microsoft Press, provides the technical details about this technology.
- Joe Zobkiw's book, Mac OS X Advanced Programming Techniques (Sams Publishing) discusses the COM side of
CFPlugin from the Mac OS X perspective. This book also includes a Carbon plug-in, and even though it is not an Open Directory plug-in, many of the concepts still apply.
- Cocoa Documentation on Loading Resources
- Core Foundation documentation on PlugIns
Aside from the sample code, you may wish to check out the following
frameworks and header files:
DirectoryService.framework
DirServices.h, DirServicesConst.h, and DirServicesTypes.h declare the functions, constants, and date types, respectively, for this framework.
DirServicesUtils.h declares functions for allocating, populating, and deallocating various Open Directory data structures, such as tDataNode, tDataList, and tRecordEntry.
CoreFoundation.framework
CFPlugInCOM.h contains Apple's definitions of the COM types, constants, and macros. The "non-Apple" style of macro names and types reflects COM's Windows heritage.
CFPlugIn.h contains definitions for various utility and dispatch functions used by plug-ins.
Updated: 2004-09-20
|