ViewVC Help
View File | Revision Log | Show Annotations | Revision Graph | Root Listing
root/cebix/BasiliskII/src/slirp/socket.c
Revision: 1.4
Committed: 2006-04-29T10:41:25Z (18 years, 5 months ago) by gbeauche
Content type: text/plain
Branch: MAIN
CVS Tags: nigel-build-19
Changes since 1.3: +2 -1 lines
Log Message:
fix build on win32

File Contents

# User Rev Content
1 gbeauche 1.1 /*
2     * Copyright (c) 1995 Danny Gasparovski.
3     *
4     * Please read the file COPYRIGHT for the
5     * terms and conditions of the copyright.
6     */
7    
8     #define WANT_SYS_IOCTL_H
9     #include <slirp.h>
10     #include "ip_icmp.h"
11     #include "main.h"
12    
13     void
14     so_init()
15     {
16     /* Nothing yet */
17     }
18    
19    
20     struct socket *
21     solookup(head, laddr, lport, faddr, fport)
22     struct socket *head;
23     struct in_addr laddr;
24     u_int lport;
25     struct in_addr faddr;
26     u_int fport;
27     {
28     struct socket *so;
29    
30     for (so = head->so_next; so != head; so = so->so_next) {
31     if (so->so_lport == lport &&
32     so->so_laddr.s_addr == laddr.s_addr &&
33     so->so_faddr.s_addr == faddr.s_addr &&
34     so->so_fport == fport)
35     break;
36     }
37    
38     if (so == head)
39     return (struct socket *)NULL;
40     return so;
41    
42     }
43    
44     /*
45     * Create a new socket, initialise the fields
46     * It is the responsibility of the caller to
47     * insque() it into the correct linked-list
48     */
49     struct socket *
50     socreate()
51     {
52     struct socket *so;
53    
54     so = (struct socket *)malloc(sizeof(struct socket));
55     if(so) {
56     memset(so, 0, sizeof(struct socket));
57     so->so_state = SS_NOFDREF;
58     so->s = -1;
59     }
60     return(so);
61     }
62    
63     /*
64     * remque and free a socket, clobber cache
65     */
66     void
67     sofree(so)
68     struct socket *so;
69     {
70     if (so->so_emu==EMU_RSH && so->extra) {
71     sofree(so->extra);
72     so->extra=NULL;
73     }
74     if (so == tcp_last_so)
75     tcp_last_so = &tcb;
76     else if (so == udp_last_so)
77     udp_last_so = &udb;
78    
79     m_free(so->so_m);
80    
81     if(so->so_next && so->so_prev)
82     remque(so); /* crashes if so is not in a queue */
83    
84     free(so);
85     }
86    
87     /*
88     * Read from so's socket into sb_snd, updating all relevant sbuf fields
89     * NOTE: This will only be called if it is select()ed for reading, so
90     * a read() of 0 (or less) means it's disconnected
91     */
92     int
93     soread(so)
94     struct socket *so;
95     {
96     int n, nn, lss, total;
97     struct sbuf *sb = &so->so_snd;
98     int len = sb->sb_datalen - sb->sb_cc;
99     struct iovec iov[2];
100     int mss = so->so_tcpcb->t_maxseg;
101    
102     DEBUG_CALL("soread");
103     DEBUG_ARG("so = %lx", (long )so);
104    
105     /*
106     * No need to check if there's enough room to read.
107     * soread wouldn't have been called if there weren't
108     */
109    
110     len = sb->sb_datalen - sb->sb_cc;
111    
112     iov[0].iov_base = sb->sb_wptr;
113     if (sb->sb_wptr < sb->sb_rptr) {
114     iov[0].iov_len = sb->sb_rptr - sb->sb_wptr;
115     /* Should never succeed, but... */
116     if (iov[0].iov_len > len)
117     iov[0].iov_len = len;
118     if (iov[0].iov_len > mss)
119     iov[0].iov_len -= iov[0].iov_len%mss;
120     n = 1;
121     } else {
122     iov[0].iov_len = (sb->sb_data + sb->sb_datalen) - sb->sb_wptr;
123     /* Should never succeed, but... */
124     if (iov[0].iov_len > len) iov[0].iov_len = len;
125     len -= iov[0].iov_len;
126     if (len) {
127     iov[1].iov_base = sb->sb_data;
128     iov[1].iov_len = sb->sb_rptr - sb->sb_data;
129     if(iov[1].iov_len > len)
130     iov[1].iov_len = len;
131     total = iov[0].iov_len + iov[1].iov_len;
132     if (total > mss) {
133     lss = total%mss;
134     if (iov[1].iov_len > lss) {
135     iov[1].iov_len -= lss;
136     n = 2;
137     } else {
138     lss -= iov[1].iov_len;
139     iov[0].iov_len -= lss;
140     n = 1;
141     }
142     } else
143     n = 2;
144     } else {
145     if (iov[0].iov_len > mss)
146     iov[0].iov_len -= iov[0].iov_len%mss;
147     n = 1;
148     }
149     }
150    
151     #ifdef HAVE_READV
152     nn = readv(so->s, (struct iovec *)iov, n);
153     DEBUG_MISC((dfd, " ... read nn = %d bytes\n", nn));
154     #else
155     nn = recv(so->s, iov[0].iov_base, iov[0].iov_len,0);
156     #endif
157     if (nn <= 0) {
158     if (nn < 0 && (errno == EINTR || errno == EAGAIN))
159     return 0;
160     else {
161     DEBUG_MISC((dfd, " --- soread() disconnected, nn = %d, errno = %d-%s\n", nn, errno,strerror(errno)));
162     sofcantrcvmore(so);
163     tcp_sockclosed(sototcpcb(so));
164     return -1;
165     }
166     }
167    
168     #ifndef HAVE_READV
169     /*
170     * If there was no error, try and read the second time round
171     * We read again if n = 2 (ie, there's another part of the buffer)
172     * and we read as much as we could in the first read
173     * We don't test for <= 0 this time, because there legitimately
174     * might not be any more data (since the socket is non-blocking),
175     * a close will be detected on next iteration.
176     * A return of -1 wont (shouldn't) happen, since it didn't happen above
177     */
178     if (n == 2 && nn == iov[0].iov_len) {
179     int ret;
180     ret = recv(so->s, iov[1].iov_base, iov[1].iov_len,0);
181     if (ret > 0)
182     nn += ret;
183     }
184    
185     DEBUG_MISC((dfd, " ... read nn = %d bytes\n", nn));
186     #endif
187    
188     /* Update fields */
189     sb->sb_cc += nn;
190     sb->sb_wptr += nn;
191     if (sb->sb_wptr >= (sb->sb_data + sb->sb_datalen))
192     sb->sb_wptr -= sb->sb_datalen;
193     return nn;
194     }
195    
196     /*
197     * Get urgent data
198     *
199     * When the socket is created, we set it SO_OOBINLINE,
200     * so when OOB data arrives, we soread() it and everything
201     * in the send buffer is sent as urgent data
202     */
203     void
204     sorecvoob(so)
205     struct socket *so;
206     {
207     struct tcpcb *tp = sototcpcb(so);
208    
209     DEBUG_CALL("sorecvoob");
210     DEBUG_ARG("so = %lx", (long)so);
211    
212     /*
213     * We take a guess at how much urgent data has arrived.
214     * In most situations, when urgent data arrives, the next
215     * read() should get all the urgent data. This guess will
216     * be wrong however if more data arrives just after the
217     * urgent data, or the read() doesn't return all the
218     * urgent data.
219     */
220     soread(so);
221     tp->snd_up = tp->snd_una + so->so_snd.sb_cc;
222     tp->t_force = 1;
223     tcp_output(tp);
224     tp->t_force = 0;
225     }
226    
227     /*
228     * Send urgent data
229     * There's a lot duplicated code here, but...
230     */
231     int
232     sosendoob(so)
233     struct socket *so;
234     {
235     struct sbuf *sb = &so->so_rcv;
236     char buff[2048]; /* XXX Shouldn't be sending more oob data than this */
237    
238     int n, len;
239    
240     DEBUG_CALL("sosendoob");
241     DEBUG_ARG("so = %lx", (long)so);
242     DEBUG_ARG("sb->sb_cc = %d", sb->sb_cc);
243    
244     if (so->so_urgc > 2048)
245     so->so_urgc = 2048; /* XXXX */
246    
247     if (sb->sb_rptr < sb->sb_wptr) {
248     /* We can send it directly */
249     n = send(so->s, sb->sb_rptr, so->so_urgc, (MSG_OOB)); /* |MSG_DONTWAIT)); */
250     so->so_urgc -= n;
251    
252     DEBUG_MISC((dfd, " --- sent %d bytes urgent data, %d urgent bytes left\n", n, so->so_urgc));
253     } else {
254     /*
255     * Since there's no sendv or sendtov like writev,
256     * we must copy all data to a linear buffer then
257     * send it all
258     */
259     len = (sb->sb_data + sb->sb_datalen) - sb->sb_rptr;
260     if (len > so->so_urgc) len = so->so_urgc;
261     memcpy(buff, sb->sb_rptr, len);
262     so->so_urgc -= len;
263     if (so->so_urgc) {
264     n = sb->sb_wptr - sb->sb_data;
265     if (n > so->so_urgc) n = so->so_urgc;
266     memcpy((buff + len), sb->sb_data, n);
267     so->so_urgc -= n;
268     len += n;
269     }
270     n = send(so->s, buff, len, (MSG_OOB)); /* |MSG_DONTWAIT)); */
271     #ifdef DEBUG
272     if (n != len)
273     DEBUG_ERROR((dfd, "Didn't send all data urgently XXXXX\n"));
274     #endif
275     DEBUG_MISC((dfd, " ---2 sent %d bytes urgent data, %d urgent bytes left\n", n, so->so_urgc));
276     }
277    
278     sb->sb_cc -= n;
279     sb->sb_rptr += n;
280     if (sb->sb_rptr >= (sb->sb_data + sb->sb_datalen))
281     sb->sb_rptr -= sb->sb_datalen;
282    
283     return n;
284     }
285    
286     /*
287     * Write data from so_rcv to so's socket,
288     * updating all sbuf field as necessary
289     */
290     int
291     sowrite(so)
292     struct socket *so;
293     {
294     int n,nn;
295     struct sbuf *sb = &so->so_rcv;
296     int len = sb->sb_cc;
297     struct iovec iov[2];
298    
299     DEBUG_CALL("sowrite");
300     DEBUG_ARG("so = %lx", (long)so);
301    
302     if (so->so_urgc) {
303     sosendoob(so);
304     if (sb->sb_cc == 0)
305     return 0;
306     }
307    
308     /*
309     * No need to check if there's something to write,
310     * sowrite wouldn't have been called otherwise
311     */
312    
313     len = sb->sb_cc;
314    
315     iov[0].iov_base = sb->sb_rptr;
316     if (sb->sb_rptr < sb->sb_wptr) {
317     iov[0].iov_len = sb->sb_wptr - sb->sb_rptr;
318     /* Should never succeed, but... */
319     if (iov[0].iov_len > len) iov[0].iov_len = len;
320     n = 1;
321     } else {
322     iov[0].iov_len = (sb->sb_data + sb->sb_datalen) - sb->sb_rptr;
323     if (iov[0].iov_len > len) iov[0].iov_len = len;
324     len -= iov[0].iov_len;
325     if (len) {
326     iov[1].iov_base = sb->sb_data;
327     iov[1].iov_len = sb->sb_wptr - sb->sb_data;
328     if (iov[1].iov_len > len) iov[1].iov_len = len;
329     n = 2;
330     } else
331     n = 1;
332     }
333     /* Check if there's urgent data to send, and if so, send it */
334    
335     #ifdef HAVE_READV
336     nn = writev(so->s, (const struct iovec *)iov, n);
337    
338     DEBUG_MISC((dfd, " ... wrote nn = %d bytes\n", nn));
339     #else
340     nn = send(so->s, iov[0].iov_base, iov[0].iov_len,0);
341     #endif
342     /* This should never happen, but people tell me it does *shrug* */
343     if (nn < 0 && (errno == EAGAIN || errno == EINTR))
344     return 0;
345    
346     if (nn <= 0) {
347     DEBUG_MISC((dfd, " --- sowrite disconnected, so->so_state = %x, errno = %d\n",
348     so->so_state, errno));
349     sofcantsendmore(so);
350     tcp_sockclosed(sototcpcb(so));
351     return -1;
352     }
353    
354     #ifndef HAVE_READV
355     if (n == 2 && nn == iov[0].iov_len) {
356     int ret;
357     ret = send(so->s, iov[1].iov_base, iov[1].iov_len,0);
358     if (ret > 0)
359     nn += ret;
360     }
361     DEBUG_MISC((dfd, " ... wrote nn = %d bytes\n", nn));
362     #endif
363    
364     /* Update sbuf */
365     sb->sb_cc -= nn;
366     sb->sb_rptr += nn;
367     if (sb->sb_rptr >= (sb->sb_data + sb->sb_datalen))
368     sb->sb_rptr -= sb->sb_datalen;
369    
370     /*
371     * If in DRAIN mode, and there's no more data, set
372     * it CANTSENDMORE
373     */
374     if ((so->so_state & SS_FWDRAIN) && sb->sb_cc == 0)
375     sofcantsendmore(so);
376    
377     return nn;
378     }
379    
380     /*
381     * recvfrom() a UDP socket
382     */
383     void
384     sorecvfrom(so)
385     struct socket *so;
386     {
387     struct sockaddr_in addr;
388 cebix 1.2 socklen_t addrlen = sizeof(struct sockaddr_in);
389 gbeauche 1.1
390     DEBUG_CALL("sorecvfrom");
391     DEBUG_ARG("so = %lx", (long)so);
392    
393     if (so->so_type == IPPROTO_ICMP) { /* This is a "ping" reply */
394     char buff[256];
395     int len;
396    
397     len = recvfrom(so->s, buff, 256, 0,
398     (struct sockaddr *)&addr, &addrlen);
399     /* XXX Check if reply is "correct"? */
400    
401     if(len == -1 || len == 0) {
402     u_char code=ICMP_UNREACH_PORT;
403    
404     if(errno == EHOSTUNREACH) code=ICMP_UNREACH_HOST;
405     else if(errno == ENETUNREACH) code=ICMP_UNREACH_NET;
406    
407     DEBUG_MISC((dfd," udp icmp rx errno = %d-%s\n",
408     errno,strerror(errno)));
409     icmp_error(so->so_m, ICMP_UNREACH,code, 0,strerror(errno));
410     } else {
411     icmp_reflect(so->so_m);
412     so->so_m = 0; /* Don't m_free() it again! */
413     }
414     /* No need for this socket anymore, udp_detach it */
415     udp_detach(so);
416     } else { /* A "normal" UDP packet */
417     struct mbuf *m;
418 gbeauche 1.4 int len;
419     ioctlsockopt_t n;
420 gbeauche 1.1
421     if (!(m = m_get())) return;
422     m->m_data += if_maxlinkhdr;
423    
424     /*
425     * XXX Shouldn't FIONREAD packets destined for port 53,
426     * but I don't know the max packet size for DNS lookups
427     */
428     len = M_FREEROOM(m);
429     /* if (so->so_fport != htons(53)) { */
430     ioctlsocket(so->s, FIONREAD, &n);
431    
432     if (n > len) {
433     n = (m->m_data - m->m_dat) + m->m_len + n + 1;
434     m_inc(m, n);
435     len = M_FREEROOM(m);
436     }
437     /* } */
438    
439     m->m_len = recvfrom(so->s, m->m_data, len, 0,
440     (struct sockaddr *)&addr, &addrlen);
441     DEBUG_MISC((dfd, " did recvfrom %d, errno = %d-%s\n",
442     m->m_len, errno,strerror(errno)));
443     if(m->m_len<0) {
444     u_char code=ICMP_UNREACH_PORT;
445    
446     if(errno == EHOSTUNREACH) code=ICMP_UNREACH_HOST;
447     else if(errno == ENETUNREACH) code=ICMP_UNREACH_NET;
448    
449     DEBUG_MISC((dfd," rx error, tx icmp ICMP_UNREACH:%i\n", code));
450     icmp_error(so->so_m, ICMP_UNREACH,code, 0,strerror(errno));
451     m_free(m);
452     } else {
453     /*
454     * Hack: domain name lookup will be used the most for UDP,
455     * and since they'll only be used once there's no need
456     * for the 4 minute (or whatever) timeout... So we time them
457     * out much quicker (10 seconds for now...)
458     */
459     if (so->so_expire) {
460     if (so->so_fport == htons(53))
461     so->so_expire = curtime + SO_EXPIREFAST;
462     else
463     so->so_expire = curtime + SO_EXPIRE;
464     }
465    
466     /* if (m->m_len == len) {
467     * m_inc(m, MINCSIZE);
468     * m->m_len = 0;
469     * }
470     */
471    
472     /*
473     * If this packet was destined for CTL_ADDR,
474     * make it look like that's where it came from, done by udp_output
475     */
476     udp_output(so, m, &addr);
477     } /* rx error */
478     } /* if ping packet */
479     }
480    
481     /*
482     * sendto() a socket
483     */
484     int
485     sosendto(so, m)
486     struct socket *so;
487     struct mbuf *m;
488     {
489     int ret;
490     struct sockaddr_in addr;
491    
492     DEBUG_CALL("sosendto");
493     DEBUG_ARG("so = %lx", (long)so);
494     DEBUG_ARG("m = %lx", (long)m);
495    
496     addr.sin_family = AF_INET;
497     if ((so->so_faddr.s_addr & htonl(0xffffff00)) == special_addr.s_addr) {
498     /* It's an alias */
499     switch(ntohl(so->so_faddr.s_addr) & 0xff) {
500     case CTL_DNS:
501     addr.sin_addr = dns_addr;
502     break;
503     case CTL_ALIAS:
504     default:
505     addr.sin_addr = loopback_addr;
506     break;
507     }
508     } else
509     addr.sin_addr = so->so_faddr;
510     addr.sin_port = so->so_fport;
511    
512     DEBUG_MISC((dfd, " sendto()ing, addr.sin_port=%d, addr.sin_addr.s_addr=%.16s\n", ntohs(addr.sin_port), inet_ntoa(addr.sin_addr)));
513    
514     /* Don't care what port we get */
515     ret = sendto(so->s, m->m_data, m->m_len, 0,
516     (struct sockaddr *)&addr, sizeof (struct sockaddr));
517     if (ret < 0)
518     return -1;
519    
520     /*
521     * Kill the socket if there's no reply in 4 minutes,
522     * but only if it's an expirable socket
523     */
524     if (so->so_expire)
525     so->so_expire = curtime + SO_EXPIRE;
526     so->so_state = SS_ISFCONNECTED; /* So that it gets select()ed */
527     return 0;
528     }
529    
530     /*
531     * XXX This should really be tcp_listen
532     */
533     struct socket *
534     solisten(port, laddr, lport, flags)
535     u_int port;
536     u_int32_t laddr;
537     u_int lport;
538     int flags;
539     {
540     struct sockaddr_in addr;
541     struct socket *so;
542 cebix 1.2 int s;
543     socklen_t addrlen = sizeof(addr);
544     int opt = 1;
545 gbeauche 1.1
546     DEBUG_CALL("solisten");
547     DEBUG_ARG("port = %d", port);
548     DEBUG_ARG("laddr = %x", laddr);
549     DEBUG_ARG("lport = %d", lport);
550     DEBUG_ARG("flags = %x", flags);
551    
552     if ((so = socreate()) == NULL) {
553     /* free(so); Not sofree() ??? free(NULL) == NOP */
554     return NULL;
555     }
556    
557     /* Don't tcp_attach... we don't need so_snd nor so_rcv */
558     if ((so->so_tcpcb = tcp_newtcpcb(so)) == NULL) {
559     free(so);
560     return NULL;
561     }
562     insque(so,&tcb);
563    
564     /*
565     * SS_FACCEPTONCE sockets must time out.
566     */
567     if (flags & SS_FACCEPTONCE)
568     so->so_tcpcb->t_timer[TCPT_KEEP] = TCPTV_KEEP_INIT*2;
569    
570     so->so_state = (SS_FACCEPTCONN|flags);
571     so->so_lport = lport; /* Kept in network format */
572     so->so_laddr.s_addr = laddr; /* Ditto */
573    
574     addr.sin_family = AF_INET;
575     addr.sin_addr.s_addr = INADDR_ANY;
576     addr.sin_port = port;
577    
578     if (((s = socket(AF_INET,SOCK_STREAM,0)) < 0) ||
579 gbeauche 1.3 (setsockopt(s,SOL_SOCKET,SO_REUSEADDR,(char *)&opt,sizeof(int)) < 0) ||
580 gbeauche 1.1 (bind(s,(struct sockaddr *)&addr, sizeof(addr)) < 0) ||
581     (listen(s,1) < 0)) {
582     int tmperrno = errno; /* Don't clobber the real reason we failed */
583    
584     close(s);
585     sofree(so);
586     /* Restore the real errno */
587     #ifdef _WIN32
588     WSASetLastError(tmperrno);
589     #else
590     errno = tmperrno;
591     #endif
592     return NULL;
593     }
594     setsockopt(s,SOL_SOCKET,SO_OOBINLINE,(char *)&opt,sizeof(int));
595    
596     getsockname(s,(struct sockaddr *)&addr,&addrlen);
597     so->so_fport = addr.sin_port;
598     if (addr.sin_addr.s_addr == 0 || addr.sin_addr.s_addr == loopback_addr.s_addr)
599     so->so_faddr = our_addr;
600     else
601     so->so_faddr = addr.sin_addr;
602    
603     so->s = s;
604     return so;
605     }
606    
607     /*
608     * Data is available in so_rcv
609     * Just write() the data to the socket
610     * XXX not yet...
611     */
612     void
613     sorwakeup(so)
614     struct socket *so;
615     {
616     /* sowrite(so); */
617     /* FD_CLR(so->s,&writefds); */
618     }
619    
620     /*
621     * Data has been freed in so_snd
622     * We have room for a read() if we want to
623     * For now, don't read, it'll be done in the main loop
624     */
625     void
626     sowwakeup(so)
627     struct socket *so;
628     {
629     /* Nothing, yet */
630     }
631    
632     /*
633     * Various session state calls
634     * XXX Should be #define's
635     * The socket state stuff needs work, these often get call 2 or 3
636     * times each when only 1 was needed
637     */
638     void
639     soisfconnecting(so)
640     register struct socket *so;
641     {
642     so->so_state &= ~(SS_NOFDREF|SS_ISFCONNECTED|SS_FCANTRCVMORE|
643     SS_FCANTSENDMORE|SS_FWDRAIN);
644     so->so_state |= SS_ISFCONNECTING; /* Clobber other states */
645     }
646    
647     void
648     soisfconnected(so)
649     register struct socket *so;
650     {
651     so->so_state &= ~(SS_ISFCONNECTING|SS_FWDRAIN|SS_NOFDREF);
652     so->so_state |= SS_ISFCONNECTED; /* Clobber other states */
653     }
654    
655     void
656     sofcantrcvmore(so)
657     struct socket *so;
658     {
659     if ((so->so_state & SS_NOFDREF) == 0) {
660     shutdown(so->s,0);
661     if(global_writefds) {
662     FD_CLR(so->s,global_writefds);
663     }
664     }
665     so->so_state &= ~(SS_ISFCONNECTING);
666     if (so->so_state & SS_FCANTSENDMORE)
667     so->so_state = SS_NOFDREF; /* Don't select it */ /* XXX close() here as well? */
668     else
669     so->so_state |= SS_FCANTRCVMORE;
670     }
671    
672     void
673     sofcantsendmore(so)
674     struct socket *so;
675     {
676     if ((so->so_state & SS_NOFDREF) == 0) {
677     shutdown(so->s,1); /* send FIN to fhost */
678     if (global_readfds) {
679     FD_CLR(so->s,global_readfds);
680     }
681     if (global_xfds) {
682     FD_CLR(so->s,global_xfds);
683     }
684     }
685     so->so_state &= ~(SS_ISFCONNECTING);
686     if (so->so_state & SS_FCANTRCVMORE)
687     so->so_state = SS_NOFDREF; /* as above */
688     else
689     so->so_state |= SS_FCANTSENDMORE;
690     }
691    
692     void
693     soisfdisconnected(so)
694     struct socket *so;
695     {
696     /* so->so_state &= ~(SS_ISFCONNECTING|SS_ISFCONNECTED); */
697     /* close(so->s); */
698     /* so->so_state = SS_ISFDISCONNECTED; */
699     /*
700     * XXX Do nothing ... ?
701     */
702     }
703    
704     /*
705     * Set write drain mode
706     * Set CANTSENDMORE once all data has been write()n
707     */
708     void
709     sofwdrain(so)
710     struct socket *so;
711     {
712     if (so->so_rcv.sb_cc)
713     so->so_state |= SS_FWDRAIN;
714     else
715     sofcantsendmore(so);
716     }
717