mirror of
				https://github.com/ossrs/srs.git
				synced 2025-03-09 15:49:59 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			541 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			541 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /*
 | |
|  * Portions created by SGI are Copyright (C) 2000 Silicon Graphics, Inc.
 | |
|  * All Rights Reserved.
 | |
|  *
 | |
|  * Redistribution and use in source and binary forms, with or without
 | |
|  * modification, are permitted provided that the following conditions
 | |
|  * are met: 
 | |
|  *
 | |
|  * 1. Redistributions of source code must retain the above copyright
 | |
|  *    notice, this list of conditions and the following disclaimer. 
 | |
|  * 2. Redistributions in binary form must reproduce the above copyright
 | |
|  *    notice, this list of conditions and the following disclaimer in the
 | |
|  *    documentation and/or other materials provided with the distribution.
 | |
|  * 3. Neither the name of Silicon Graphics, Inc. nor the names of its
 | |
|  *    contributors may be used to endorse or promote products derived from
 | |
|  *    this software without specific prior written permission. 
 | |
|  *
 | |
|  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 | |
|  * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 | |
|  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 | |
|  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 | |
|  * HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 | |
|  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 | |
|  * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 | |
|  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 | |
|  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 | |
|  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 | |
|  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 | |
|  */
 | |
| 
 | |
| #include <stdio.h>
 | |
| #include <stdlib.h>
 | |
| #include <string.h>
 | |
| #include <signal.h>
 | |
| #include <unistd.h>
 | |
| #include <fcntl.h>
 | |
| #include <sys/types.h>
 | |
| #include <sys/stat.h>
 | |
| #include <sys/socket.h>
 | |
| #include <netinet/in.h>
 | |
| #include <arpa/inet.h>
 | |
| #include <netdb.h>
 | |
| #include "st.h"
 | |
| 
 | |
| #define IOBUFSIZE (16*1024)
 | |
| 
 | |
| #define IOV_LEN   256
 | |
| #define IOV_COUNT (IOBUFSIZE / IOV_LEN)
 | |
| 
 | |
| #ifndef INADDR_NONE
 | |
| #define INADDR_NONE 0xffffffff
 | |
| #endif
 | |
| 
 | |
| static char *prog;                     /* Program name   */
 | |
| static struct sockaddr_in rmt_addr;    /* Remote address */
 | |
| 
 | |
| static unsigned long testing;
 | |
| #define TESTING_VERBOSE		0x1
 | |
| #define TESTING_READV		0x2
 | |
| #define	TESTING_READ_RESID	0x4
 | |
| #define TESTING_WRITEV		0x8
 | |
| #define TESTING_WRITE_RESID	0x10
 | |
| 
 | |
| static void read_address(const char *str, struct sockaddr_in *sin);
 | |
| static void start_daemon(void);
 | |
| static int  cpu_count(void);
 | |
| static void set_concurrency(int nproc);
 | |
| static void *handle_request(void *arg);
 | |
| static void print_sys_error(const char *msg);
 | |
| 
 | |
| 
 | |
| /*
 | |
|  * This program acts as a generic gateway. It listens for connections
 | |
|  * to a local address ('-l' option). Upon accepting a client connection,
 | |
|  * it connects to the specified remote address ('-r' option) and then
 | |
|  * just pumps the data through without any modification.
 | |
|  */
 | |
| int main(int argc, char *argv[])
 | |
