1 | // SPDX-License-Identifier: GPL-2.0-or-later |
2 | /* |
3 | * Copyright (C) 2011 matt mooney <mfm@muteddisk.com> |
4 | * 2005-2007 Takahiro Hirofuchi |
5 | * Copyright (C) 2015-2016 Samsung Electronics |
6 | * Igor Kotrasinski <i.kotrasinsk@samsung.com> |
7 | * Krzysztof Opasiak <k.opasiak@samsung.com> |
8 | */ |
9 | |
10 | #ifdef HAVE_CONFIG_H |
11 | #include "../config.h" |
12 | #endif |
13 | |
14 | #define _GNU_SOURCE |
15 | #include <errno.h> |
16 | #include <unistd.h> |
17 | #include <netdb.h> |
18 | #include <string.h> |
19 | #include <stdlib.h> |
20 | #include <sys/types.h> |
21 | #include <sys/stat.h> |
22 | #include <arpa/inet.h> |
23 | #include <sys/socket.h> |
24 | #include <netinet/in.h> |
25 | |
26 | #ifdef HAVE_LIBWRAP |
27 | #include <tcpd.h> |
28 | #endif |
29 | |
30 | #include <getopt.h> |
31 | #include <signal.h> |
32 | #include <poll.h> |
33 | |
34 | #include "usbip_host_driver.h" |
35 | #include "usbip_host_common.h" |
36 | #include "usbip_device_driver.h" |
37 | #include "usbip_common.h" |
38 | #include "usbip_network.h" |
39 | #include "list.h" |
40 | |
41 | #undef PROGNAME |
42 | #define PROGNAME "usbipd" |
43 | #define MAXSOCKFD 20 |
44 | |
45 | #define MAIN_LOOP_TIMEOUT 10 |
46 | |
47 | #define DEFAULT_PID_FILE "/var/run/" PROGNAME ".pid" |
48 | |
49 | static const char usbip_version_string[] = PACKAGE_STRING; |
50 | |
51 | static const char usbipd_help_string[] = |
52 | "usage: usbipd [options]\n" |
53 | "\n" |
54 | " -4, --ipv4\n" |
55 | " Bind to IPv4. Default is both.\n" |
56 | "\n" |
57 | " -6, --ipv6\n" |
58 | " Bind to IPv6. Default is both.\n" |
59 | "\n" |
60 | " -e, --device\n" |
61 | " Run in device mode.\n" |
62 | " Rather than drive an attached device, create\n" |
63 | " a virtual UDC to bind gadgets to.\n" |
64 | "\n" |
65 | " -D, --daemon\n" |
66 | " Run as a daemon process.\n" |
67 | "\n" |
68 | " -d, --debug\n" |
69 | " Print debugging information.\n" |
70 | "\n" |
71 | " -PFILE, --pid FILE\n" |
72 | " Write process id to FILE.\n" |
73 | " If no FILE specified, use " DEFAULT_PID_FILE "\n" |
74 | "\n" |
75 | " -tPORT, --tcp-port PORT\n" |
76 | " Listen on TCP/IP port PORT.\n" |
77 | "\n" |
78 | " -h, --help\n" |
79 | " Print this help.\n" |
80 | "\n" |
81 | " -v, --version\n" |
82 | " Show version.\n" ; |
83 | |
84 | static struct usbip_host_driver *driver; |
85 | |
86 | static void usbipd_help(void) |
87 | { |
88 | printf("%s\n" , usbipd_help_string); |
89 | } |
90 | |
91 | static int recv_request_import(int sockfd) |
92 | { |
93 | struct op_import_request req; |
94 | struct usbip_exported_device *edev; |
95 | struct usbip_usb_device pdu_udev; |
96 | struct list_head *i; |
97 | int found = 0; |
98 | int status = ST_OK; |
99 | int rc; |
100 | |
101 | memset(&req, 0, sizeof(req)); |
102 | |
103 | rc = usbip_net_recv(sockfd, &req, sizeof(req)); |
104 | if (rc < 0) { |
105 | dbg("usbip_net_recv failed: import request" ); |
106 | return -1; |
107 | } |
108 | PACK_OP_IMPORT_REQUEST(0, &req); |
109 | |
110 | list_for_each(i, &driver->edev_list) { |
111 | edev = list_entry(i, struct usbip_exported_device, node); |
112 | if (!strncmp(req.busid, edev->udev.busid, SYSFS_BUS_ID_SIZE)) { |
113 | info("found requested device: %s" , req.busid); |
114 | found = 1; |
115 | break; |
116 | } |
117 | } |
118 | |
119 | if (found) { |
120 | /* should set TCP_NODELAY for usbip */ |
121 | usbip_net_set_nodelay(sockfd); |
122 | |
123 | /* export device needs a TCP/IP socket descriptor */ |
124 | status = usbip_export_device(edev, sockfd); |
125 | if (status < 0) |
126 | status = ST_NA; |
127 | } else { |
128 | info("requested device not found: %s" , req.busid); |
129 | status = ST_NODEV; |
130 | } |
131 | |
132 | rc = usbip_net_send_op_common(sockfd, OP_REP_IMPORT, status); |
133 | if (rc < 0) { |
134 | dbg("usbip_net_send_op_common failed: %#0x" , OP_REP_IMPORT); |
135 | return -1; |
136 | } |
137 | |
138 | if (status) { |
139 | dbg("import request busid %s: failed" , req.busid); |
140 | return -1; |
141 | } |
142 | |
143 | memcpy(&pdu_udev, &edev->udev, sizeof(pdu_udev)); |
144 | usbip_net_pack_usb_device(pack: 1, udev: &pdu_udev); |
145 | |
146 | rc = usbip_net_send(sockfd, &pdu_udev, sizeof(pdu_udev)); |
147 | if (rc < 0) { |
148 | dbg("usbip_net_send failed: devinfo" ); |
149 | return -1; |
150 | } |
151 | |
152 | dbg("import request busid %s: complete" , req.busid); |
153 | |
154 | return 0; |
155 | } |
156 | |
157 | static int send_reply_devlist(int connfd) |
158 | { |
159 | struct usbip_exported_device *edev; |
160 | struct usbip_usb_device pdu_udev; |
161 | struct usbip_usb_interface pdu_uinf; |
162 | struct op_devlist_reply reply; |
163 | struct list_head *j; |
164 | int rc, i; |
165 | |
166 | /* |
167 | * Exclude devices that are already exported to a client from |
168 | * the exportable device list to avoid: |
169 | * - import requests for devices that are exported only to |
170 | * fail the request. |
171 | * - revealing devices that are imported by a client to |
172 | * another client. |
173 | */ |
174 | |
175 | reply.ndev = 0; |
176 | /* number of exported devices */ |
177 | list_for_each(j, &driver->edev_list) { |
178 | edev = list_entry(j, struct usbip_exported_device, node); |
179 | if (edev->status != SDEV_ST_USED) |
180 | reply.ndev += 1; |
181 | } |
182 | info("exportable devices: %d" , reply.ndev); |
183 | |
184 | rc = usbip_net_send_op_common(connfd, OP_REP_DEVLIST, ST_OK); |
185 | if (rc < 0) { |
186 | dbg("usbip_net_send_op_common failed: %#0x" , OP_REP_DEVLIST); |
187 | return -1; |
188 | } |
189 | PACK_OP_DEVLIST_REPLY(1, &reply); |
190 | |
191 | rc = usbip_net_send(connfd, &reply, sizeof(reply)); |
192 | if (rc < 0) { |
193 | dbg("usbip_net_send failed: %#0x" , OP_REP_DEVLIST); |
194 | return -1; |
195 | } |
196 | |
197 | list_for_each(j, &driver->edev_list) { |
198 | edev = list_entry(j, struct usbip_exported_device, node); |
199 | if (edev->status == SDEV_ST_USED) |
200 | continue; |
201 | |
202 | dump_usb_device(&edev->udev); |
203 | memcpy(&pdu_udev, &edev->udev, sizeof(pdu_udev)); |
204 | usbip_net_pack_usb_device(pack: 1, udev: &pdu_udev); |
205 | |
206 | rc = usbip_net_send(connfd, &pdu_udev, sizeof(pdu_udev)); |
207 | if (rc < 0) { |
208 | dbg("usbip_net_send failed: pdu_udev" ); |
209 | return -1; |
210 | } |
211 | |
212 | for (i = 0; i < edev->udev.bNumInterfaces; i++) { |
213 | dump_usb_interface(&edev->uinf[i]); |
214 | memcpy(&pdu_uinf, &edev->uinf[i], sizeof(pdu_uinf)); |
215 | usbip_net_pack_usb_interface(pack: 1, uinf: &pdu_uinf); |
216 | |
217 | rc = usbip_net_send(connfd, &pdu_uinf, |
218 | sizeof(pdu_uinf)); |
219 | if (rc < 0) { |
220 | err("usbip_net_send failed: pdu_uinf" ); |
221 | return -1; |
222 | } |
223 | } |
224 | } |
225 | |
226 | return 0; |
227 | } |
228 | |
229 | static int recv_request_devlist(int connfd) |
230 | { |
231 | struct op_devlist_request req; |
232 | int rc; |
233 | |
234 | memset(&req, 0, sizeof(req)); |
235 | |
236 | rc = usbip_net_recv(connfd, &req, sizeof(req)); |
237 | if (rc < 0) { |
238 | dbg("usbip_net_recv failed: devlist request" ); |
239 | return -1; |
240 | } |
241 | |
242 | rc = send_reply_devlist(connfd); |
243 | if (rc < 0) { |
244 | dbg("send_reply_devlist failed" ); |
245 | return -1; |
246 | } |
247 | |
248 | return 0; |
249 | } |
250 | |
251 | static int recv_pdu(int connfd) |
252 | { |
253 | uint16_t code = OP_UNSPEC; |
254 | int ret; |
255 | int status; |
256 | |
257 | ret = usbip_net_recv_op_common(connfd, &code, &status); |
258 | if (ret < 0) { |
259 | dbg("could not receive opcode: %#0x" , code); |
260 | return -1; |
261 | } |
262 | |
263 | ret = usbip_refresh_device_list(driver); |
264 | if (ret < 0) { |
265 | dbg("could not refresh device list: %d" , ret); |
266 | return -1; |
267 | } |
268 | |
269 | info("received request: %#0x(%d)" , code, connfd); |
270 | switch (code) { |
271 | case OP_REQ_DEVLIST: |
272 | ret = recv_request_devlist(connfd); |
273 | break; |
274 | case OP_REQ_IMPORT: |
275 | ret = recv_request_import(sockfd: connfd); |
276 | break; |
277 | case OP_REQ_DEVINFO: |
278 | case OP_REQ_CRYPKEY: |
279 | default: |
280 | err("received an unknown opcode: %#0x" , code); |
281 | ret = -1; |
282 | } |
283 | |
284 | if (ret == 0) |
285 | info("request %#0x(%d): complete" , code, connfd); |
286 | else |
287 | info("request %#0x(%d): failed" , code, connfd); |
288 | |
289 | return ret; |
290 | } |
291 | |
292 | #ifdef HAVE_LIBWRAP |
293 | static int tcpd_auth(int connfd) |
294 | { |
295 | struct request_info request; |
296 | int rc; |
297 | |
298 | request_init(&request, RQ_DAEMON, PROGNAME, RQ_FILE, connfd, 0); |
299 | fromhost(&request); |
300 | rc = hosts_access(&request); |
301 | if (rc == 0) |
302 | return -1; |
303 | |
304 | return 0; |
305 | } |
306 | #endif |
307 | |
308 | static int do_accept(int listenfd) |
309 | { |
310 | int connfd; |
311 | struct sockaddr_storage ss; |
312 | socklen_t len = sizeof(ss); |
313 | char host[NI_MAXHOST], port[NI_MAXSERV]; |
314 | int rc; |
315 | |
316 | memset(&ss, 0, sizeof(ss)); |
317 | |
318 | connfd = accept(listenfd, (struct sockaddr *)&ss, &len); |
319 | if (connfd < 0) { |
320 | err("failed to accept connection" ); |
321 | return -1; |
322 | } |
323 | |
324 | rc = getnameinfo((struct sockaddr *)&ss, len, host, sizeof(host), |
325 | port, sizeof(port), NI_NUMERICHOST | NI_NUMERICSERV); |
326 | if (rc) |
327 | err("getnameinfo: %s" , gai_strerror(rc)); |
328 | |
329 | #ifdef HAVE_LIBWRAP |
330 | rc = tcpd_auth(connfd); |
331 | if (rc < 0) { |
332 | info("denied access from %s" , host); |
333 | close(connfd); |
334 | return -1; |
335 | } |
336 | #endif |
337 | info("connection from %s:%s" , host, port); |
338 | |
339 | return connfd; |
340 | } |
341 | |
342 | int process_request(int listenfd) |
343 | { |
344 | pid_t childpid; |
345 | int connfd; |
346 | |
347 | connfd = do_accept(listenfd); |
348 | if (connfd < 0) |
349 | return -1; |
350 | childpid = fork(); |
351 | if (childpid == 0) { |
352 | close(listenfd); |
353 | recv_pdu(connfd); |
354 | exit(0); |
355 | } |
356 | close(connfd); |
357 | return 0; |
358 | } |
359 | |
360 | static void addrinfo_to_text(struct addrinfo *ai, char buf[], |
361 | const size_t buf_size) |
362 | { |
363 | char hbuf[NI_MAXHOST]; |
364 | char sbuf[NI_MAXSERV]; |
365 | int rc; |
366 | |
367 | buf[0] = '\0'; |
368 | |
369 | rc = getnameinfo(ai->ai_addr, ai->ai_addrlen, hbuf, sizeof(hbuf), |
370 | sbuf, sizeof(sbuf), NI_NUMERICHOST | NI_NUMERICSERV); |
371 | if (rc) |
372 | err("getnameinfo: %s" , gai_strerror(rc)); |
373 | |
374 | snprintf(buf, buf_size, "%s:%s" , hbuf, sbuf); |
375 | } |
376 | |
377 | static int listen_all_addrinfo(struct addrinfo *ai_head, int sockfdlist[], |
378 | int maxsockfd) |
379 | { |
380 | struct addrinfo *ai; |
381 | int ret, nsockfd = 0; |
382 | const size_t ai_buf_size = NI_MAXHOST + NI_MAXSERV + 2; |
383 | char ai_buf[ai_buf_size]; |
384 | |
385 | for (ai = ai_head; ai && nsockfd < maxsockfd; ai = ai->ai_next) { |
386 | int sock; |
387 | |
388 | addrinfo_to_text(ai, ai_buf, ai_buf_size); |
389 | dbg("opening %s" , ai_buf); |
390 | sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); |
391 | if (sock < 0) { |
392 | err("socket: %s: %d (%s)" , |
393 | ai_buf, errno, strerror(errno)); |
394 | continue; |
395 | } |
396 | |
397 | usbip_net_set_reuseaddr(sockfd: sock); |
398 | usbip_net_set_nodelay(sockfd: sock); |
399 | /* We use seperate sockets for IPv4 and IPv6 |
400 | * (see do_standalone_mode()) */ |
401 | usbip_net_set_v6only(sockfd: sock); |
402 | |
403 | ret = bind(sock, ai->ai_addr, ai->ai_addrlen); |
404 | if (ret < 0) { |
405 | err("bind: %s: %d (%s)" , |
406 | ai_buf, errno, strerror(errno)); |
407 | close(sock); |
408 | continue; |
409 | } |
410 | |
411 | ret = listen(sock, SOMAXCONN); |
412 | if (ret < 0) { |
413 | err("listen: %s: %d (%s)" , |
414 | ai_buf, errno, strerror(errno)); |
415 | close(sock); |
416 | continue; |
417 | } |
418 | |
419 | info("listening on %s" , ai_buf); |
420 | sockfdlist[nsockfd++] = sock; |
421 | } |
422 | |
423 | return nsockfd; |
424 | } |
425 | |
426 | static struct addrinfo *do_getaddrinfo(char *host, int ai_family) |
427 | { |
428 | struct addrinfo hints, *ai_head; |
429 | int rc; |
430 | |
431 | memset(&hints, 0, sizeof(hints)); |
432 | hints.ai_family = ai_family; |
433 | hints.ai_socktype = SOCK_STREAM; |
434 | hints.ai_flags = AI_PASSIVE; |
435 | |
436 | rc = getaddrinfo(host, usbip_port_string, &hints, &ai_head); |
437 | if (rc) { |
438 | err("failed to get a network address %s: %s" , usbip_port_string, |
439 | gai_strerror(rc)); |
440 | return NULL; |
441 | } |
442 | |
443 | return ai_head; |
444 | } |
445 | |
446 | static void signal_handler(int i) |
447 | { |
448 | dbg("received '%s' signal" , strsignal(i)); |
449 | } |
450 | |
451 | static void set_signal(void) |
452 | { |
453 | struct sigaction act; |
454 | |
455 | memset(&act, 0, sizeof(act)); |
456 | act.sa_handler = signal_handler; |
457 | sigemptyset(&act.sa_mask); |
458 | sigaction(SIGTERM, &act, NULL); |
459 | sigaction(SIGINT, &act, NULL); |
460 | act.sa_handler = SIG_IGN; |
461 | sigaction(SIGCHLD, &act, NULL); |
462 | } |
463 | |
464 | static const char *pid_file; |
465 | |
466 | static void write_pid_file(void) |
467 | { |
468 | if (pid_file) { |
469 | dbg("creating pid file %s" , pid_file); |
470 | FILE *fp; |
471 | |
472 | fp = fopen(pid_file, "w" ); |
473 | if (!fp) { |
474 | err("pid_file: %s: %d (%s)" , |
475 | pid_file, errno, strerror(errno)); |
476 | return; |
477 | } |
478 | fprintf(fp, "%d\n" , getpid()); |
479 | fclose(fp); |
480 | } |
481 | } |
482 | |
483 | static void remove_pid_file(void) |
484 | { |
485 | if (pid_file) { |
486 | dbg("removing pid file %s" , pid_file); |
487 | unlink(pid_file); |
488 | } |
489 | } |
490 | |
491 | static int do_standalone_mode(int daemonize, int ipv4, int ipv6) |
492 | { |
493 | struct addrinfo *ai_head; |
494 | int sockfdlist[MAXSOCKFD]; |
495 | int nsockfd, family; |
496 | int i, terminate; |
497 | struct pollfd *fds; |
498 | struct timespec timeout; |
499 | sigset_t sigmask; |
500 | |
501 | if (usbip_driver_open(driver)) |
502 | return -1; |
503 | |
504 | if (daemonize) { |
505 | if (daemon(0, 0) < 0) { |
506 | err("daemonizing failed: %s" , strerror(errno)); |
507 | usbip_driver_close(driver); |
508 | return -1; |
509 | } |
510 | umask(0); |
511 | usbip_use_syslog = 1; |
512 | } |
513 | set_signal(); |
514 | write_pid_file(); |
515 | |
516 | info("starting " PROGNAME " (%s)" , usbip_version_string); |
517 | |
518 | /* |
519 | * To suppress warnings on systems with bindv6only disabled |
520 | * (default), we use seperate sockets for IPv6 and IPv4 and set |
521 | * IPV6_V6ONLY on the IPv6 sockets. |
522 | */ |
523 | if (ipv4 && ipv6) |
524 | family = AF_UNSPEC; |
525 | else if (ipv4) |
526 | family = AF_INET; |
527 | else |
528 | family = AF_INET6; |
529 | |
530 | ai_head = do_getaddrinfo(NULL, family); |
531 | if (!ai_head) { |
532 | usbip_driver_close(driver); |
533 | return -1; |
534 | } |
535 | nsockfd = listen_all_addrinfo(ai_head, sockfdlist, |
536 | maxsockfd: sizeof(sockfdlist) / sizeof(*sockfdlist)); |
537 | freeaddrinfo(ai_head); |
538 | if (nsockfd <= 0) { |
539 | err("failed to open a listening socket" ); |
540 | usbip_driver_close(driver); |
541 | return -1; |
542 | } |
543 | |
544 | dbg("listening on %d address%s" , nsockfd, (nsockfd == 1) ? "" : "es" ); |
545 | |
546 | fds = calloc(nsockfd, sizeof(struct pollfd)); |
547 | for (i = 0; i < nsockfd; i++) { |
548 | fds[i].fd = sockfdlist[i]; |
549 | fds[i].events = POLLIN; |
550 | } |
551 | timeout.tv_sec = MAIN_LOOP_TIMEOUT; |
552 | timeout.tv_nsec = 0; |
553 | |
554 | sigfillset(&sigmask); |
555 | sigdelset(&sigmask, SIGTERM); |
556 | sigdelset(&sigmask, SIGINT); |
557 | |
558 | terminate = 0; |
559 | while (!terminate) { |
560 | int r; |
561 | |
562 | r = ppoll(fds, nsockfd, &timeout, &sigmask); |
563 | if (r < 0) { |
564 | dbg("%s" , strerror(errno)); |
565 | terminate = 1; |
566 | } else if (r) { |
567 | for (i = 0; i < nsockfd; i++) { |
568 | if (fds[i].revents & POLLIN) { |
569 | dbg("read event on fd[%d]=%d" , |
570 | i, sockfdlist[i]); |
571 | process_request(listenfd: sockfdlist[i]); |
572 | } |
573 | } |
574 | } else { |
575 | dbg("heartbeat timeout on ppoll()" ); |
576 | } |
577 | } |
578 | |
579 | info("shutting down " PROGNAME); |
580 | free(fds); |
581 | usbip_driver_close(driver); |
582 | |
583 | return 0; |
584 | } |
585 | |
586 | int main(int argc, char *argv[]) |
587 | { |
588 | static const struct option longopts[] = { |
589 | { "ipv4" , no_argument, NULL, '4' }, |
590 | { "ipv6" , no_argument, NULL, '6' }, |
591 | { "daemon" , no_argument, NULL, 'D' }, |
592 | { "daemon" , no_argument, NULL, 'D' }, |
593 | { "debug" , no_argument, NULL, 'd' }, |
594 | { "device" , no_argument, NULL, 'e' }, |
595 | { "pid" , optional_argument, NULL, 'P' }, |
596 | { "tcp-port" , required_argument, NULL, 't' }, |
597 | { "help" , no_argument, NULL, 'h' }, |
598 | { "version" , no_argument, NULL, 'v' }, |
599 | { NULL, 0, NULL, 0 } |
600 | }; |
601 | |
602 | enum { |
603 | cmd_standalone_mode = 1, |
604 | cmd_help, |
605 | cmd_version |
606 | } cmd; |
607 | |
608 | int daemonize = 0; |
609 | int ipv4 = 0, ipv6 = 0; |
610 | int opt, rc = -1; |
611 | |
612 | pid_file = NULL; |
613 | |
614 | usbip_use_stderr = 1; |
615 | usbip_use_syslog = 0; |
616 | |
617 | if (geteuid() != 0) |
618 | err("not running as root?" ); |
619 | |
620 | cmd = cmd_standalone_mode; |
621 | driver = &host_driver; |
622 | for (;;) { |
623 | opt = getopt_long(argc, argv, "46DdeP::t:hv" , longopts, NULL); |
624 | |
625 | if (opt == -1) |
626 | break; |
627 | |
628 | switch (opt) { |
629 | case '4': |
630 | ipv4 = 1; |
631 | break; |
632 | case '6': |
633 | ipv6 = 1; |
634 | break; |
635 | case 'D': |
636 | daemonize = 1; |
637 | break; |
638 | case 'd': |
639 | usbip_use_debug = 1; |
640 | break; |
641 | case 'h': |
642 | cmd = cmd_help; |
643 | break; |
644 | case 'P': |
645 | pid_file = optarg ? optarg : DEFAULT_PID_FILE; |
646 | break; |
647 | case 't': |
648 | usbip_setup_port_number(optarg); |
649 | break; |
650 | case 'v': |
651 | cmd = cmd_version; |
652 | break; |
653 | case 'e': |
654 | driver = &device_driver; |
655 | break; |
656 | case '?': |
657 | usbipd_help(); |
658 | default: |
659 | goto err_out; |
660 | } |
661 | } |
662 | |
663 | if (!ipv4 && !ipv6) |
664 | ipv4 = ipv6 = 1; |
665 | |
666 | switch (cmd) { |
667 | case cmd_standalone_mode: |
668 | rc = do_standalone_mode(daemonize, ipv4, ipv6); |
669 | remove_pid_file(); |
670 | break; |
671 | case cmd_version: |
672 | printf(PROGNAME " (%s)\n" , usbip_version_string); |
673 | rc = 0; |
674 | break; |
675 | case cmd_help: |
676 | usbipd_help(); |
677 | rc = 0; |
678 | break; |
679 | default: |
680 | usbipd_help(); |
681 | goto err_out; |
682 | } |
683 | |
684 | err_out: |
685 | return (rc > -1 ? EXIT_SUCCESS : EXIT_FAILURE); |
686 | } |
687 | |