source: ntrip/trunk/ntripserver/NtripLinuxServer.c@ 24

Last change on this file since 24 was 24, checked in by stoecker, 19 years ago

some UDP fixes

File size: 20.1 KB
Line 
1/*
2 * NtripServerLinux.c
3 *
4 * Copyright (c) 2003...2005
5 * German Federal Agency for Cartography and Geodesy (BKG)
6 *
7 * Developed for Networked Transport of RTCM via Internet Protocol (NTRIP)
8 * for streaming GNSS data over the Internet.
9 *
10 * Designed by Informatik Centrum Dortmund http://www.icd.de
11 *
12 * NTRIP is currently an experimental technology.
13 * The BKG disclaims any liability nor responsibility to any person or
14 * entity with respect to any loss or damage caused, or alleged to be
15 * caused, directly or indirectly by the use and application of the NTRIP
16 * technology.
17 *
18 * For latest information and updates, access:
19 * http://igs.ifag.de/index_ntrip.htm
20 *
21 * Georg Weber
22 * BKG, Frankfurt, Germany, June 2003-06-13
23 * E-mail: euref-ip@bkg.bund.de
24 *
25 * Based on the GNU General Public License published nmead
26 *
27 * This program is free software; you can redistribute it and/or
28 * modify it under the terms of the GNU General Public License
29 * as published by the Free Software Foundation; either version 2
30 * of the License, or (at your option) any later version.
31 *
32 * This program is distributed in the hope that it will be useful,
33 * but WITHOUT ANY WARRANTY; without even the implied warranty of
34 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
35 * GNU General Public License for more details.
36 *
37 * You should have received a copy of the GNU General Public License
38 * along with this program; if not, write to the Free Software
39 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
40 * USA.
41 */
42
43/* $Id: NtripLinuxServer.c,v 1.12 2005/06/02 09:33:11 stoecker Exp $
44 * Changes - Version 0.7
45 * Sep 22 2003 Steffen Tschirpke <St.Tschirpke@actina.de>
46 * - socket support
47 * - command line option handling
48 * - error handling
49 * - help screen
50 *
51 * Changes - Version 0.9
52 * Feb 15 2005 Dirk Stoecker <soft@dstoecker.de>
53 * - some minor updates, fixed serial baudrate settings
54 *
55 * Changes - Version 0.10
56 * Apr 05 2005 Dirk Stoecker <soft@dstoecker.de>
57 * - some cleanup and miscellaneous fixes
58 * - replaced non-working simulate with file input (stdin)
59 * - TCP sending now somewhat more stable
60 * - cleanup of error handling
61 * - Modes may be symbolic and not only numeric
62 *
63 * Changes - Version 0.11
64 * Jun 02 2005 Dirk Stoecker <soft@dstoecker.de>
65 * - added SISNeT support
66 * - added UDP support
67 * - cleanup of host and port handling
68 * - added inactivity alarm of 60 seconds
69 *
70 * Changes - Version 0.12
71 * Jun 07 2005 Dirk Stoecker <soft@dstoecker.de>
72 * - added UDP bindmode
73 */
74
75#include <ctype.h>
76#include <errno.h>
77#include <fcntl.h>
78#include <getopt.h>
79#include <netdb.h>
80#include <signal.h>
81#include <stdio.h>
82#include <stdlib.h>
83#include <string.h>
84#include <unistd.h>
85#include <arpa/inet.h>
86#include <netinet/in.h>
87#include <sys/socket.h>
88#include <sys/termios.h>
89#include <sys/types.h>
90
91#ifndef MSG_DONTWAIT
92#define MSG_DONTWAIT 0 /* prevent compiler errors */
93#endif
94#ifndef O_EXLOCK
95#define O_EXLOCK 0 /* prevent compiler errors */
96#endif
97
98enum MODE { SERIAL = 1, TCPSOCKET = 2, INFILE = 3, SISNET = 4, UDPSOCKET = 5 };
99
100#define VERSION "NTRIP NtripServerLinux/0.12"
101#define BUFSZ 1024
102
103/* default socket source */
104#define SERV_HOST_ADDR "127.0.0.1"
105#define SERV_TCP_PORT 1025
106
107/* default destination */
108#define NTRIP_CASTER "www.euref-ip.net"
109#define NTRIP_PORT 80
110
111/* default sisnet source */
112#define SISNET_SERVER "131.176.49.142"
113#define SISNET_PORT 7777
114
115#define ALARMTIME 60
116
117static int ttybaud = 19200;
118static const char *ttyport = "/dev/gps";
119static const char *filepath = "/dev/stdin";
120static enum MODE mode = INFILE;
121static int sisnetv3 = 0;
122static int gpsfd = -1;
123
124/* Forward references */
125static int openserial(const char * tty, int blocksz, int baud);
126static void send_receive_loop(int sock, int fd, int sisnet);
127static void usage(int);
128
129static void sighandler_alarm(/*int arg*/)
130{
131 fprintf(stderr, "ERROR: more than %d seconds no activity\n", ALARMTIME);
132 exit(1);
133}
134
135/*
136* main
137*
138* Main entry point for the program. Processes command-line arguments and
139* prepares for action.
140*
141* Parameters:
142* argc : integer : Number of command-line arguments.
143* argv : array of char : Command-line arguments as an array of zero-terminated
144* pointers to strings.
145*
146* Return Value:
147* The function does not return a value (although its return type is int).
148*
149* Remarks:
150*
151*/
152
153int main(int argc, char **argv)
154{
155 int c;
156 int size = 2048; /* for setting send buffer size */
157
158 const char *inhost = 0;
159 const char *outhost = 0;
160 unsigned int outport = 0;
161 unsigned int inport = 0;
162 const char *mountpoint = NULL;
163 const char *password = "";
164 const char *sisnetpassword = "";
165 const char *sisnetuser = "";
166 const char *initfile = NULL;
167 int bindmode = 0;
168 int sock_id;
169 char szSendBuffer[BUFSZ];
170 int nBufferBytes;
171 struct hostent *he;
172 struct sockaddr_in addr;
173
174 signal(SIGALRM,sighandler_alarm);
175 alarm(ALARMTIME);
176 /* get and check program arguments */
177 if(argc <= 1)
178 {
179 usage(2);
180 exit(1);
181 }
182 while((c = getopt(argc, argv, "M:i:h:b:p:s:a:m:c:H:P:f:l:u:V:B")) != EOF)
183 {
184 switch (c)
185 {
186 case 'M':
187 if(!strcmp(optarg, "serial")) mode = 1;
188 else if(!strcmp(optarg, "tcpsocket")) mode = 2;
189 else if(!strcmp(optarg, "file")) mode = 3;
190 else if(!strcmp(optarg, "sisnet")) mode = 4;
191 else if(!strcmp(optarg, "udpsocket")) mode = 5;
192 else mode = atoi(optarg);
193 if((mode == 0) || (mode > 5))
194 {
195 fprintf(stderr, "ERROR: can't convert %s to a valid mode\n", optarg);
196 usage(-1);
197 }
198 break;
199 case 'i': /* gps serial ttyport */
200 ttyport = optarg;
201 break;
202 case 'B':
203 bindmode = 1;
204 break;
205 case 'V':
206 if(!strcmp("3.0", optarg)) sisnetv3 = 1;
207 else if(strcmp("2.1", optarg))
208 {
209 fprintf(stderr, "ERROR: unknown SISNeT version %s\n", optarg);
210 usage(-2);
211 }
212 case 'b': /* serial ttyin speed */
213 ttybaud = atoi(optarg);
214 if(ttybaud <= 1)
215 {
216 fprintf(stderr, "ERROR: can't convert %s to valid serial speed\n", optarg);
217 usage(1);
218 }
219 break;
220 case 'a': /* http server IP address A.B.C.D */
221 outhost = optarg;
222 break;
223 case 'p': /* http server port */
224 outport = atoi(optarg);
225 if(outport <= 1 || outport > 65535)
226 {
227 fprintf(stderr, "ERROR: can't convert %s to a valid HTTP server port\n",
228 optarg);
229 usage(1);
230 }
231 break;
232 case 'm': /* http server mountpoint */
233 mountpoint = optarg;
234 break;
235 case 's': /* datastream from file */
236 filepath = optarg;
237 break;
238 case 'f':
239 initfile = optarg;
240 break;
241 case 'u':
242 sisnetuser = optarg;
243 break;
244 case 'l':
245 sisnetpassword = optarg;
246 break;
247 case 'c': /* password */
248 password = optarg;
249 break;
250 case 'H': /* host */
251 inhost = optarg;
252 break;
253 case 'P': /* port */
254 inport = atoi(optarg);
255 if(inport <= 1 || inport > 65535)
256 {
257 fprintf(stderr, "ERROR: can't convert %s to a valid port number\n",
258 optarg);
259 usage(1);
260 }
261 break;
262 case 'h': /* help */
263 case '?':
264 usage(0);
265 break;
266 default:
267 usage(2);
268 break;
269 }
270 }
271
272 argc -= optind;
273 argv += optind;
274
275 if(argc > 0)
276 {
277 fprintf(stderr, "ERROR: Extra args on command line: ");
278 for(; argc > 0; argc--)
279 {
280 fprintf(stderr, " %s", *argv++);
281 }
282 fprintf(stderr, "\n");
283 usage(1); /* never returns */
284 }
285
286 if(mountpoint == NULL)
287 {
288 fprintf(stderr, "ERROR: Missing mountpoint argument\n");
289 exit(1);
290 }
291 if(!password[0])
292 {
293 fprintf(stderr, "WARNING: Missing password argument - are you really sure?\n");
294 }
295
296 if(!outhost) outhost = NTRIP_CASTER;
297 if(!outport) outport = NTRIP_PORT;
298
299 switch(mode)
300 {
301 case INFILE:
302 {
303 gpsfd = open(filepath, O_RDONLY);
304 if(!gpsfd)
305 {
306 perror("ERROR: opening input file");
307 exit(1);
308 }
309 /* set blocking mode in case it was not set (seems to be sometimes for fifo's) */
310 fcntl(gpsfd, F_SETFL, 0);
311 printf("file input: file = %s\n", filepath);
312 }
313 break;
314 case SERIAL: /* open serial port */
315 {
316 gpsfd = openserial(ttyport, 1, ttybaud);
317 if(gpsfd < 0)
318 {
319 exit(1);
320 }
321 printf("serial input: device = %s, speed = %d\n", ttyport, ttybaud);
322 }
323 break;
324 case TCPSOCKET: case UDPSOCKET: case SISNET:
325 {
326 if(mode == SISNET)
327 {
328 if(!inhost) inhost = SISNET_SERVER;
329 if(!inport) inport = SISNET_PORT;
330 }
331 else
332 {
333 if(!inport) inport = SERV_TCP_PORT;
334 if(!inhost) inhost = "127.0.0.1";
335 }
336
337 if(!(he = gethostbyname(inhost)))
338 {
339 fprintf(stderr, "ERROR: host %s unknown\n", inhost);
340 usage(-2);
341 }
342
343 if((gpsfd = socket(AF_INET, mode == UDPSOCKET ? SOCK_DGRAM : SOCK_STREAM, 0)) < 0)
344 {
345 fprintf(stderr, "ERROR: can't create socket\n");
346 exit(1);
347 }
348
349 memset((char *) &addr, 0x00, sizeof(addr));
350 if(!bindmode)
351 memcpy(&addr.sin_addr, he->h_addr, (size_t)he->h_length);
352 addr.sin_family = AF_INET;
353 addr.sin_port = htons(inport);
354
355 printf("%s input: host = %s, port = %d%s%s%s\n", mode == SISNET ? "sisnet"
356 : mode == TCPSOCKET ? "tcp socket" : "udp socket",
357 bindmode ? "127.0.0.1" : inet_ntoa(addr.sin_addr), inport,
358 initfile ? ", initfile = " : "",
359 initfile ? initfile : "", bindmode ? " binding mode" : "");
360
361 if(bindmode)
362 {
363 if(bind(gpsfd, (struct sockaddr *) &addr, sizeof(addr)) < 0)
364 {
365 fprintf(stderr, "ERROR: can't bind input to port %d\n", inport);
366 exit(1);
367 }
368 }
369 else if(connect(gpsfd, (struct sockaddr *) &addr, sizeof(addr)) < 0)
370 {
371 fprintf(stderr, "ERROR: can't connect input to %s at port %d\n",
372 inet_ntoa(addr.sin_addr), inport);
373 exit(1);
374 }
375 if(initfile && mode != SISNET)
376 {
377 char buffer[1024];
378 FILE *fh;
379 int i;
380
381 if((fh = fopen(initfile, "r")))
382 {
383 while((i = fread(buffer, 1, sizeof(buffer), fh)) > 0)
384 {
385 if((send(gpsfd, buffer, (size_t)i, 0)) != i)
386 {
387 perror("ERROR: sending init file");
388 exit(1);
389 }
390 }
391 if(i < 0)
392 {
393 perror("ERROR: reading init file");
394 exit(1);
395 }
396 fclose(fh);
397 }
398 else
399 {
400 fprintf(stderr, "ERROR: can't read init file %s\n", initfile);
401 exit(1);
402 }
403 }
404 }
405 if(mode == SISNET)
406 {
407 int i, j;
408 char buffer[1024];
409
410 i = snprintf(buffer, sizeof(buffer), sisnetv3 ? "AUTH,%s,%s\r\n" : "AUTH,%s,%s",
411 sisnetuser,sisnetpassword);
412 if((send(gpsfd, buffer, (size_t)i, 0)) != i)
413 {
414 perror("ERROR: sending authentication");
415 exit(1);
416 }
417 i = sisnetv3 ? 7 : 5;
418 if((j = recv(gpsfd, buffer, i, 0)) != i && strncmp("*AUTH", buffer, 5))
419 {
420 fprintf(stderr, "ERROR: SISNeT connect failed:");
421 for(i = 0; i < j; ++i)
422 {
423 if(buffer[i] != '\r' && buffer[i] != '\n')
424 {
425 fprintf(stderr, "%c", isprint(buffer[i]) ? buffer[i] : '.');
426 }
427 }
428 fprintf(stderr, "\n");
429 exit(1);
430 }
431 }
432 break;
433 default:
434 usage(-1);
435 break;
436 }
437
438 /* ----- main part ----- */
439 while(1)
440 {
441 if(!(he = gethostbyname(outhost)))
442 {
443 fprintf(stderr, "ERROR: host %s unknown\n", outhost);
444 usage(-2);
445 }
446
447 /* create socket */
448 if((sock_id = socket(AF_INET, SOCK_STREAM, 0)) < 0)
449 {
450 fprintf(stderr, "ERROR: could not create socket\n");
451 exit(2);
452 }
453
454 memset((char *) &addr, 0x00, sizeof(addr));
455 memcpy(&addr.sin_addr, he->h_addr, (size_t)he->h_length);
456 addr.sin_family = AF_INET;
457 addr.sin_port = htons(outport);
458
459 /* connect to caster */
460 fprintf(stderr, "caster output: host = %s, port = %d, mountpoint = %s\n",
461 inet_ntoa(addr.sin_addr), outport, mountpoint);
462 if(connect(sock_id, (struct sockaddr *) &addr, sizeof(addr)) < 0)
463 {
464 fprintf(stderr, "ERROR: can't connect output to %s at port %d\n",
465 inet_ntoa(addr.sin_addr), outport);
466 close(sock_id);
467 exit(3);
468 }
469
470 /* set socket buffer size */
471 setsockopt(sock_id, SOL_SOCKET, SO_SNDBUF, (const char *) &size,
472 sizeof(const char *));
473 /* send message to caster */
474 szSendBuffer[0] = '\0';
475 sprintf(szSendBuffer, "SOURCE %s /%s\r\n", password, mountpoint);
476 strcat(szSendBuffer, "Source-Agent: ");
477 strcat(szSendBuffer, VERSION);
478 strcat(szSendBuffer, "\r\n");
479 strcat(szSendBuffer, "\r\n");
480 strcat(szSendBuffer, "\0");
481 nBufferBytes = strlen(szSendBuffer);
482 if((send(sock_id, szSendBuffer, (size_t)nBufferBytes, 0)) != nBufferBytes)
483 {
484 fprintf(stderr, "ERROR: could not send to caster\n");
485 close(sock_id);
486 sleep(5);
487 exit(0);
488 }
489 /* check caster's response */
490 nBufferBytes = recv(sock_id, szSendBuffer, sizeof(szSendBuffer), 0);
491 szSendBuffer[nBufferBytes] = '\0';
492 if(strcmp(szSendBuffer, "OK\r\n"))
493 {
494 char *a;
495 fprintf(stderr, "ERROR: caster's reply is not OK : ");
496 for(a = szSendBuffer; *a && *a != '\n' && *a != '\r'; ++a)
497 {
498 fprintf(stderr, "%.1s", isprint(*a) ? a : ".");
499 }
500 fprintf(stderr, "\n");
501 close(sock_id);
502 sleep(5);
503 exit(0);
504 }
505 printf("connection successfull\n");
506 send_receive_loop(sock_id, gpsfd, mode == SISNET);
507 }
508 exit(0);
509}
510
511static void send_receive_loop(int sock, int fd, int sisnet)
512{
513 char buffer[BUFSZ] = { 0 };
514 char sisnetbackbuffer[200];
515 int nBufferBytes = 0, i;
516 /* data transmission */
517 printf("transfering data ...\n");
518 while(1)
519 {
520 alarm(ALARMTIME);
521
522 if(!nBufferBytes)
523 {
524 if(sisnet)
525 {
526 int i;
527 /* a somewhat higher rate than 1 second to get really each block */
528 /* means we need to skip double blocks sometimes */
529 struct timeval tv = {0,700000};
530 select(0, 0, 0, 0, &tv);
531 memcpy(sisnetbackbuffer, buffer, sizeof(sisnetbackbuffer));
532 i = (sisnetv3 ? 5 : 3);
533 if((send(gpsfd, "MSG\r\n", i, 0)) != i)
534 {
535 perror("ERROR: sending data request");
536 exit(1);
537 }
538 }
539 /* receiving data */
540 nBufferBytes = read(fd, buffer, BUFSZ);
541 if(!nBufferBytes)
542 {
543 printf("WARNING: no data received from input\n");
544 continue;
545 }
546 else if(nBufferBytes < 0)
547 {
548 perror("ERROR: reading input failed");
549 exit(1);
550 }
551 /* we can compare the whole buffer, as the additional bytes remain unchanged */
552 if(!memcmp(sisnetbackbuffer, buffer, sizeof(sisnetbackbuffer)))
553 {
554 nBufferBytes = 0;
555 }
556 }
557 if(nBufferBytes)
558 {
559 /* send data */
560 if((i = send(sock, buffer, (size_t)nBufferBytes, MSG_DONTWAIT))
561 != nBufferBytes)
562 {
563 if(i < 0 && errno != EAGAIN)
564 {
565 perror("WARNING: could not send data - retry connection");
566 close(sock);
567 sleep(5);
568 return;
569 }
570 else if(i)
571 {
572 memmove(buffer, buffer+i, (size_t)(nBufferBytes-i));
573 nBufferBytes -= i;
574 }
575 }
576 else
577 {
578 nBufferBytes = 0;
579 }
580 }
581 }
582}
583
584/*
585 * openserial
586 *
587 * Open the serial port with the given device name and configure it for
588 * reading NMEA data from a GPS receiver.
589 *
590 * Parameters:
591 * tty : pointer to : A zero-terminated string containing the device
592 * unsigned char name of the appropriate serial port.
593 * blocksz : integer : Block size for port I/O
594 * baud : integer : Baud rate for port I/O
595 *
596 * Return Value:
597 * The function returns a file descriptor for the opened port if successful.
598 * The function returns -1 in the event of an error.
599 *
600 * Remarks:
601 *
602 */
603
604static int openserial(const char * tty, int blocksz, int baud)
605{
606 int fd;
607 struct termios termios;
608
609 fd = open(tty, O_RDWR | O_NONBLOCK | O_EXLOCK);
610 if(fd < 0)
611 {
612 perror("ERROR: opening serial connection");
613 return (-1);
614 }
615 if(tcgetattr(fd, &termios) < 0)
616 {
617 perror("ERROR: get serial attributes");
618 return (-1);
619 }
620 termios.c_iflag = 0;
621 termios.c_oflag = 0; /* (ONLRET) */
622 termios.c_cflag = CS8 | CLOCAL | CREAD;
623 termios.c_lflag = 0;
624 {
625 int cnt;
626 for(cnt = 0; cnt < NCCS; cnt++)
627 termios.c_cc[cnt] = -1;
628 }
629 termios.c_cc[VMIN] = blocksz;
630 termios.c_cc[VTIME] = 2;
631
632#if (B4800 != 4800)
633 /*
634 * Not every system has speed settings equal to absolute speed value.
635 */
636
637 switch (baud)
638 {
639 case 300:
640 baud = B300;
641 break;
642 case 1200:
643 baud = B1200;
644 break;
645 case 2400:
646 baud = B2400;
647 break;
648 case 4800:
649 baud = B4800;
650 break;
651 case 9600:
652 baud = B9600;
653 break;
654 case 19200:
655 baud = B19200;
656 break;
657 case 38400:
658 baud = B38400;
659 break;
660#ifdef B57600
661 case 57600:
662 baud = B57600;
663 break;
664#endif
665#ifdef B115200
666 case 115200:
667 baud = B115200;
668 break;
669#endif
670#ifdef B230400
671 case 230400:
672 baud = B230400;
673 break;
674#endif
675 default:
676 fprintf(stderr, "WARNING: Baud settings not useful, using 19200\n");
677 baud = B19200;
678 break;
679 }
680#endif
681
682 if(cfsetispeed(&termios, baud) != 0)
683 {
684 perror("ERROR: setting serial speed with cfsetispeed");
685 return (-1);
686 }
687 if(cfsetospeed(&termios, baud) != 0)
688 {
689 perror("ERROR: setting serial speed with cfsetospeed");
690 return (-1);
691 }
692 if(tcsetattr(fd, TCSANOW, &termios) < 0)
693 {
694 perror("ERROR: setting serial attributes");
695 return (-1);
696 }
697 if(fcntl(fd, F_SETFL, 0) == -1)
698 {
699 perror("WARNING: setting blocking mode failed");
700 }
701 return (fd);
702}
703
704/*
705 * usage
706 *
707 * Send a usage message to standard error and quit the program.
708 *
709 * Parameters:
710 * None.
711 *
712 * Return Value:
713 * The function does not return a value.
714 *
715 * Remarks:
716 *
717 */
718
719static void usage(int rc)
720{
721 fprintf(stderr, "Usage: %s [OPTIONS]\n", VERSION);
722 fprintf(stderr, " Options are:\n");
723 fprintf(stderr, " -a caster name or address (default: %s)\n",
724 NTRIP_CASTER);
725 fprintf(stderr, " -p caster port (default: %d)\n", NTRIP_PORT);
726 fprintf(stderr, " -m caster mountpoint\n");
727 fprintf(stderr, " -c password for caster login\n");
728 fprintf(stderr, " -h|? print this help screen\n");
729 fprintf(stderr, " -M <mode> sets the mode\n");
730 fprintf(stderr, " (1=serial, 2=tcpsocket, 3=file, 4=sisnet, 5=udpsocket)\n");
731 fprintf(stderr, " Mode = file:\n");
732 fprintf(stderr, " -s file, simulate data stream by reading log file\n");
733 fprintf(stderr, " default/current setting is %s\n", filepath);
734 fprintf(stderr, " Mode = serial:\n");
735 fprintf(stderr, " -b baud_rate, sets serial input baud rate\n");
736 fprintf(stderr, " default/current value is %d\n", ttybaud);
737 fprintf(stderr, " -i input_device, sets name of serial input device\n");
738 fprintf(stderr, " default/current value is %s\n", ttyport);
739 fprintf(stderr, " (normally a symbolic link to /dev/tty\?\?)\n");
740 fprintf(stderr, " Mode = tcpsocket or udpsocket:\n");
741 fprintf(stderr, " -P receiver port (default: %d)\n", SERV_TCP_PORT);
742 fprintf(stderr, " -H hostname of TCP server (default: %s)\n", SERV_HOST_ADDR);
743 fprintf(stderr, " -f initfile send to server\n");
744 fprintf(stderr, " -B bindmode (bind to incoming UDP stream)\n");
745 fprintf(stderr, " Mode = sisnet:\n");
746 fprintf(stderr, " -P receiver port (default: %d)\n", SISNET_PORT);
747 fprintf(stderr, " -H hostname of TCP server (default: %s)\n", SISNET_SERVER);
748 fprintf(stderr, " -u username\n");
749 fprintf(stderr, " -l password\n");
750 fprintf(stderr, " -V version [2.1 or 3.0] (default: 2.1)\n");
751 fprintf(stderr, "\n");
752 exit(rc);
753}
Note: See TracBrowser for help on using the repository browser.