| {
 | |
|   extern char *optarg;
 | |
|   int opt, sock, n;
 | |
|   int laddr, raddr, num_procs, alt_ev, one_process;
 | |
|   int serialize_accept = 0;
 | |
|   struct sockaddr_in lcl_addr, cli_addr;
 | |
|   st_netfd_t cli_nfd, srv_nfd;
 | |
| 
 | |
|   prog = argv[0];
 | |
|   num_procs = laddr = raddr = alt_ev = one_process = 0;
 | |
| 
 | |
|   /* Parse arguments */
 | |
|   while((opt = getopt(argc, argv, "l:r:p:Saht:X")) != EOF) {
 | |
|     switch (opt) {
 | |
|     case 'a':
 | |
|       alt_ev = 1;
 | |
|       break;
 | |
|     case 'l':
 | |
|       read_address(optarg, &lcl_addr);
 | |
|       laddr = 1;
 | |
|       break;
 | |
|     case 'r':
 | |
|       read_address(optarg, &rmt_addr);
 | |
|       if (rmt_addr.sin_addr.s_addr == INADDR_ANY) {
 | |
| 	fprintf(stderr, "%s: invalid remote address: %s\n", prog, optarg);
 | |
| 	exit(1);
 | |
|       }
 | |
|       raddr = 1;
 | |
|       break;
 | |
|     case 'p':
 | |
|       num_procs = atoi(optarg);
 | |
|       if (num_procs < 1) {
 | |
| 	fprintf(stderr, "%s: invalid number of processes: %s\n", prog, optarg);
 | |
| 	exit(1);
 | |
|       }
 | |
|       break;
 | |
|     case 'S':
 | |
|       /*
 | |
|        * Serialization decision is tricky on some platforms. For example,
 | |
|        * Solaris 2.6 and above has kernel sockets implementation, so supposedly
 | |
|        * there is no need for serialization. The ST library may be compiled
 | |
|        * on one OS version, but used on another, so the need for serialization
 | |
|        * should be determined at run time by the application. Since it's just
 | |
|        * an example, the serialization decision is left up to user.
 | |
|        * Only on platforms where the serialization is never needed on any OS
 | |
|        * version st_netfd_serialize_accept() is a no-op.
 | |
|        */
 | |
|       serialize_accept = 1;
 | |
|       break;
 | |
|     case 't':
 | |
|       testing = strtoul(optarg, NULL, 0);
 | |
|       break;
 | |
|     case 'X':
 | |
|       one_process = 1;
 | |
|       break;
 | |
|     case 'h':
 | |
|     case '?':
 | |
|       fprintf(stderr, "Usage: %s [options] -l <[host]:port> -r <host:port>\n",
 | |
|        prog);
 | |
|       fprintf(stderr, "options are:\n");
 | |
|       fprintf(stderr, "  -p <num_processes>	number of parallel processes\n");
 | |
|       fprintf(stderr, "  -S			serialize accepts\n");
 | |
|       fprintf(stderr, "  -a			use alternate event system\n");
 | |
| #ifdef DEBUG
 | |
|       fprintf(stderr, "  -t mask		testing/debugging mode\n");
 | |
|       fprintf(stderr, "  -X			one process, don't daemonize\n");
 | |
| #endif
 | |
|       exit(1);
 | |
|     }
 | |
|   }
 | |
|   if (!laddr) {
 | |
|     fprintf(stderr, "%s: local address required\n", prog);
 | |
|     exit(1);
 | |
|   }
 | |
|   if (!raddr) {
 | |
|     fprintf(stderr, "%s: remote address required\n", prog);
 | |
|     exit(1);
 | |
|   }
 | |
|   if (num_procs == 0)
 | |
|     num_procs = cpu_count();
 | |
| 
 | |
|   fprintf(stderr, "%s: starting proxy daemon on %s:%d\n", prog,
 | |
| 	  inet_ntoa(lcl_addr.sin_addr), ntohs(lcl_addr.sin_port));
 | |
| 
 | |
|   /* Start the daemon */
 | |
|   if (one_process)
 | |
|     num_procs = 1;
 | |
|   else
 | |
|     start_daemon();
 | |
| 
 | |
|   if (alt_ev)
 | |
|     st_set_eventsys(ST_EVENTSYS_ALT);
 | |
| 
 | |
|   /* Initialize the ST library */
 | |
|   if (st_init() < 0) {
 | |
|     print_sys_error("st_init");
 | |
|     exit(1);
 | |
|   }
 | |
| 
 | |
|   /* Create and bind listening socket */
 | |
|   if ((sock = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
 | |
|     print_sys_error("socket");
 | |
|     exit(1);
 | |
|   }
 | |
|   n = 1;
 | |
|   if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char *)&n, sizeof(n)) < 0) {
 | |
|     print_sys_error("setsockopt");
 | |
|     exit(1);
 | |
|   }
 | |
