18 #ifdef AFS_FREELANCE_CLIENT
19 #include "cm_freelance.h"
22 extern void afsi_log(char *pattern, ...);
24 int cm_noLocalMountPoints;
26 int cm_fakeDirCallback=0;
27 int cm_fakeGettingCallback=0;
28 int cm_fakeDirVersion = 0x8;
29 cm_localMountPoint_t* cm_localMountPoints;
30 osi_mutex_t cm_Freelance_Lock;
31 int cm_localMountPointChangeFlag = 0;
32 int cm_freelanceEnabled = 0;
34 void cm_InitFakeRootDir();
36 void cm_InitFreelance() {
37 lock_InitializeMutex(&cm_Freelance_Lock, "Freelance Lock");
39 // yj: first we make a call to cm_initLocalMountPoints
40 // to read all the local mount points from an ini file
41 cm_InitLocalMountPoints();
43 // then we make a call to InitFakeRootDir to create
44 // a fake root directory based on the local mount points
50 /* yj: Initialization of the fake root directory */
51 /* to be called while holding freelance lock unless during init. */
52 void cm_InitFakeRootDir() {
58 // allocate space for the fake info
59 cm_dirHeader_t fakeDirHeader;
60 cm_dirEntry_t fakeEntry;
61 cm_pageHeader_t fakePageHeader;
63 // i'm going to calculate how much space is needed for
64 // this fake root directory. we have these rules:
65 // 1. there are cm_noLocalMountPoints number of entries
66 // 2. each page is CM_DIR_PAGESIZE in size
67 // 3. the first 13 chunks of the first page are used for
69 // 4. the first chunk of all subsequent pages are used
70 // for page header stuff
71 // 5. a max of CM_DIR_EPP entries are allowed per page
72 // 6. each entry takes 1 or more chunks, depending on
73 // the size of the mount point string, as determined
75 // 7. each chunk is CM_DIR_CHUNKSIZE bytes
77 int CPP = CM_DIR_PAGESIZE / CM_DIR_CHUNKSIZE;
78 int curChunk = 13; // chunks 0 - 12 are used for header stuff
79 // of the first page in the directory
82 int curDirEntryInPage = 0;
86 /* Reserve 2 directory chunks for "." and ".." */
89 while (curDirEntry!=cm_noLocalMountPoints) {
90 sizeOfCurEntry = cm_NameEntries((cm_localMountPoints+curDirEntry)->namep, 0);
91 if ((curChunk + sizeOfCurEntry >= CPP) ||
92 (curDirEntryInPage + 1 >= CM_DIR_EPP)) {
94 curDirEntryInPage = 0;
97 curChunk += sizeOfCurEntry;
102 dirSize = (curPage+1) * CM_DIR_PAGESIZE;
103 cm_FakeRootDir = malloc(dirSize);
104 cm_fakeDirSize = dirSize;
106 // yj: when we get here, we've figured out how much memory we need and
107 // allocated the appropriate space for it. we now prceed to fill
108 // it up with entries.
111 curDirEntryInPage = 0;
114 // fields in the directory entry that are unused.
116 fakeEntry.length = 0;
118 fakeEntry.fid.unique = htonl(1);
120 // the first page is special, it uses fakeDirHeader instead of fakePageHeader
121 // we fill up the page with dirEntries that belong there and we make changes
122 // to the fakeDirHeader.header.freeBitmap along the way. Then when we're done
123 // filling up the dirEntries in this page, we copy the fakeDirHeader into
124 // the top of the page.
126 // init the freeBitmap array
128 fakeDirHeader.header.freeBitmap[i]=0;
130 fakeDirHeader.header.freeBitmap[0] = 0xff;
131 fakeDirHeader.header.freeBitmap[1] = 0x7f;
134 // we start counting at 13 because the 0th to 12th chunks are used for header
137 // stick the first 2 entries "." and ".." in
138 fakeEntry.fid.unique = htonl(1);
139 fakeEntry.fid.vnode = htonl(1);
140 strcpy(fakeEntry.name, ".");
141 currentPos = cm_FakeRootDir + curPage * CM_DIR_PAGESIZE + curChunk * CM_DIR_CHUNKSIZE;
142 memcpy(currentPos, &fakeEntry, CM_DIR_CHUNKSIZE);
143 curChunk++; curDirEntryInPage++;
144 strcpy(fakeEntry.name, "..");
145 currentPos = cm_FakeRootDir + curPage * CM_DIR_PAGESIZE + curChunk * CM_DIR_CHUNKSIZE;
146 memcpy(currentPos, &fakeEntry, CM_DIR_CHUNKSIZE);
147 curChunk++; curDirEntryInPage++;
149 // keep putting stuff into page 0 if
150 // 1. we're not done with all entries
151 // 2. we have less than CM_DIR_EPP entries in page 0
152 // 3. we're not out of chunks in page 0
154 while( (curDirEntry!=cm_noLocalMountPoints) &&
155 (curDirEntryInPage < CM_DIR_EPP) &&
156 (curChunk + cm_NameEntries((cm_localMountPoints+curDirEntry)->namep, 0) <= CPP))
159 noChunks = cm_NameEntries((cm_localMountPoints+curDirEntry)->namep, 0);
160 fakeEntry.fid.vnode = htonl(curDirEntry + 2);
161 currentPos = cm_FakeRootDir + curPage * CM_DIR_PAGESIZE + curChunk * CM_DIR_CHUNKSIZE;
163 memcpy(currentPos, &fakeEntry, CM_DIR_CHUNKSIZE);
164 strcpy(currentPos + 12, (cm_localMountPoints+curDirEntry)->namep);
167 for (i=0; i<noChunks; i++) {
168 t1 = (curChunk + i) / 8;
169 t2 = curChunk + i - (t1*8);
170 fakeDirHeader.header.freeBitmap[t1] |= (1 << t2);
175 // when we get here, we're done with filling in the entries for page 0
176 // copy in the header info
178 memcpy(cm_FakeRootDir, &fakeDirHeader, 13 * CM_DIR_CHUNKSIZE);
182 // ok, page 0's done. Move on to the next page.
183 while (curDirEntry!=cm_noLocalMountPoints) {
185 curChunk = 1; // the zeroth chunk is reserved for page header
186 curDirEntryInPage = 0;
187 for (i=0; i<8; i++) {
188 fakePageHeader.freeBitmap[i]=0;
190 fakePageHeader.freeCount = 0;
191 fakePageHeader.pgcount = 0;
192 fakePageHeader.tag = htons(1234);
194 // while we're on the same page...
195 while ( (curDirEntry!=cm_noLocalMountPoints) &&
196 (curDirEntryInPage < CM_DIR_EPP) &&
197 (curChunk + cm_NameEntries((cm_localMountPoints+curDirEntry)->namep, 0) <= CPP))
199 // add an entry to this page
201 noChunks = cm_NameEntries((cm_localMountPoints+curDirEntry)->namep, 0);
202 fakeEntry.fid.vnode=htonl(curDirEntry+2);
203 currentPos = cm_FakeRootDir + curPage * CM_DIR_PAGESIZE + curChunk * CM_DIR_CHUNKSIZE;
204 memcpy(currentPos, &fakeEntry, CM_DIR_CHUNKSIZE);
205 strcpy(currentPos + 12, (cm_localMountPoints+curDirEntry)->namep);
208 for (i=0; i<noChunks; i++) {
209 t1 = (curChunk + i) / 8;
210 t2 = curChunk + i - (t1*8);
211 fakePageHeader.freeBitmap[t1] |= (1 << t2);
215 memcpy(cm_FakeRootDir + curPage * CM_DIR_PAGESIZE, &fakePageHeader, sizeof(fakePageHeader));
220 // we know the fakeDir is setup properly, so we claim that we have callback
221 cm_fakeDirCallback=1;
223 // when we get here, we've set up everything! done!
226 int cm_FakeRootFid(cm_fid_t *fidp)
228 fidp->cell = 0x1; /* root cell */
229 fidp->volume = AFS_FAKE_ROOT_VOL_ID; /* root.afs ? */
235 int cm_getLocalMountPointChange() {
236 return cm_localMountPointChangeFlag;
239 int cm_clearLocalMountPointChange() {
240 cm_localMountPointChangeFlag = 0;
244 /* called directly from ioctl */
245 /* called while not holding freelance lock */
246 int cm_noteLocalMountPointChange() {
247 lock_ObtainMutex(&cm_Freelance_Lock);
249 cm_localMountPointChangeFlag = 1;
250 lock_ReleaseMutex(&cm_Freelance_Lock);
254 int cm_reInitLocalMountPoints() {
257 cm_scache_t *scp, **lscpp, *tscp;
260 osi_Log0(afsd_logp,"----- freelance reinitialization starts ----- ");
262 // first we invalidate all the SCPs that were created
263 // for the local mount points
265 osi_Log0(afsd_logp,"Invalidating local mount point scp... ");
268 aFid.volume=AFS_FAKE_ROOT_VOL_ID;
272 lock_ObtainWrite(&cm_scacheLock);
273 lock_ObtainMutex(&cm_Freelance_Lock); /* always scache then freelance lock */
274 for (i=0; i<cm_noLocalMountPoints; i++) {
275 hash = CM_SCACHE_HASH(&aFid);
276 for (scp=cm_hashTablep[hash]; scp; scp=scp->nextp) {
277 if (scp->fid.volume == aFid.volume &&
278 scp->fid.vnode == aFid.vnode &&
279 scp->fid.unique == aFid.unique
282 // mark the scp to be reused
283 lock_ReleaseWrite(&cm_scacheLock);
284 lock_ObtainMutex(&scp->mx);
285 cm_DiscardSCache(scp);
286 lock_ReleaseMutex(&scp->mx);
287 cm_CallbackNotifyChange(scp);
288 lock_ObtainWrite(&cm_scacheLock);
291 // take the scp out of the hash
292 lscpp = &cm_hashTablep[hash];
293 for (tscp=*lscpp; tscp; lscpp = &tscp->nextp, tscp = *lscpp) {
294 if (tscp == scp) break;
297 scp->flags &= ~CM_SCACHEFLAG_INHASH;
302 aFid.vnode = aFid.vnode + 1;
304 lock_ReleaseWrite(&cm_scacheLock);
305 osi_Log0(afsd_logp,"\tall old scp cleared!");
307 // we must free the memory that was allocated in the prev
308 // cm_InitLocalMountPoints call
309 osi_Log0(afsd_logp,"Removing old localmountpoints... ");
310 free(cm_localMountPoints);
311 osi_Log0(afsd_logp,"\tall old localmountpoints cleared!");
313 // now re-init the localmountpoints
314 osi_Log0(afsd_logp,"Creating new localmountpoints... ");
315 cm_InitLocalMountPoints();
316 osi_Log0(afsd_logp,"\tcreated new set of localmountpoints!");
319 // now we have to free the memory allocated in cm_initfakerootdir
320 osi_Log0(afsd_logp,"Removing old fakedir... ");
321 free(cm_FakeRootDir);
322 osi_Log0(afsd_logp,"\t\told fakedir removed!");
324 // then we re-create that dir
325 osi_Log0(afsd_logp,"Creating new fakedir... ");
326 cm_InitFakeRootDir();
327 osi_Log0(afsd_logp,"\t\tcreated new fakedir!");
329 lock_ReleaseMutex(&cm_Freelance_Lock);
331 osi_Log0(afsd_logp,"----- freelance reinit complete -----");
336 // yj: open up the ini file and read all the local mount
337 // points that are stored there. Part of the initialization
338 // process for the freelance client.
339 /* to be called while holding freelance lock unless during init. */
340 long cm_InitLocalMountPoints() {
345 cm_localMountPoint_t* aLocalMountPoint;
348 char rootCellName[256];
350 HKEY hkFreelance = 0;
351 DWORD dwType, dwSize;
357 if (RegOpenKeyEx( HKEY_LOCAL_MACHINE,
358 "SOFTWARE\\OpenAFS\\Client\\Freelance",
360 KEY_READ|KEY_WRITE|KEY_QUERY_VALUE,
361 &hkFreelance) == ERROR_SUCCESS) {
363 RegQueryInfoKey( hkFreelance,
366 NULL, /* lpReserved */
367 NULL, /* lpcSubKeys */
368 NULL, /* lpcMaxSubKeyLen */
369 NULL, /* lpcMaxClassLen */
370 &dwMountPoints, /* lpcValues */
371 NULL, /* lpcMaxValueNameLen */
372 NULL, /* lpcMaxValueLen */
373 NULL, /* lpcbSecurityDescriptor */
374 NULL /* lpftLastWriteTime */
377 if ( dwMountPoints == 0 ) {
378 sprintf(line,"%s#%s:root.cell.\n",rootCellName,rootCellName);
380 dwSize = strlen(line) + 1;
381 RegSetValueEx( hkFreelance, "0", 0, dwType, line, dwSize);
382 sprintf(line,".%s%%%s:root.cell.\n",rootCellName,rootCellName);
383 dwSize = strlen(line) + 1;
384 RegSetValueEx( hkFreelance, "1", 0, dwType, line, dwSize);
388 // get the number of entries there are from the first line
390 cm_noLocalMountPoints = dwMountPoints;
392 // create space to store the local mount points
393 cm_localMountPoints = malloc(sizeof(cm_localMountPoint_t) * cm_noLocalMountPoints);
394 aLocalMountPoint = cm_localMountPoints;
396 // now we read n lines and parse them into local mount points
397 // where n is the number of local mount points there are, as
399 // Each line in the ini file represents 1 local mount point and
400 // is in the format xxx#yyy:zzz, where xxx is the directory
401 // entry name, yyy is the cell name and zzz is the volume name.
402 // #yyy:zzz together make up the mount point.
403 for ( dwIndex = 0 ; dwIndex < dwMountPoints; dwIndex++ ) {
404 TCHAR szValueName[16];
405 DWORD dwValueSize = 16;
406 dwSize = sizeof(line);
407 RegEnumValue( hkFreelance, dwIndex, szValueName, &dwValueSize, NULL,
408 &dwType, line, &dwSize);
410 // line is not empty, so let's parse it
411 t = strchr(line, '#');
413 t = strchr(line, '%');
414 // make sure that there is a '#' or '%' separator in the line
416 afsi_log("error occurred while parsing entry in %s: no # or %% separator in line %d", AFS_FREELANCE_INI, i);
417 fprintf(stderr, "error occurred while parsing entry in afs_freelance.ini: no # or %% separator in line %d", i);
420 aLocalMountPoint->namep=malloc(t-line+1);
421 memcpy(aLocalMountPoint->namep, line, t-line);
422 *(aLocalMountPoint->namep + (t-line)) = 0;
424 aLocalMountPoint->mountPointStringp=malloc(strlen(line) - (t-line) + 1);
425 memcpy(aLocalMountPoint->mountPointStringp, t, strlen(line)-(t-line)-2);
426 *(aLocalMountPoint->mountPointStringp + (strlen(line)-(t-line)-2)) = 0;
428 osi_Log2(afsd_logp,"found mount point: name %s, string %s",
429 osi_LogSaveString(afsd_logp,aLocalMountPoint->namep),
430 osi_LogSaveString(afsd_logp,aLocalMountPoint->mountPointStringp));
435 RegCloseKey(hkFreelance);
440 cm_GetConfigDir(hdir);
441 strcat(hdir, AFS_FREELANCE_INI);
442 // open the ini file for reading
443 fp = fopen(hdir, "r");
445 // if we fail to open the file, create an empty one
447 fp = fopen(hdir, "w");
448 code = cm_GetRootCellName(rootCellName);
451 fprintf(fp,"%s#%s:root.cell.\n",rootCellName,rootCellName);
452 fprintf(fp,".%s%%%s:root.cell.\n",rootCellName,rootCellName);
458 return 0; /* success */
462 // we successfully opened the file
463 osi_Log0(afsd_logp,"opened afs_freelance.ini");
467 RegCreateKeyEx( HKEY_LOCAL_MACHINE,
468 "SOFTWARE\\OpenAFS\\Client\\Freelance",
471 REG_OPTION_NON_VOLATILE,
479 // now we read the first line to see how many entries
481 fgets(line, sizeof(line), fp);
483 // if the line is empty at any point when we're reading
484 // we're screwed. report error and return.
486 afsi_log("error occurred while reading afs_freelance.ini");
487 fprintf(stderr, "error occurred while reading afs_freelance.ini");
491 // get the number of entries there are from the first line
493 cm_noLocalMountPoints = atoi(line);
495 // create space to store the local mount points
496 cm_localMountPoints = malloc(sizeof(cm_localMountPoint_t) * cm_noLocalMountPoints);
497 aLocalMountPoint = cm_localMountPoints;
499 // now we read n lines and parse them into local mount points
500 // where n is the number of local mount points there are, as
502 // Each line in the ini file represents 1 local mount point and
503 // is in the format xxx#yyy:zzz, where xxx is the directory
504 // entry name, yyy is the cell name and zzz is the volume name.
505 // #yyy:zzz together make up the mount point.
506 for (i=0; i<cm_noLocalMountPoints; i++) {
507 fgets(line, sizeof(line), fp);
508 // check that the line is not empty
510 afsi_log("error occurred while parsing entry in %s: empty line in line %d", AFS_FREELANCE_INI, i);
511 fprintf(stderr, "error occurred while parsing entry in afs_freelance.ini: empty line in line %d", i);
518 /* we are migrating to the registry */
519 sprintf(szIndex,"%d",dwIndex++);
521 dwSize = strlen(line) + 1;
522 RegSetValueEx( hkFreelance, szIndex, 0, dwType, line, dwSize);
526 // line is not empty, so let's parse it
527 t = strchr(line, '#');
529 t = strchr(line, '%');
530 // make sure that there is a '#' or '%' separator in the line
532 afsi_log("error occurred while parsing entry in %s: no # or %% separator in line %d", AFS_FREELANCE_INI, i);
533 fprintf(stderr, "error occurred while parsing entry in afs_freelance.ini: no # or %% separator in line %d", i);
536 aLocalMountPoint->namep=malloc(t-line+1);
537 memcpy(aLocalMountPoint->namep, line, t-line);
538 *(aLocalMountPoint->namep + (t-line)) = 0;
540 aLocalMountPoint->mountPointStringp=malloc(strlen(line) - (t-line) + 1);
541 memcpy(aLocalMountPoint->mountPointStringp, t, strlen(line)-(t-line)-2);
542 *(aLocalMountPoint->mountPointStringp + (strlen(line)-(t-line)-2)) = 0;
544 osi_Log2(afsd_logp,"found mount point: name %s, string %s",
545 aLocalMountPoint->namep,
546 aLocalMountPoint->mountPointStringp);
553 RegCloseKey(hkFreelance);
560 int cm_getNoLocalMountPoints() {
561 return cm_noLocalMountPoints;
564 cm_localMountPoint_t* cm_getLocalMountPoint(int vnode) {
568 long cm_FreelanceAddMount(char *filename, char *cellname, char *volume, int rw, cm_fid_t *fidp)
577 HKEY hkFreelance = 0;
578 DWORD dwType, dwSize;
583 /* before adding, verify the cell name; if it is not a valid cell,
584 don't add the mount point.
585 allow partial matches as a means of poor man's alias. */
586 /* major performance issue? */
587 osi_Log3(afsd_logp,"Freelance Add Mount request: filename=%s cellname=%s volume=%s %s",
588 osi_LogSaveString(afsd_logp,filename),
589 osi_LogSaveString(afsd_logp,cellname),
590 osi_LogSaveString(afsd_logp,volume), rw ? "rw" : "ro");
591 if (cellname[0] == '.') {
592 if (!cm_GetCell_Gen(&cellname[1], &fullname[1], CM_FLAG_CREATE))
596 if (!cm_GetCell_Gen(cellname, fullname, CM_FLAG_CREATE))
600 osi_Log1(afsd_logp,"Freelance Adding Mount for Cell: %s",
601 osi_LogSaveString(afsd_logp,cellname));
603 lock_ObtainMutex(&cm_Freelance_Lock);
606 if (RegOpenKeyEx( HKEY_LOCAL_MACHINE,
607 "SOFTWARE\\OpenAFS\\Client\\Freelance",
609 KEY_READ|KEY_WRITE|KEY_QUERY_VALUE,
610 &hkFreelance) == ERROR_SUCCESS) {
612 RegQueryInfoKey( hkFreelance,
615 NULL, /* lpReserved */
616 NULL, /* lpcSubKeys */
617 NULL, /* lpcMaxSubKeyLen */
618 NULL, /* lpcMaxClassLen */
619 &dwMountPoints, /* lpcValues */
620 NULL, /* lpcMaxValueNameLen */
621 NULL, /* lpcMaxValueLen */
622 NULL, /* lpcbSecurityDescriptor */
623 NULL /* lpftLastWriteTime */
627 sprintf(line, "%s%%%s:%s\n", filename, fullname, volume);
629 sprintf(line, "%s#%s:%s\n", filename, fullname, volume);
631 /* If we are adding a new value, there must be an unused name
632 * within the range 0 to dwMountPoints
634 for ( dwIndex = 0; dwIndex <= dwMountPoints; dwIndex++ ) {
636 sprintf(szIndex, "%d", dwIndex);
637 if (RegQueryValueEx( hkFreelance, szIndex, 0, &dwType, NULL, &dwSize) != ERROR_SUCCESS) {
638 /* found an unused value */
640 dwSize = strlen(line) + 1;
641 RegSetValueEx( hkFreelance, szIndex, 0, dwType, line, dwSize);
645 RegCloseKey(hkFreelance);
649 cm_GetConfigDir(hfile);
650 strcat(hfile, AFS_FREELANCE_INI);
651 fp = fopen(hfile, "r+");
653 return CM_ERROR_INVAL;
654 fgets(line, sizeof(line), fp);
657 fseek(fp, 0, SEEK_SET);
658 fprintf(fp, "%d", n);
659 fseek(fp, 0, SEEK_END);
661 fprintf(fp, "%s%%%s:%s\n", filename, fullname, volume);
663 fprintf(fp, "%s#%s:%s\n", filename, fullname, volume);
666 lock_ReleaseMutex(&cm_Freelance_Lock);
668 /* cm_reInitLocalMountPoints(); */
671 fidp->vnode = cm_noLocalMountPoints + 1; /* vnode value of last mt pt */
673 cm_noteLocalMountPointChange();
677 long cm_FreelanceRemoveMount(char *toremove)
683 char hfile[120], hfile2[120];
687 HKEY hkFreelance = 0;
688 DWORD dwType, dwSize;
693 lock_ObtainMutex(&cm_Freelance_Lock);
697 if (RegOpenKeyEx( HKEY_LOCAL_MACHINE,
698 "SOFTWARE\\OpenAFS\\Client\\Freelance",
700 KEY_READ|KEY_WRITE|KEY_QUERY_VALUE,
701 &hkFreelance) == ERROR_SUCCESS) {
703 RegQueryInfoKey( hkFreelance,
706 NULL, /* lpReserved */
707 NULL, /* lpcSubKeys */
708 NULL, /* lpcMaxSubKeyLen */
709 NULL, /* lpcMaxClassLen */
710 &dwMountPoints, /* lpcValues */
711 NULL, /* lpcMaxValueNameLen */
712 NULL, /* lpcMaxValueLen */
713 NULL, /* lpcbSecurityDescriptor */
714 NULL /* lpftLastWriteTime */
717 for ( dwIndex = 0; dwIndex < dwMountPoints; dwIndex++ ) {
718 TCHAR szValueName[16];
719 DWORD dwValueSize = 16;
720 dwSize = sizeof(line);
721 RegEnumValue( hkFreelance, dwIndex, szValueName, &dwValueSize, NULL,
722 &dwType, line, &dwSize);
724 cp=strchr(line, '#');
726 cp=strchr(line, '%');
727 memcpy(shortname, line, cp-line);
728 shortname[cp-line]=0;
730 if (!strcmp(shortname, toremove)) {
731 RegDeleteValue( hkFreelance, szValueName );
735 RegCloseKey(hkFreelance);
739 cm_GetConfigDir(hfile);
740 strcat(hfile, AFS_FREELANCE_INI);
741 strcpy(hfile2, hfile);
743 fp1=fopen(hfile, "r+");
745 return CM_ERROR_INVAL;
746 fp2=fopen(hfile2, "w+");
749 return CM_ERROR_INVAL;
752 fgets(line, sizeof(line), fp1);
754 fprintf(fp2, "%d\n", n-1);
756 for (i=0; i<n; i++) {
757 fgets(line, sizeof(line), fp1);
758 cp=strchr(line, '#');
760 cp=strchr(line, '%');
761 memcpy(shortname, line, cp-line);
762 shortname[cp-line]=0;
764 if (strcmp(shortname, toremove)==0) {
775 return CM_ERROR_NOSUCHFILE;
778 rename(hfile2, hfile);
781 lock_ReleaseMutex(&cm_Freelance_Lock);
782 cm_noteLocalMountPointChange();
786 #endif /* AFS_FREELANCE_CLIENT */