pcscdaemon.c

Go to the documentation of this file.
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