|   if (bind(sock, (struct sockaddr *)&lcl_addr, sizeof(lcl_addr)) < 0) {
 | |
|     print_sys_error("bind");
 | |
|     exit(1);
 | |
|   }
 | |
|   listen(sock, 128);
 | |
|   if ((srv_nfd = st_netfd_open_socket(sock)) == NULL) {
 | |
|     print_sys_error("st_netfd_open");
 | |
|     exit(1);
 | |
|   }
 | |
|   /* See the comment regarding serialization decision above */
 | |
|   if (num_procs > 1 && serialize_accept && st_netfd_serialize_accept(srv_nfd)
 | |
|       < 0) {
 | |
|     print_sys_error("st_netfd_serialize_accept");
 | |
|     exit(1);
 | |
|   }
 | |
| 
 | |
|   /* Start server processes */
 | |
|   if (!one_process)
 | |
|     set_concurrency(num_procs);
 | |
| 
 | |
|   for ( ; ; ) {
 | |
|     n = sizeof(cli_addr);
 | |
|     cli_nfd = st_accept(srv_nfd, (struct sockaddr *)&cli_addr, &n,
 | |
|      ST_UTIME_NO_TIMEOUT);
 | |
|     if (cli_nfd == NULL) {
 | |
|       print_sys_error("st_accept");
 | |
|       exit(1);
 | |
|     }
 | |
|     if (st_thread_create(handle_request, cli_nfd, 0, 0) == NULL) {
 | |
|       print_sys_error("st_thread_create");
 | |
|       exit(1);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /* NOTREACHED */
 | |
|   return 1;
 | |
| }
 | |
| 
 | |
| 
 | |
| static void read_address(const char *str, struct sockaddr_in *sin)
 | |
| {
 | |
|   char host[128], *p;
 | |
|   struct hostent *hp;
 | |
|   unsigned short port;
 | |
| 
 | |
|   strcpy(host, str);
 | |
|   if ((p = strchr(host, ':')) == NULL) {
 | |
|     fprintf(stderr, "%s: invalid address: %s\n", prog, host);
 | |
|     exit(1);
 | |
|   }
 | |
|   *p++ = '\0';
 | |
|   port = (unsigned short) atoi(p);
 | |
|   if (port < 1) {
 | |
|     fprintf(stderr, "%s: invalid port: %s\n", prog, p);
 | |
|     exit(1);
 | |
|   }
 | |
| 
 | |
|   memset(sin, 0, sizeof(struct sockaddr_in));
 | |
|   sin->sin_family = AF_INET;
 | |
|   sin->sin_port = htons(port);
 | |
|   if (host[0] == '\0') {
 | |
|     sin->sin_addr.s_addr = INADDR_ANY;
 | |
|     return;
 | |
|   }
 | |
|   sin->sin_addr.s_addr = inet_addr(host);
 | |
|   if (sin->sin_addr.s_addr == INADDR_NONE) {
 | |
|     /* not dotted-decimal */
 | |
|     if ((hp = gethostbyname(host)) == NULL) {
 | |
|       fprintf(stderr, "%s: can't resolve address: %s\n", prog, host);
 | |
|       exit(1);
 | |
|     }
 | |
|     memcpy(&sin->sin_addr, hp->h_addr, hp->h_length);
 | |
|   }
 | |
| }
 | |
| 
 | |
| #ifdef DEBUG
 | |
| static void show_iov(const struct iovec *iov, int niov)
 | |
| {
 | |
|   int i;
 | |
|   size_t total;
 | |
| 
 | |
|   printf("iov %p has %d entries:\n", iov, niov);
 | |
|   total = 0;
 | |
|   for (i = 0; i < niov; i++) {
 | |
|     printf("iov[%3d] iov_base=%p iov_len=0x%lx(%lu)\n",
 | |
|      i, iov[i].iov_base, (unsigned long) iov[i].iov_len,
 | |
|      (unsigned long) iov[i].iov_len);
 | |
|     total += iov[i].iov_len;
 | |
|   }
 | |
|   printf("total 0x%lx(%ld)\n", (unsigned long) total, (unsigned long) total);
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * This version is tricked out to test all the
 | |
|  * st_(read|write)v?(_resid)? variants.  Use the non-DEBUG version for
 | |
|  * anything serious.  st_(read|write) are all this function really
 | |
|  * needs.
 | |
|  */
 | |
| static int pass(st_netfd_t in, st_netfd_t out)
 | |
| {
 | |
|   char buf[IOBUFSIZE];
 | |
|   struct iovec iov[IOV_COUNT];
 | |
|   int ioviter, nw, nr;
 | |
| 
 | |
|   if (testing & TESTING_READV) {
 | |
|     for (ioviter = 0; ioviter < IOV_COUNT; ioviter++) {
 | |
|       iov[ioviter].iov_base = &buf[ioviter * IOV_LEN];
 | |
|       iov[ioviter].iov_len = IOV_LEN;
 | |
|     }
 | |
|     if (testing & TESTING_VERBOSE) {
 | |
|       printf("readv(%p)...\n", in);
 | |
|       show_iov(iov, IOV_COUNT);
 | |
|     }
 | |
|     if (testing & TESTING_READ_RESID) {
 | |
|       struct iovec *riov = iov;
 | |
|       int riov_cnt = IOV_COUNT;
 | |
|       if (st_readv_resid(in, &riov, &riov_cnt, ST_UTIME_NO_TIMEOUT) == 0) {
 | |
| 	if (testing & TESTING_VERBOSE) {
 | |
| 	  printf("resid\n");
 | |
| 	  show_iov(riov, riov_cnt);
 | |
| 	  printf("full\n");
 | |
| 	  show_iov(iov, IOV_COUNT);
 | |
| 	}
 | |
| 	nr = 0;
 | |
| 	for (ioviter = 0; ioviter < IOV_COUNT; ioviter++)
 | |
| 	  nr += iov[ioviter].iov_len;
 | |
| 	nr = IOBUFSIZE - nr;
 | |
|       } else
 | |
| 	nr = -1;
 | |
|     } else
 | |
|       nr = (int) st_readv(in, iov, IOV_COUNT, ST_UTIME_NO_TIMEOUT);
 | |
|   } else {
 | |
|     if (testing & TESTING_READ_RESID) {
 | |
|       size_t resid = IOBUFSIZE;
 | |
|       if (st_read_resid(in, buf, &resid, ST_UTIME_NO_TIMEOUT) == 0)
 | |
| 	nr = IOBUFSIZE - resid;
 | |
|       else
 | |
| 	nr = -1;
 | |
|     } else
 | |
|       nr = (int) st_read(in, buf, IOBUFSIZE, ST_UTIME_NO_TIMEOUT);
 | |
|   }
 | |
|   if (testing & TESTING_VERBOSE)
 | |
|     printf("got 0x%x(%d) E=%d\n", nr, nr, errno);
 | |
| 
 | |
|   if (nr <= 0)
 | |
|     return 0;
 | |
| 
 | |
|   if (testing & TESTING_WRITEV) {
 | |
|     for (nw = 0, ioviter = 0; nw < nr;
 | |
|      nw += iov[ioviter].iov_len, ioviter++) {
 | |
|       iov[ioviter].iov_base = &buf[nw];
 | |
|       iov[ioviter].iov_len = nr - nw;
 | |
|       if (iov[ioviter].iov_len > IOV_LEN)
 | |
| 	iov[ioviter].iov_len = IOV_LEN;
 | |
|     }
 | |
|     if (testing & TESTING_VERBOSE) {
 | |
|       printf("writev(%p)...\n", out);
 | |
|       show_iov(iov, ioviter);
 | |
|     }
 | |
|     if (testing & TESTING_WRITE_RESID) {
 | |
|       struct iovec *riov = iov;
 | |
|       int riov_cnt = ioviter;
 | |
|       if (st_writev_resid(out, &riov, &riov_cnt, ST_UTIME_NO_TIMEOUT) == 0) {
 | |
| 	if (testing & TESTING_VERBOSE) {
 | |
| 	  printf("resid\n");
 | |
| 	  show_iov(riov, riov_cnt);
 | |
| 	  printf("full\n");
 | |
| 	  show_iov(iov, ioviter);
 | |
| 	}
 | |
| 	nw = 0;
 | |
| 	while (--ioviter >= 0)
 | |
| 	  nw += iov[ioviter].iov_len;
 | |
| 	nw = nr - nw;
 | |
|       } else
 | |
| 	nw = -1;
 | |
|     } else
 | |
|       nw = st_writev(out, iov, ioviter, ST_UTIME_NO_TIMEOUT);
 | |
|   } else {
 | |
|     if (testing & TESTING_WRITE_RESID) {
 | |
|       size_t resid = nr;
 | |
|       if (st_write_resid(out, buf, &resid, ST_UTIME_NO_TIMEOUT) == 0)
 | |
| 	nw = nr - resid;
 | |
|       else
 | |
| 	nw = -1;
 | |
|     } else
 | |
|       nw = st_write(out, buf, nr, ST_UTIME_NO_TIMEOUT);
 | |
|   }
 | |
|   if (testing & TESTING_VERBOSE)
 | |
|     printf("put 0x%x(%d) E=%d\n", nw, nw, errno);
 | |
| 
 | |
|   if (nw != nr)
 | |
|     return 0;
 | |
| 
 | |
|   return 1;
 | |
| }
 | |
| #else /* DEBUG */
 | |
| /*
 | |
|  * This version is the simple one suitable for serious use.
 | |
|  */
 | |
| static int pass(st_netfd_t in, st_netfd_t out)
 | |
| {
 | |
|   char buf[IOBUFSIZE];
 | |
|   int nw, nr;
 | |
| 
 | |
|   nr = (int) st_read(in, buf, IOBUFSIZE, ST_UTIME_NO_TIMEOUT);
 | |
|   if (nr <= 0)
 | |
|     return 0;
 | |
| 
 | |
|   nw = st_write(out, buf, nr, ST_UTIME_NO_TIMEOUT);
 | |
|   if (nw != nr)
 | |
|     return 0;
 | |
| 
 | |
|   return 1;
 | |
| }
 | |
| #endif
 | |
| 
 | |
| static void *handle_request(void *arg)
 | |
| {
 | |
|   struct pollfd pds[2];
 | |
|   st_netfd_t cli_nfd, rmt_nfd;
 | |
|   int sock;
 | |
| 
 | |
|   cli_nfd = (st_netfd_t) arg;
 | |
|   pds[0].fd = st_netfd_fileno(cli_nfd);
 | |
|   pds[0].events = POLLIN;
 | |
| 
 | |
|   /* Connect to remote host */
 | |
|   if ((sock = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
 | |
|     print_sys_error("socket");
 | |
|     goto done;
 | |
|   }
 | |
|   if ((rmt_nfd = st_netfd_open_socket(sock)) == NULL) {
 | |
|     print_sys_error("st_netfd_open_socket");
 | |
|     close(sock);
 | |
|     goto done;
 | |
|   }
 | |
|   if (st_connect(rmt_nfd, (struct sockaddr *)&rmt_addr,
 | |
| 		 sizeof(rmt_addr), ST_UTIME_NO_TIMEOUT) < 0) {
 | |
|     print_sys_error("st_connect");
 | |
|     st_netfd_close(rmt_nfd);
 | |
|     goto done;
 | |
|   }
 | |
|   pds[1].fd = sock;
 | |
|   pds[1].events = POLLIN;
 | |
| 
 | |
|   /*
 | |
|    * Now just pump the data through.
 | |
|    * XXX This should use one thread for each direction for true full-duplex.
 | |
|    */
 | |
|   for ( ; ; ) {
 | |
|     pds[0].revents = 0;
 | |
|     pds[1].revents = 0;
 | |
| 
 | |
|     if (st_poll(pds, 2, ST_UTIME_NO_TIMEOUT) <= 0) {
 | |
|       print_sys_error("st_poll");
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     if (pds[0].revents & POLLIN) {
 | |
|       if (!pass(cli_nfd, rmt_nfd))
 | |
| 	break;
 | |
|     }
 | |
| 
 | |
|     if (pds[1].revents & POLLIN) {
 | |
|       if (!pass(rmt_nfd, cli_nfd))
 | |
| 	break;
 | |
|     }
 | |
|   }
 | |
|   st_netfd_close(rmt_nfd);
 | |
| 
 | |
| done:
 | |
| 
 | |
|   st_netfd_close(cli_nfd);
 | |
| 
 | |
|   return NULL;
 | |
| }
 | |
| 
 | |
| static void start_daemon(void)
 | |
| {
 | |
|   pid_t pid;
 | |
| 
 | |
|   /* Start forking */
 | |
|   if ((pid = fork()) < 0) {
 | |
|     print_sys_error("fork");
 | |
|     exit(1);
 | |
|   }
 | |
|   if (pid > 0)
 | |
|     exit(0);                        /* parent */
 | |
| 
 | |
|   /* First child process */
 | |
|   setsid();                         /* become session leader */
 | |
| 
 | |
|   if ((pid = fork()) < 0) {
 | |
|     print_sys_error("fork");
 | |
|     exit(1);
 | |
|   }
 | |
|   if (pid > 0)                      /* first child */
 | |
|     exit(0);
 | |
| 
 | |
|   chdir("/");
 | |
|   umask(022);
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * Create separate processes ("virtual processors"). Since it's just an
 | |
|  * example, there is no watchdog - the parent just exits leaving children
 | |
|  * on their own.
 | |
|  */
 | |
| static void set_concurrency(int nproc)
 | |
| {
 | |
|   pid_t pid;
 | |
|   int i;
 | |
| 
 | |
|   if (nproc < 1)
 | |
|     nproc = 1;
 | |
| 
 | |
|   for (i = 0; i < nproc; i++) {
 | |
|     if ((pid = fork()) < 0) {
 | |
|       print_sys_error("fork");
 | |
|       exit(1);
 | |
|     }
 | |
|     /* Child returns */
 | |
|     if (pid == 0)
 | |
|       return;
 | |
|   }
 | |
| 
 | |
|   /* Parent just exits */
 | |
|   exit(0);
 | |
| }
 | |
| 
 | |
| static int cpu_count(void)
 | |
| {
 | |
|   int n;
 | |
| 
 | |
| #if defined (_SC_NPROCESSORS_ONLN)
 | |
|   n = (int) sysconf(_SC_NPROCESSORS_ONLN);
 | |
| #elif defined (_SC_NPROC_ONLN)
 | |
|   n = (int) sysconf(_SC_NPROC_ONLN);
 | |
| #elif defined (HPUX)
 | |
| #include <sys/mpctl.h>
 | |
|   n = mpctl(MPC_GETNUMSPUS, 0, 0);
 | |
| #else
 | |
|   n = -1;
 | |
|   errno = ENOSYS;
 | |
| #endif
 | |
| 
 | |
|   return n;
 | |
| }
 | |
| 
 | |
| static void print_sys_error(const char *msg)
 | |
| {
 | |
|   fprintf(stderr, "%s: %s: %s\n", prog, msg, strerror(errno));
 | |
| }
 | |
| 
 |