pcsc-lite 1.6.4
|
00001 /* 00002 * MUSCLE SmartCard Development ( http://www.linuxnet.com ) 00003 * 00004 * Copyright (C) 1999-2002 00005 * David Corcoran <corcoran@linuxnet.com> 00006 * Copyright (C) 2002-2010 00007 * Ludovic Rousseau <ludovic.rousseau@free.fr> 00008 * 00009 * $Id: pcscdaemon.c 5071 2010-07-26 13:33:56Z rousseau $ 00010 */ 00011 00021 #include "config.h" 00022 #include <time.h> 00023 #include <signal.h> 00024 #include <sys/types.h> 00025 #include <sys/stat.h> 00026 #include <fcntl.h> 00027 #include <errno.h> 00028 #include <stdio.h> 00029 #include <unistd.h> 00030 #include <stdlib.h> 00031 #include <string.h> 00032 #ifdef HAVE_GETOPT_H 00033 #include <getopt.h> 00034 #endif 00035 00036 #include "misc.h" 00037 #include "pcsclite.h" 00038 #include "pcscd.h" 00039 #include "debuglog.h" 00040 #include "winscard_msg.h" 00041 #include "winscard_svc.h" 00042 #include "sys_generic.h" 00043 #include "hotplug.h" 00044 #include "readerfactory.h" 00045 #include "configfile.h" 00046 #include "powermgt_generic.h" 00047 #include "utils.h" 00048 00049 #ifndef TRUE 00050 #define TRUE 1 00051 #define FALSE 0 00052 #endif 00053 00054 char AraKiri = FALSE; 00055 static char Init = TRUE; 00056 char AutoExit = FALSE; 00057 static int ExitValue = EXIT_FAILURE; 00058 int HPForceReaderPolling = 0; 00059 static int pipefd[] = {-1, -1}; 00060 00061 /* 00062 * Some internal functions 00063 */ 00064 static void at_exit(void); 00065 static void clean_temp_files(void); 00066 static void signal_reload(int sig); 00067 static void signal_trap(int); 00068 static void print_version (void); 00069 static void print_usage (char const * const); 00070 00079 static void SVCServiceRunLoop(void) 00080 { 00081 int rsp; 00082 LONG rv; 00083 uint32_t dwClientID; /* Connection ID used to reference the Client */ 00084 00085 rv = 0; 00086 00087 while (TRUE) 00088 { 00089 switch (rsp = ProcessEventsServer(&dwClientID)) 00090 { 00091 00092 case 0: 00093 Log2(PCSC_LOG_DEBUG, "A new context thread creation is requested: %d", dwClientID); 00094 rv = CreateContextThread(&dwClientID); 00095 00096 if (rv != SCARD_S_SUCCESS) 00097 Log1(PCSC_LOG_ERROR, "Problem during the context thread creation"); 00098 break; 00099 00100 case 2: 00101 /* 00102 * timeout in ProcessEventsServer(): do nothing 00103 * this is used to catch the Ctrl-C signal at some time when 00104 * nothing else happens 00105 */ 00106 break; 00107 00108 case -1: 00109 Log1(PCSC_LOG_ERROR, "Error in ProcessEventsServer"); 00110 break; 00111 00112 case -2: 00113 /* Nothing to do in case of a syscall interrupted 00114 * It happens when SIGUSR1 (reload) or SIGINT (Ctrl-C) is received 00115 * We just try again */ 00116 break; 00117 00118 default: 00119 Log2(PCSC_LOG_ERROR, "ProcessEventsServer unknown retval: %d", 00120 rsp); 00121 break; 00122 } 00123 00124 if (AraKiri) 00125 { 00126 /* stop the hotpug thread and waits its exit */ 00127 #ifdef USE_USB 00128 (void)HPStopHotPluggables(); 00129 #endif 00130 (void)SYS_Sleep(1); 00131 00132 /* now stop all the drivers */ 00133 RFCleanupReaders(); 00134 ContextsDeinitialize(); 00135 at_exit(); 00136 } 00137 } 00138 } 00139 00140 int main(int argc, char **argv) 00141 { 00142 int rv; 00143 char setToForeground; 00144 char HotPlug; 00145 char *newReaderConfig; 00146 struct stat fStatBuf; 00147 int customMaxThreadCounter = 0; 00148 int customMaxReaderHandles = 0; 00149 int customMaxThreadCardHandles = 0; 00150 int opt; 00151 #ifdef HAVE_GETOPT_LONG 00152 int option_index = 0; 00153 static struct option long_options[] = { 00154 {"config", 1, NULL, 'c'}, 00155 {"foreground", 0, NULL, 'f'}, 00156 {"help", 0, NULL, 'h'}, 00157 {"version", 0, NULL, 'v'}, 00158 {"apdu", 0, NULL, 'a'}, 00159 {"debug", 0, NULL, 'd'}, 00160 {"info", 0, NULL, 0}, 00161 {"error", 0, NULL, 'e'}, 00162 {"critical", 0, NULL, 'C'}, 00163 {"hotplug", 0, NULL, 'H'}, 00164 {"force-reader-polling", optional_argument, NULL, 0}, 00165 {"max-thread", 1, NULL, 't'}, 00166 {"max-card-handle-per-thread", 1, NULL, 's'}, 00167 {"max-card-handle-per-reader", 1, NULL, 'r'}, 00168 {"auto-exit", 0, NULL, 'x'}, 00169 {NULL, 0, NULL, 0} 00170 }; 00171 #endif 00172 #define OPT_STRING "c:fdhvaeCHt:r:s:x" 00173 00174 rv = 0; 00175 newReaderConfig = NULL; 00176 setToForeground = FALSE; 00177 HotPlug = FALSE; 00178 00179 /* 00180 * test the version 00181 */ 00182 if (strcmp(PCSCLITE_VERSION_NUMBER, VERSION) != 0) 00183 { 00184 printf("BUILD ERROR: The release version number PCSCLITE_VERSION_NUMBER\n"); 00185 printf(" in pcsclite.h (%s) does not match the release version number\n", 00186 PCSCLITE_VERSION_NUMBER); 00187 printf(" generated in config.h (%s) (see configure.in).\n", VERSION); 00188 00189 return EXIT_FAILURE; 00190 } 00191 00192 /* 00193 * By default we create a daemon (not connected to any output) 00194 * so log to syslog to have error messages. 00195 */ 00196 DebugLogSetLogType(DEBUGLOG_SYSLOG_DEBUG); 00197 00198 /* 00199 * Handle any command line arguments 00200 */ 00201 #ifdef HAVE_GETOPT_LONG 00202 while ((opt = getopt_long (argc, argv, OPT_STRING, long_options, &option_index)) != -1) { 00203 #else 00204 while ((opt = getopt (argc, argv, OPT_STRING)) != -1) { 00205 #endif 00206 switch (opt) { 00207 #ifdef HAVE_GETOPT_LONG 00208 case 0: 00209 if (strcmp(long_options[option_index].name, 00210 "force-reader-polling") == 0) 00211 HPForceReaderPolling = optarg ? abs(atoi(optarg)) : 1; 00212 break; 00213 #endif 00214 case 'c': 00215 Log2(PCSC_LOG_INFO, "using new config file: %s", optarg); 00216 newReaderConfig = optarg; 00217 break; 00218 00219 case 'f': 00220 setToForeground = TRUE; 00221 /* debug to stderr instead of default syslog */ 00222 DebugLogSetLogType(DEBUGLOG_STDERR_DEBUG); 00223 Log1(PCSC_LOG_INFO, 00224 "pcscd set to foreground with debug send to stderr"); 00225 break; 00226 00227 case 'd': 00228 DebugLogSetLevel(PCSC_LOG_DEBUG); 00229 break; 00230 00231 case 'e': 00232 DebugLogSetLevel(PCSC_LOG_ERROR); 00233 break; 00234 00235 case 'C': 00236 DebugLogSetLevel(PCSC_LOG_CRITICAL); 00237 break; 00238 00239 case 'h': 00240 print_usage (argv[0]); 00241 return EXIT_SUCCESS; 00242 00243 case 'v': 00244 print_version (); 00245 return EXIT_SUCCESS; 00246 00247 case 'a': 00248 (void)DebugLogSetCategory(DEBUG_CATEGORY_APDU); 00249 break; 00250 00251 case 'H': 00252 /* debug to stderr instead of default syslog */ 00253 DebugLogSetLogType(DEBUGLOG_STDERR_DEBUG); 00254 HotPlug = TRUE; 00255 break; 00256 00257 case 't': 00258 customMaxThreadCounter = optarg ? atoi(optarg) : 0; 00259 Log2(PCSC_LOG_INFO, "setting customMaxThreadCounter to: %d", 00260 customMaxThreadCounter); 00261 break; 00262 00263 case 'r': 00264 customMaxReaderHandles = optarg ? atoi(optarg) : 0; 00265 Log2(PCSC_LOG_INFO, "setting customMaxReaderHandles to: %d", 00266 customMaxReaderHandles); 00267 break; 00268 00269 case 's': 00270 customMaxThreadCardHandles = optarg ? atoi(optarg) : 0; 00271 Log2(PCSC_LOG_INFO, "setting customMaxThreadCardHandles to: %d", 00272 customMaxThreadCardHandles); 00273 break; 00274 00275 case 'x': 00276 AutoExit = TRUE; 00277 Log2(PCSC_LOG_INFO, "Auto exit after %d seconds of inactivity", 00278 TIME_BEFORE_SUICIDE); 00279 break; 00280 00281 default: 00282 print_usage (argv[0]); 00283 return EXIT_FAILURE; 00284 } 00285 00286 } 00287 00288 if (argv[optind]) 00289 { 00290 printf("Unknown option: %s\n", argv[optind]); 00291 print_usage(argv[0]); 00292 return EXIT_FAILURE; 00293 } 00294 00295 /* 00296 * test the presence of /var/run/pcscd/pcscd.comm 00297 */ 00298 00299 rv = stat(PCSCLITE_CSOCK_NAME, &fStatBuf); 00300 00301 if (rv == 0) 00302 { 00303 pid_t pid; 00304 00305 /* read the pid file to get the old pid and test if the old pcscd is 00306 * still running 00307 */ 00308 pid = GetDaemonPid(); 00309 00310 if (pid != -1) 00311 { 00312 if (HotPlug) 00313 return SendHotplugSignal(); 00314 00315 rv = kill(pid, 0); 00316 if (0 == rv) 00317 { 00318 Log1(PCSC_LOG_CRITICAL, 00319 "file " PCSCLITE_CSOCK_NAME " already exists."); 00320 Log2(PCSC_LOG_CRITICAL, 00321 "Another pcscd (pid: %d) seems to be running.", pid); 00322 return EXIT_FAILURE; 00323 } 00324 else 00325 if (ESRCH == errno) 00326 { 00327 /* the old pcscd is dead. make some cleanup */ 00328 clean_temp_files(); 00329 } 00330 else 00331 { 00332 /* permission denied or other error */ 00333 Log2(PCSC_LOG_CRITICAL, "kill failed: %s", strerror(errno)); 00334 return EXIT_FAILURE; 00335 } 00336 } 00337 else 00338 { 00339 if (HotPlug) 00340 { 00341 Log1(PCSC_LOG_CRITICAL, "file " PCSCLITE_RUN_PID " do not exist"); 00342 Log1(PCSC_LOG_CRITICAL, "Hotplug failed"); 00343 return EXIT_FAILURE; 00344 } 00345 00346 Log1(PCSC_LOG_CRITICAL, 00347 "file " PCSCLITE_CSOCK_NAME " already exists."); 00348 Log1(PCSC_LOG_CRITICAL, 00349 "Maybe another pcscd is running?"); 00350 Log1(PCSC_LOG_CRITICAL, 00351 "I can't read process pid from " PCSCLITE_RUN_PID); 00352 Log1(PCSC_LOG_CRITICAL, "Remove " PCSCLITE_CSOCK_NAME); 00353 Log1(PCSC_LOG_CRITICAL, 00354 "if pcscd is not running to clear this message."); 00355 return EXIT_FAILURE; 00356 } 00357 } 00358 else 00359 if (HotPlug) 00360 { 00361 Log1(PCSC_LOG_CRITICAL, "Hotplug failed: pcscd is not running"); 00362 return EXIT_FAILURE; 00363 } 00364 00365 /* like in daemon(3): changes the current working directory to the 00366 * root ("/") */ 00367 (void)chdir("/"); 00368 00369 if (AutoExit) 00370 { 00371 int pid; 00372 00373 /* create a new session so that Ctrl-C on the application will 00374 * not also quit pcscd */ 00375 setsid(); 00376 00377 /* fork() so that pcscd always return in --auto-exit mode */ 00378 pid = fork(); 00379 if (-1 == pid ) 00380 Log2(PCSC_LOG_CRITICAL, "fork() failed: %s", strerror(errno)); 00381 00382 if (pid) 00383 /* father */ 00384 return EXIT_SUCCESS; 00385 } 00386 00387 /* 00388 * If this is set to one the user has asked it not to fork 00389 */ 00390 if (!setToForeground) 00391 { 00392 int pid; 00393 00394 if (pipe(pipefd) == -1) 00395 { 00396 Log2(PCSC_LOG_CRITICAL, "pipe() failed: %s", strerror(errno)); 00397 return EXIT_FAILURE; 00398 } 00399 00400 pid = fork(); 00401 if (-1 == pid) 00402 { 00403 Log2(PCSC_LOG_CRITICAL, "fork() failed: %s", strerror(errno)); 00404 return EXIT_FAILURE; 00405 } 00406 00407 /* like in daemon(3): redirect standard input, standard output 00408 * and standard error to /dev/null */ 00409 (void)close(0); 00410 (void)close(1); 00411 (void)close(2); 00412 00413 if (pid) 00414 /* in the father */ 00415 { 00416 char buf; 00417 int ret; 00418 00419 /* close write side */ 00420 close(pipefd[1]); 00421 00422 /* wait for the son to write the return code */ 00423 ret = read(pipefd[0], &buf, 1); 00424 if (ret <= 0) 00425 return 2; 00426 00427 close(pipefd[0]); 00428 00429 /* exit code */ 00430 return buf; 00431 } 00432 else 00433 /* in the son */ 00434 { 00435 /* close read side */ 00436 close(pipefd[0]); 00437 } 00438 } 00439 00440 /* 00441 * cleanly remove /var/run/pcscd/files when exiting 00442 * signal_trap() does just set a global variable used by the main loop 00443 */ 00444 (void)signal(SIGQUIT, signal_trap); 00445 (void)signal(SIGTERM, signal_trap); 00446 (void)signal(SIGINT, signal_trap); 00447 00448 /* exits on SIGALARM to allow pcscd to suicide if not used */ 00449 (void)signal(SIGALRM, signal_trap); 00450 00451 /* 00452 * If PCSCLITE_IPC_DIR does not exist then create it 00453 */ 00454 rv = stat(PCSCLITE_IPC_DIR, &fStatBuf); 00455 if (rv < 0) 00456 { 00457 int mode = S_IROTH | S_IXOTH | S_IRGRP | S_IXGRP | S_IRWXU; 00458 00459 rv = mkdir(PCSCLITE_IPC_DIR, mode); 00460 if (rv != 0) 00461 { 00462 Log2(PCSC_LOG_CRITICAL, 00463 "cannot create " PCSCLITE_IPC_DIR ": %s", strerror(errno)); 00464 return EXIT_FAILURE; 00465 } 00466 00467 /* set mode so that the directory is world readable and 00468 * executable even is umask is restrictive 00469 * The directory containes files used by libpcsclite */ 00470 (void)chmod(PCSCLITE_IPC_DIR, mode); 00471 } 00472 00473 /* 00474 * Record our pid to make it easier 00475 * to kill the correct pcscd 00476 */ 00477 { 00478 int f; 00479 int mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; 00480 00481 f = open(PCSCLITE_RUN_PID, O_RDWR | O_CREAT, mode); 00482 if (f != -1) 00483 { 00484 char pid[PID_ASCII_SIZE]; 00485 00486 (void)snprintf(pid, sizeof(pid), "%u\n", (unsigned) getpid()); 00487 (void)write(f, pid, strlen(pid)); 00488 (void)close(f); 00489 00490 /* set mode so that the file is world readable even is umask is 00491 * restrictive 00492 * The file is used by libpcsclite */ 00493 (void)chmod(PCSCLITE_RUN_PID, mode); 00494 } 00495 else 00496 Log2(PCSC_LOG_CRITICAL, "cannot create " PCSCLITE_RUN_PID ": %s", 00497 strerror(errno)); 00498 } 00499 00500 /* cleanly remove /var/run/pcscd/pcsc.* files when exiting */ 00501 if (atexit(at_exit)) 00502 Log2(PCSC_LOG_CRITICAL, "atexit() failed: %s", strerror(errno)); 00503 00504 /* 00505 * Allocate memory for reader structures 00506 */ 00507 rv = RFAllocateReaderSpace(customMaxReaderHandles); 00508 if (SCARD_S_SUCCESS != rv) 00509 at_exit(); 00510 00511 #ifdef USE_SERIAL 00512 /* 00513 * Grab the information from the reader.conf 00514 */ 00515 if (newReaderConfig) 00516 { 00517 rv = RFStartSerialReaders(newReaderConfig); 00518 if (rv != 0) 00519 { 00520 Log3(PCSC_LOG_CRITICAL, "invalid file %s: %s", newReaderConfig, 00521 strerror(errno)); 00522 at_exit(); 00523 } 00524 } 00525 else 00526 { 00527 rv = RFStartSerialReaders(PCSCLITE_CONFIG_DIR); 00528 if (rv == -1) 00529 at_exit(); 00530 } 00531 #endif 00532 00533 Log1(PCSC_LOG_INFO, "pcsc-lite " VERSION " daemon ready."); 00534 00535 /* 00536 * post initialistion 00537 */ 00538 Init = FALSE; 00539 00540 /* 00541 * Hotplug rescan 00542 */ 00543 (void)signal(SIGUSR1, signal_reload); 00544 00545 /* 00546 * Initialize the comm structure 00547 */ 00548 rv = InitializeSocket(); 00549 if (rv) 00550 { 00551 Log1(PCSC_LOG_CRITICAL, "Error initializing pcscd."); 00552 at_exit(); 00553 } 00554 00555 /* 00556 * Initialize the contexts structure 00557 */ 00558 rv = ContextsInitialize(customMaxThreadCounter, customMaxThreadCardHandles); 00559 00560 if (rv == -1) 00561 { 00562 Log1(PCSC_LOG_CRITICAL, "Error initializing pcscd."); 00563 at_exit(); 00564 } 00565 00566 (void)signal(SIGPIPE, SIG_IGN); 00567 (void)signal(SIGHUP, SIG_IGN); /* needed for Solaris. The signal is sent 00568 * when the shell is existed */ 00569 00570 #if !defined(PCSCLITE_STATIC_DRIVER) && defined(USE_USB) 00571 /* 00572 * Set up the search for USB/PCMCIA devices 00573 */ 00574 rv = HPSearchHotPluggables(); 00575 if (rv) 00576 at_exit(); 00577 00578 rv = HPRegisterForHotplugEvents(); 00579 if (rv) 00580 at_exit(); 00581 #endif 00582 00583 /* 00584 * Set up the power management callback routine 00585 */ 00586 (void)PMRegisterForPowerEvents(); 00587 00588 /* initialisation succeeded */ 00589 if (pipefd[1] >= 0) 00590 { 00591 char buf = 0; 00592 00593 /* write a 0 (success) to father process */ 00594 write(pipefd[1], &buf, 1); 00595 close(pipefd[1]); 00596 } 00597 00598 SVCServiceRunLoop(); 00599 00600 Log1(PCSC_LOG_ERROR, "SVCServiceRunLoop returned"); 00601 return EXIT_FAILURE; 00602 } 00603 00604 static void at_exit(void) 00605 { 00606 Log1(PCSC_LOG_INFO, "cleaning " PCSCLITE_IPC_DIR); 00607 00608 clean_temp_files(); 00609 00610 if (pipefd[1] >= 0) 00611 { 00612 char buf; 00613 00614 /* write the error code to father process */ 00615 buf = ExitValue; 00616 write(pipefd[1], &buf, 1); 00617 close(pipefd[1]); 00618 } 00619 00620 _exit(ExitValue); 00621 } 00622 00623 static void clean_temp_files(void) 00624 { 00625 int rv; 00626 00627 rv = remove(PCSCLITE_CSOCK_NAME); 00628 if (rv != 0) 00629 Log2(PCSC_LOG_ERROR, "Cannot remove " PCSCLITE_CSOCK_NAME ": %s", 00630 strerror(errno)); 00631 00632 rv = remove(PCSCLITE_RUN_PID); 00633 if (rv != 0) 00634 Log2(PCSC_LOG_ERROR, "Cannot remove " PCSCLITE_RUN_PID ": %s", 00635 strerror(errno)); 00636 } 00637 00638 static void signal_reload(/*@unused@*/ int sig) 00639 { 00640 (void)signal(SIGUSR1, signal_reload); 00641 00642 (void)sig; 00643 00644 if (AraKiri) 00645 return; 00646 00647 #ifdef USE_USB 00648 HPReCheckSerialReaders(); 00649 #endif 00650 } /* signal_reload */ 00651 00652 static void signal_trap(int sig) 00653 { 00654 Log2(PCSC_LOG_INFO, "Received signal: %d", sig); 00655 00656 /* the signal handler is called several times for the same Ctrl-C */ 00657 if (AraKiri == FALSE) 00658 { 00659 Log1(PCSC_LOG_INFO, "Preparing for suicide"); 00660 AraKiri = TRUE; 00661 00662 /* if still in the init/loading phase the AraKiri will not be 00663 * seen by the main event loop 00664 */ 00665 if (Init) 00666 { 00667 Log1(PCSC_LOG_INFO, "Suicide during init"); 00668 at_exit(); 00669 } 00670 } 00671 else 00672 { 00673 /* if pcscd do not want to die */ 00674 static int lives = 2; 00675 00676 lives--; 00677 /* no live left. Something is blocking the normal death. */ 00678 if (0 == lives) 00679 { 00680 Log1(PCSC_LOG_INFO, "Forced suicide"); 00681 at_exit(); 00682 } 00683 } 00684 } 00685 00686 static void print_version (void) 00687 { 00688 printf("%s version %s.\n", PACKAGE, VERSION); 00689 printf("Copyright (C) 1999-2002 by David Corcoran <corcoran@linuxnet.com>.\n"); 00690 printf("Copyright (C) 2001-2010 by Ludovic Rousseau <ludovic.rousseau@free.fr>.\n"); 00691 printf("Copyright (C) 2003-2004 by Damien Sauveron <sauveron@labri.fr>.\n"); 00692 printf("Report bugs to <muscle@lists.musclecard.com>.\n"); 00693 00694 printf ("Enabled features:%s\n", PCSCLITE_FEATURES); 00695 } 00696 00697 static void print_usage (char const * const progname) 00698 { 00699 printf("Usage: %s options\n", progname); 00700 printf("Options:\n"); 00701 #ifdef HAVE_GETOPT_LONG 00702 printf(" -a, --apdu log APDU commands and results\n"); 00703 printf(" -c, --config path to reader.conf\n"); 00704 printf(" -f, --foreground run in foreground (no daemon),\n"); 00705 printf(" send logs to stderr instead of syslog\n"); 00706 printf(" -h, --help display usage information\n"); 00707 printf(" -H, --hotplug ask the daemon to rescan the available readers\n"); 00708 printf(" -v, --version display the program version number\n"); 00709 printf(" -d, --debug display lower level debug messages\n"); 00710 printf(" --info display info level debug messages (default level)\n"); 00711 printf(" -e --error display error level debug messages\n"); 00712 printf(" -C --critical display critical only level debug messages\n"); 00713 printf(" --force-reader-polling ignore the IFD_GENERATE_HOTPLUG reader capability\n"); 00714 printf(" -t, --max-thread maximum number of threads (default %d)\n", PCSC_MAX_CONTEXT_THREADS); 00715 printf(" -s, --max-card-handle-per-thread maximum number of card handle per thread (default: %d)\n", PCSC_MAX_CONTEXT_CARD_HANDLES); 00716 printf(" -r, --max-card-handle-per-reader maximum number of card handle per reader (default: %d)\n", PCSC_MAX_READER_HANDLES); 00717 #else 00718 printf(" -a log APDU commands and results\n"); 00719 printf(" -c path to reader.conf\n"); 00720 printf(" -f run in foreground (no daemon), send logs to stderr instead of syslog\n"); 00721 printf(" -d display debug messages. Output may be:\n"); 00722 printf(" -h display usage information\n"); 00723 printf(" -H ask the daemon to rescan the available readers\n"); 00724 printf(" -v display the program version number\n"); 00725 printf(" -t maximum number of threads\n"); 00726 printf(" -s maximum number of card handle per thread\n"); 00727 printf(" -r maximum number of card handle per reader\n"); 00728 #endif 00729 } 00730