1 /* sane - Scanner Access Now Easy.
2    Copyright (C) 1996, 1997 David Mosberger-Tang
3    This file is part of the SANE package.
4 
5    This program is free software; you can redistribute it and/or
6    modify it under the terms of the GNU General Public License as
7    published by the Free Software Foundation; either version 2 of the
8    License, or (at your option) any later version.
9 
10    This program is distributed in the hope that it will be useful, but
11    WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13    General Public License for more details.
14 
15    You should have received a copy of the GNU General Public License
16    along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 
18    As a special exception, the authors of SANE give permission for
19    additional uses of the libraries contained in this release of SANE.
20 
21    The exception is that, if you link a SANE library with other files
22    to produce an executable, this does not by itself cause the
23    resulting executable to be covered by the GNU General Public
24    License.  Your use of that executable is in no way restricted on
25    account of linking the SANE library code into it.
26 
27    This exception does not, however, invalidate any other reasons why
28    the executable file might be covered by the GNU General Public
29    License.
30 
31    If you submit changes to SANE to the maintainers to be included in
32    a subsequent release, you agree by submitting the changes that
33    those changes may be distributed with this exception intact.
34 
35    If you write modifications of your own for SANE, it is your choice
36    whether to permit this exception to apply to your modifications.
37    If you do not wish that, delete this exception notice.
38 
39    This file defines a server for Apollo Domain/OS systems.  It does all
40 of the scsi_$ calls that are needed for SANE.  This is necessary because
41 Domain/OS will not allow a child process to access a parent's SCSI
42 device.  The interface is through a common, mapped area.  Mutex locks
43 are used to prevent concurrency problems, and eventcounts are used to
44 notify a waiting process that its request has completed.
45 
46     This program is intended to support only one device at a time,
47 although multiple instances of this program may run concurrently.  It is
48 intended that this program be forked/execd by a SANE application, and
49 that it will exit when the application exits.
50 
51     Upon startup, the program is invoked with the path to an object that
52 needs to be mapped for communication.  The parent process will have
53 already initialized the 'public' eventcounts and locks, and will be
54 waiting for the ResultReady eventcount to be incremented.  After
55 initialization, the server will increment this eventcount, and wait for
56 an incoming request, which is signified by the CommandAvailable
57 eventcount.  This EC will be incremented after another process has
58 filled in the parameter area.
59 
60 DBG levels:
61     0   Error - always printed.
62     1   Basic monitor - print entry to main functions, or errors that are
63          normally suppressed because they are reported at a higher level.
64     2   Medium monitor - show intermediate steps in functions
65     3   Detailed monitor - if its there, print it
66 
67 */
68 
69 #include <assert.h>
70 #include <ctype.h>
71 #include <stdio.h>
72 #include <stdlib.h>
73 #include <string.h>
74 
75 #include <apollo/base.h>
76 #include <apollo/ec2.h>
77 #include <apollo/error.h>
78 #include <apollo/fault.h>
79 #include <apollo/ms.h>
80 #include <apollo/mutex.h>
81 #include <apollo/pfm.h>
82 #include <apollo/scsi.h>
83 
84 #include "../include/sane/config.h"
85 
86 #include "../include/sane/sanei_scsi.h"
87 
88 #include "../include/sane/sanei_debug.h"
89 
90 #include "sanei_DomainOS.h"
91 
92 /* Timeout period for SCSI wait, in milliseconds.
93 We are using 100 seconds here. */
94 #define DomainScsiTimeout 100000
95 
96 /* Communication Area pointer */
97 struct DomainServerCommon *com;
98 
99 /* Handle for fault handler */
100 pfm_$fh_handle_t FaultHandle;
101 
102 
103 static struct
104    {
105    void *DomainSCSIPtr;         /* Pointer to the data block for this device */
106    void *DomainSensePtr;        /* Pointer to the sense area for this device */
107    u_int in_use  : 1;           /* is this DomainFdInfo in use? */
108    u_int fake_fd : 1;           /* is this a fake file descriptor? */
109    scsi_$handle_t scsi_handle;  /* SCSI handle */
110    scsi_$operation_id_t op_id;  /* op_id of current request */
111    } *DomainFdInfo;
112 
113 /* This function is called error might have occurred, but it would be one that I
114 don't know how to handle, or never expect to happen.  */
DomainErrorCheck(status_$t status,const char * message)115 static void DomainErrorCheck(status_$t status, const char *message)
116    {
117    char *subsystem, *module, *code;
118    short subsystem_length, module_length, code_length;
119 
120    if (status.all)
121       {
122       DBG(0, "Unrecoverable Domain/OS Error 0x%08x:  %s\n", status.all, message);
123       error_$find_text(status, &subsystem, &subsystem_length, &module, &module_length, &code, &code_length);
124       if (subsystem_length && module_length && code_length)
125          DBG(0, "%.*s (%.*s/%.*s)\n", code_length, code, subsystem_length, subsystem, module_length, module);
126       exit(EXIT_FAILURE);
127       }
128    }
129 
130 
131 /* This function is the fault handler for the server.  Currently, it only
132 handles asynchronous faults.  It always returns to the faulting code, but
133 it disables the handler, so that the server can be killed if the parent is
134 unable to do so. */
FaultHandler(pfm_$fault_rec_t * FaultStatusPtr)135 pfm_$fh_func_val_t FaultHandler(pfm_$fault_rec_t *FaultStatusPtr)
136    {
137    status_$t status;
138 
139    DBG(1, "In fault handler, status is %08x\n", FaultStatusPtr->status.all);
140    switch (FaultStatusPtr->status.all)
141       {
142       case fault_$quit:
143          pfm_$release_fault_handler(FaultHandle, &status);
144          DomainErrorCheck(status, "Can't release fault handler");
145          return pfm_$return_to_faulting_code;
146       default:
147          DBG(0, "Unrecognized fault type %08x, exiting\n", FaultStatusPtr->status.all);
148          exit(EXIT_FAILURE);
149       }
150    }
151 
152 
DomainSCSIOpen(void)153 static void DomainSCSIOpen(void)
154    {
155    static int num_alloced = 0;
156    int fd;
157    scsi_$handle_t scsi_handle;
158    pinteger len;
159    void *DataBasePtr;
160 
161    /* Find fake fd. */
162    for (fd = 0; fd < num_alloced; ++fd)
163       if (!DomainFdInfo[fd].in_use)
164          break;
165 
166    /* Acquire the device */
167    DBG(1, "DomainSCSIOpen: dev='%s', fd=%d\n", com->open_path, fd);
168    len = strlen(com->open_path);
169    scsi_$acquire((char *)com->open_path, len, &scsi_handle, &com->CommandStatus);
170    if (com->CommandStatus.all != status_$ok)
171       {
172       /* we have a failure, return an error code, and generate debug output */
173       DBG(1, "DomainSCSIOpen: acquire failed, Domain/OS status is %08x\n", com->CommandStatus.all);
174       error_$print(com->CommandStatus);
175       return;
176       }
177    else
178       {
179       /* device acquired, setup buffers and buffer pointers */
180       DBG(2, "DomainSCSIOpen: acquire OK, handle is %x\n", scsi_handle);
181       /* Create/map the data area */
182       tmpnam(com->open_path);
183       DBG(2, "DomainSCSIOpen: Data block name will be '%s'\n", com->open_path);
184       DataBasePtr = ms_$crmapl(com->open_path, strlen(com->open_path), 0, DomainMaxDataSize + DomainSenseSize, ms_$cowriters, &com->CommandStatus);
185       DomainErrorCheck(com->CommandStatus, "Creating Data Area");
186       assert((((int)DataBasePtr) & 0x3ff) == 0);  /* Relies on Domain/OS mapping new objects on page boundary */
187       DBG(2, "Data Buffer block created at %p, length = 0x%lx\n", DataBasePtr, DomainMaxDataSize + DomainSenseSize);
188       /* Wire the buffer */
189       scsi_$wire(scsi_handle, (void *)DataBasePtr, DomainMaxDataSize + DomainSenseSize, &com->CommandStatus);
190       if (com->CommandStatus.all == status_$ok)
191          {
192          /* success, indicate status */
193          DBG(2, "Buffer wire was successful\n");
194          }
195       else
196          {
197          /* failure, print detail and return code */
198          DBG(1, "Buffer wire failed, Domain/OS status is %08x\n", com->CommandStatus.all);
199          error_$print(com->CommandStatus);
200          return;
201          }
202       }
203 
204    if (fd >= num_alloced)
205       {
206       size_t new_size, old_size;
207 
208       old_size = num_alloced * sizeof (DomainFdInfo[0]);
209       num_alloced = fd + 8;
210       new_size = num_alloced * sizeof (DomainFdInfo[0]);
211       if (DomainFdInfo)
212          DomainFdInfo = realloc (DomainFdInfo, new_size);
213       else
214          DomainFdInfo = malloc (new_size);
215       memset ((char *) DomainFdInfo + old_size, 0, new_size - old_size);
216       assert(DomainFdInfo);
217       }
218    DomainFdInfo[fd].in_use = 1;
219    DomainFdInfo[fd].scsi_handle = scsi_handle;
220    DomainFdInfo[fd].DomainSCSIPtr = DataBasePtr;
221    DomainFdInfo[fd].DomainSensePtr = ((char *)DataBasePtr) + DomainMaxDataSize;
222    com->fd = fd;
223    }
224 
225 
DomainSCSIClose(void)226 static void DomainSCSIClose(void)
227    {
228    DomainFdInfo[com->fd].in_use = 0;
229    DBG(1, "sanei_scsi_close:  fd=%d\n", com->fd);
230    /* Unwire the buffer */
231    scsi_$unwire(DomainFdInfo[com->fd].scsi_handle, DomainFdInfo[com->fd].DomainSCSIPtr, DomainMaxDataSize + DomainSenseSize, true, &com->CommandStatus);
232    DomainErrorCheck(com->CommandStatus, "Unwiring SCSI buffers");
233    /* Release the device */
234    scsi_$release(DomainFdInfo[com->fd].scsi_handle, &com->CommandStatus);
235    DomainErrorCheck(com->CommandStatus, "Releasing device");
236    /* Unmap the buffer area */
237    ms_$unmap(DomainFdInfo[com->fd].DomainSCSIPtr, DomainMaxDataSize + DomainSenseSize, &com->CommandStatus);
238    DomainErrorCheck(com->CommandStatus, "Unmapping device data area");
239    }
240 
241 
242 /* I have never seen this called, and I'm not sure what to do with it, so I
243 guarantee that it will generate a fault, and I can add support for it.  */
DomainSCSIFlushAll(void)244 static void DomainSCSIFlushAll(void)
245    {
246    status_$t status;
247 
248    DBG(1, "DomainSCSIFlushAll: ()\n");
249    DBG(0, "Error - unimplemented feature in module" "BACKEND_NAME");
250    assert(1==0);
251    }
252 
253 
254 /* This function must only be called from DomainSCSIEnter.  The current
255 server architecture requires that the Wait immediately follow the Enter
256 command.  */
DomainSCSIWait(void)257 static void DomainSCSIWait(void)
258    {
259    int count;
260    char *ascii_wait_status, *ascii_op_status;
261    pinteger return_count;
262    scsi_$op_status_t status_list[1];
263    scsi_$wait_index_t wait_index;
264 
265    /* wait for the command completion */
266    wait_index = scsi_$wait(DomainFdInfo[com->fd].scsi_handle, DomainScsiTimeout, true, DomainFdInfo[com->fd].op_id, 1, status_list, &return_count, &com->CommandStatus);
267    DBG(2, " scsi_$wait returned status = %08x\n", com->CommandStatus.all);
268    if (com->CommandStatus.all == status_$ok)
269       {
270       switch (wait_index)
271          {
272          case scsi_device_advance:  ascii_wait_status = "scsi_device_advance"; break;
273          case scsi_timeout:         ascii_wait_status = "scsi_timeout"; break;
274          case scsi_async_fault:     ascii_wait_status = "scsi_async_fault"; break;
275          default:                   ascii_wait_status = "unknown"; break;
276          }
277       DBG(2, " scsi_$wait status is %s, return_count is %d\n", ascii_wait_status, return_count);
278       if (wait_index != scsi_device_advance)
279          {
280          DBG(1, "Error - SCSI timeout, or async fault\n");
281          com->CommandStatus.all = scsi_$operation_timeout;
282          }
283       else for (count = 0; count < return_count; count++)
284          {
285          switch (status_list[count].op_status)
286             {
287             case scsi_good_status:                ascii_op_status = "scsi_good_status"; break;
288             case scsi_check_condition:            ascii_op_status = "scsi_check_condition"; break;
289             case scsi_condition_met:              ascii_op_status = "scsi_condition_met"; break;
290             case scsi_rsv1:                       ascii_op_status = "scsi_rsv1"; break;
291             case scsi_busy:                       ascii_op_status = "scsi_busy"; break;
292             case scsi_rsv2:                       ascii_op_status = "scsi_rsv2"; break;
293             case scsi_rsv3:                       ascii_op_status = "scsi_rsv3"; break;
294             case scsi_rsv4:                       ascii_op_status = "scsi_rsv4"; break;
295             case scsi_intermediate_good:          ascii_op_status = "scsi_intermediate_good"; break;
296             case scsi_rsv5:                       ascii_op_status = "scsi_rsv5"; break;
297             case scsi_intermediate_condition_met: ascii_op_status = "scsi_intermediate_condition_met"; break;
298             case scsi_rsv6:                       ascii_op_status = "scsi_rsv6"; break;
299             case scsi_reservation_conflict:       ascii_op_status = "scsi_reservation_conflict"; break;
300             case scsi_rsv7:                       ascii_op_status = "scsi_rsv7"; break;
301             case scsi_rsv8:                       ascii_op_status = "scsi_rsv8"; break;
302             case scsi_rsv9:                       ascii_op_status = "scsi_rsv9"; break;
303             case scsi_undefined_status:           ascii_op_status = "scsi_undefined_status"; break;
304             default:                              ascii_op_status = "unknown"; break;
305             }
306          DBG(2, " list[%d]: op=%x  cmd_status=%08x, status=%s\n", count, status_list[count].op, status_list[count].cmd_status.all, ascii_op_status);
307          switch (status_list[count].cmd_status.all)
308             {
309             case status_$ok:
310                switch (status_list[count].op_status)
311                   {
312                   case scsi_good_status:
313                      break;
314                   case scsi_busy:
315                      com->CommandStatus.all = status_$ok | 0x80000000;
316                      com->SCSIStatus = scsi_busy;
317                      break;
318                   case scsi_check_condition:
319                      {
320                      static unsigned char scanner_sense_cdb[] = {3, 0, 0, 0, DomainSenseSize, 0};
321                      static scsi_$cdb_t sense_cdb;
322                      static linteger sense_cdb_size;
323                      static scsi_$operation_id_t sense_op_id;
324                      static status_$t sense_status;
325                      static pinteger sense_return_count;
326                      static int temp;
327 
328                      /* Issue the sense command (wire, issue, wait, unwire */
329                      sense_cdb_size = sizeof(scanner_sense_cdb);
330                      memcpy(&sense_cdb, scanner_sense_cdb, sense_cdb_size);
331                      scsi_$do_command_2(DomainFdInfo[com->fd].scsi_handle, sense_cdb, sense_cdb_size, DomainFdInfo[com->fd].DomainSensePtr, DomainSenseSize, scsi_read, &sense_op_id, &sense_status);
332                      DomainErrorCheck(sense_status, "Executing sense command");
333                      scsi_$wait(DomainFdInfo[com->fd].scsi_handle, DomainScsiTimeout, true, sense_op_id, 1, status_list, &sense_return_count, &sense_status);
334                      /* The following debug output is scanner specific */
335                      DBG(2, "Sense information:  Error code=%02x, ASC=%02x, ASCQ=%02x\n", ((u_char *)DomainFdInfo[com->fd].DomainSensePtr)[0], ((char *)DomainFdInfo[com->fd].DomainSensePtr)[0xc], ((char *)DomainFdInfo[com->fd].DomainSensePtr)[0xd]);
336                      DBG(2, " Sense dump:\n");
337                      for (temp = 0; temp < DomainSenseSize; temp++)
338                         DBG(2, " %02x", ((u_char *)DomainFdInfo[com->fd].DomainSensePtr)[temp]);
339                      DBG(2, "\n");
340                      /* see if buffer underrun - ILI/Valid are set, and command was a read */
341                      /* Warning - this might be UMAX specific */
342                      if ((((char *)DomainFdInfo[com->fd].DomainSensePtr)[0] == 0xf0) && (((char *)DomainFdInfo[com->fd].DomainSensePtr)[2] & 0x20) && (com->cdb.g0.cmd == 0x28))
343                         {
344                         /* Warning - the following code is specific to endianness and int size */
345                         /*   Its also very ugly */
346                         DBG(2, "Shortening destination length by %x bytes\n", *(int *)(((char *)DomainFdInfo[com->fd].DomainSensePtr)+3));
347                         com->dst_size -= *(int *)(((char *)DomainFdInfo[com->fd].DomainSensePtr)+3);
348                         DBG(2, "Final dest size is %x\n", com->dst_size);
349                         }
350                      else
351                         {
352                         /* Set this status so that the sense handler can be called */
353                         com->CommandStatus.all = status_$ok | 0x80000000;
354                         com->SCSIStatus = scsi_check_condition;
355                         }
356                      }
357                      break;
358                   default:
359                      /* I fault out in this case because I want to know about this error,
360                         and this guarantees that it will get attention. */
361                      DBG(0, "Unrecoverable Domain/OS scsi handler error:  status=%08x\n", status_list[count].op_status);
362                      exit(EXIT_FAILURE);
363                   }
364                break;
365             /* Handle recognized error conditions by copying the error code over */
366             case scsi_$operation_timeout:
367             case scsi_$dma_underrun:  /* received by some backend code */
368             case scsi_$hdwr_failure:  /* received when both scanners were active */
369                com->CommandStatus = status_list[count].cmd_status;
370                break;
371             default:
372                DBG(0, "Unrecoverable DomainOS scsi handler error:  status=%08x\n", status_list[count].cmd_status.all);
373                error_$print(status_list[count].cmd_status);
374                exit(EXIT_FAILURE);
375             }
376          }
377       /* Dump the buffer contents */
378       if (com->direction == scsi_read)
379          {
380          DBG(3, "first words of buffer are:\n");
381          for (return_count = 0; return_count < com->dst_size; return_count++)
382             DBG(3, "%02X%c", ((unsigned char *)DomainFdInfo[com->fd].DomainSCSIPtr)[return_count], (return_count % 16) == 15 ? '\n' : ' ');
383          DBG(3, "\n");
384          }
385       }
386    else
387       {
388       /* scsi_$wait failed */
389       DBG(1, "scsi_$wait failed, status is %08x\n", com->CommandStatus.all);
390       }
391    }
392 
393 
DomainSCSIEnter(void)394 static void DomainSCSIEnter(void)
395    {
396    static int count;
397 
398    /* Give some debug info */
399    DBG(1, "Entering DomainSCSIEnter, fd=%d, opcode=%02X\n", com->fd, com->cdb.all[0]);
400    DBG(2, " CDB Contents: ");
401    for (count = 0; count < com->cdb_size; count++)
402       DBG(2, " %02X", com->cdb.all[count]);
403    DBG(2, "\n");
404    DBG(2, "Buffer address is 0x%08x\n", DomainFdInfo[com->fd].DomainSCSIPtr);
405    DBG(2, "Buffer size is %x\n", com->buf_size);
406    DBG(2, "Direction is %s\n", (com->direction == scsi_read) ? "READ" : "WRITE");
407    /* now queue the command */
408    scsi_$do_command_2(DomainFdInfo[com->fd].scsi_handle, com->cdb, com->cdb_size, DomainFdInfo[com->fd].DomainSCSIPtr, com->buf_size, com->direction, &DomainFdInfo[com->fd].op_id, &com->CommandStatus);
409    if (com->CommandStatus.all == status_$ok)
410       {
411       /* success, indicate status */
412       DBG(2, " scsi_$do_command_2 was successful, op_id is %x\n", DomainFdInfo[com->fd].op_id);
413 
414       /* If we supported multiple outstanding requests for one device, this would be
415          a good breakpoint.  We would store the op_id in a private place, and construct
416          a queue for each device.  This complicates things, and SANE doesn't seem to need
417          it, so it won't be implemented.  The current server architecture does the wait
418          automatically, and status for the entire operation is returned.  This means that
419          the sanei_scsi_req_enter and sanei_scsi_req_wait calls don't make sense, and
420          should generate an error. */
421       DomainSCSIWait();
422       }
423    else
424       {
425       /* failure, print detail and return code */
426       DBG(1, " scsi_$do_command_2 failed, status is %08x\n", com->CommandStatus.all);
427       }
428    }
429 
430 
431 /* This function is not currently used. */
DomainSCSIReqWait(void)432 static void DomainSCSIReqWait(void)
433    {
434    DBG(1, "sanei_scsi_req_wait: (id=%p)\n", NULL);
435    return;
436    }
437 
438 
439 /* Startup the server */
sanei_DomainOS_init(char * path)440 static void sanei_DomainOS_init(char *path)
441    {
442    int done, index;
443    long CommandTriggerValue;
444    ec2_$ptr_t CommandAvailablePtr[1];
445    status_$t status;
446    unsigned long length_mapped;
447 
448    DBG(1, "Starting Domain SANE Server, common area path = '%s'\n", path);
449    com = ms_$mapl(path, strlen(path), 0, sizeof(struct DomainServerCommon), ms_$cowriters, ms_$wr, true, &length_mapped, &status);
450    DomainErrorCheck(status, "Can't open common area");
451    if (length_mapped < sizeof(struct DomainServerCommon))
452       {
453       DBG(0, "Error - can't open common area '%s' to required length\n", path);
454       DBG(0, " Required length = %lx, returned length = %lx\n", sizeof(struct DomainServerCommon), length_mapped);
455       exit(EXIT_FAILURE);
456       }
457    /* Make the file temporary, so it will disappear when it is closed */
458    ms_$mk_temporary(com, &status);
459    DomainErrorCheck(status, "Can't make common file temporary");
460    DBG(2, "Domain Server common area mapped, length is %lx\n", length_mapped);
461    /* The communication area is open, give the initial response */
462    ec2_$advance(&com->ResultReady, &status);
463    DomainErrorCheck(status, "Can't advance ResultReady EC after startup");
464    /* Enter the command loop */
465    CommandAvailablePtr[0] = &com->CommandAvailable;
466    CommandTriggerValue = ec2_$read(com->CommandAvailable) + 1;
467    /* Inhibit asynchronous faults */
468 /*   pfm_$inhibit();*/
469    /* Establish the fault handler */
470    FaultHandle = pfm_$establish_fault_handler(pfm_$all_faults, 0, FaultHandler, &status);
471    DomainErrorCheck(status, "Can't establish fault handler");
472    done = 0;
473    do
474       {
475       /* Wait for the command */
476       DBG(2, "Waiting for incoming command\n");
477       do
478          {
479          index = ec2_$wait_svc(CommandAvailablePtr, &CommandTriggerValue, 1, &status);
480          }
481       while (status.all == ec2_$wait_quit);
482       DomainErrorCheck(status, "Error waiting on CommandAvailable EC");
483       assert (index == 1);
484       /* Get the trigger value for next time - this avoids a race/deadlock */
485       CommandTriggerValue = ec2_$read(com->CommandAvailable) + 1;
486       /* decode/execute the command */
487       DBG(2, "Received a command - opcode is %x\n", com->opcode);
488       switch(com->opcode)
489          {
490          case Open:
491             DomainSCSIOpen();
492             ec2_$advance(&com->CommandAccepted, &status);
493             DomainErrorCheck(status, "Can't advance CommandAccepted EC on open");
494             break;
495          case Close:
496             DomainSCSIClose();
497             ec2_$advance(&com->CommandAccepted, &status);
498             DomainErrorCheck(status, "Can't advance CommandAccepted EC on close");
499             break;
500          case Enter:
501             DomainSCSIEnter();
502             ec2_$advance(&com->CommandAccepted, &status);
503             DomainErrorCheck(status, "Can't advance CommandAccepted EC on enter");
504             break;
505          case Exit:
506             done = 1;
507             /* This lets the parent know that the command was accepted.  It can be
508                used to avoid sending a signal.  */
509             ec2_$advance(&com->CommandAccepted, &status);
510             DomainErrorCheck(status, "Can't advance CommandAccepted EC on exit");
511             break;
512          default:
513             DBG(1, "Invalid command %x received\n", com->opcode);
514          }
515       DBG(2, "Command processing complete\n");
516       }
517    while (!done);
518    /* This would be a good place to close all devices, but for now we'll assume
519       they have already been closed by a well-behaved program */
520    /* Unmap the common area */
521    ms_$unmap(com, sizeof(struct DomainServerCommon), &status);
522    DomainErrorCheck(status, "Error unmapping common area");
523    DBG(1, "Exiting Domain SANE Server\n");
524 /*   pfm_$enable();*/
525    exit(EXIT_SUCCESS);
526    }
527