* peer_addr is now const * new_link_udp_server() can bind to address or INADDR_ANY * socket_udp_receive() happens without a loop * socket_udp_send() happens without a loop * sending in two packets (first size, then data) is only performed for TCP, SCTP, not for UDP anymore (it is unreliable, so we could miss something and will receive complete garbage)
643 lines
16 KiB
C
643 lines
16 KiB
C
/*
|
|
* Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
|
|
* contributor license agreements. See the NOTICE file distributed with
|
|
* this work for additional information regarding copyright ownership.
|
|
* The OpenAirInterface Software Alliance licenses this file to You under
|
|
* the OAI Public License, Version 1.1 (the "License"); you may not use this file
|
|
* except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.openairinterface.org/?page_id=698
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*-------------------------------------------------------------------------------
|
|
* For more information about the OpenAirInterface (OAI) Software Alliance:
|
|
* contact@openairinterface.org
|
|
*/
|
|
|
|
/*! \file socket_link.c
|
|
* \brief this is the implementation of a TCP socket ASYNC IF
|
|
* \author Cedric Roux
|
|
* \date November 2015
|
|
* \version 1.0
|
|
* \email: cedric.roux@eurecom.fr
|
|
* @ingroup _mac
|
|
*/
|
|
|
|
#include "socket_link.h"
|
|
#include "common/utils/LOG/log.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <unistd.h>
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <netinet/ip.h>
|
|
#include <netinet/tcp.h>
|
|
#include <arpa/inet.h>
|
|
#include <stdint.h>
|
|
|
|
socket_link_t *new_link_server(int port)
|
|
{
|
|
socket_link_t *ret = NULL;
|
|
int reuse;
|
|
struct sockaddr_in addr;
|
|
socklen_t addrlen;
|
|
int socket_server = -1;
|
|
int no_delay;
|
|
|
|
|
|
ret = calloc(1, sizeof(socket_link_t));
|
|
if (ret == NULL) {
|
|
LOG_E(MAC, "%s:%d: out of memory\n", __FILE__, __LINE__);
|
|
goto error;
|
|
}
|
|
ret->socket_fd = -1;
|
|
|
|
|
|
//printf("MAC create a new link server socket at port %d\n", port);
|
|
|
|
socket_server = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (socket_server == -1) {
|
|
LOG_E(MAC, "%s:%d: socket: %s\n", __FILE__, __LINE__, strerror(errno));
|
|
goto error;
|
|
}
|
|
|
|
reuse = 1;
|
|
if (setsockopt(socket_server, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1) {
|
|
|
|
LOG_E(MAC, "%s:%d: setsockopt: %s\n", __FILE__, __LINE__, strerror(errno));
|
|
goto error;
|
|
}
|
|
|
|
no_delay = 1;
|
|
if (setsockopt(socket_server, IPPROTO_TCP, TCP_NODELAY, &no_delay, sizeof(no_delay)) == -1) {
|
|
|
|
LOG_E(MAC, "%s:%d: setsockopt: %s\n", __FILE__, __LINE__, strerror(errno));
|
|
goto error;
|
|
}
|
|
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_port = htons(port);
|
|
addr.sin_addr.s_addr = INADDR_ANY;
|
|
if (bind(socket_server, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
|
|
LOG_E(MAC, "%s:%d: bind: %s\n", __FILE__, __LINE__, strerror(errno));
|
|
goto error;
|
|
}
|
|
|
|
if (listen(socket_server, 5)) {
|
|
LOG_E(MAC, "%s:%d: listen: %s\n", __FILE__, __LINE__, strerror(errno));
|
|
goto error;
|
|
}
|
|
|
|
addrlen = sizeof(addr);
|
|
ret->socket_fd = accept(socket_server, (struct sockaddr *)&addr, &addrlen);
|
|
if (ret->socket_fd == -1) {
|
|
LOG_E(MAC, "%s:%d: accept: %s\n", __FILE__, __LINE__, strerror(errno));
|
|
goto error;
|
|
}
|
|
|
|
//printf("MAC connection from %s:%d\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
|
|
return ret;
|
|
|
|
error:
|
|
close(socket_server);
|
|
if (ret != NULL) close(ret->socket_fd);
|
|
free(ret);
|
|
|
|
LOG_E(MAC, "ERROR in new_link_server (see above), returning NULL\n");
|
|
return NULL;
|
|
}
|
|
|
|
socket_link_t *new_link_client(const char *server, int port)
|
|
{
|
|
socket_link_t *ret = NULL;
|
|
struct sockaddr_in addr;
|
|
int no_delay;
|
|
|
|
ret = calloc(1, sizeof(socket_link_t));
|
|
if (ret == NULL) {
|
|
LOG_E(MAC, "%s:%d: out of memory\n", __FILE__, __LINE__);
|
|
goto error;
|
|
}
|
|
ret->socket_fd = -1;
|
|
|
|
LOG_D(MAC, "create a new link client socket connecting to %s:%d\n", server, port);
|
|
|
|
ret->socket_fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (ret->socket_fd == -1) {
|
|
LOG_E(MAC, "%s:%d: socket: %s\n", __FILE__, __LINE__, strerror(errno));
|
|
goto error;
|
|
}
|
|
|
|
no_delay = 1;
|
|
if (setsockopt(ret->socket_fd, SOL_TCP, TCP_NODELAY, &no_delay, sizeof(no_delay)) == -1) {
|
|
LOG_E(MAC, "%s:%d: setsockopt: %s\n", __FILE__, __LINE__, strerror(errno));
|
|
goto error;
|
|
}
|
|
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_port = htons(port);
|
|
if (inet_aton(server, &addr.sin_addr) == 0) {
|
|
LOG_E(MAC, "invalid IP address '%s', use a.b.c.d notation\n", server);
|
|
goto error;
|
|
}
|
|
if (connect(ret->socket_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
|
|
LOG_E(MAC, "%s:%d: connect: %s\n", __FILE__, __LINE__, strerror(errno));
|
|
goto error;
|
|
}
|
|
|
|
LOG_D(MAC, "connection to %s:%d established\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
|
|
return ret;
|
|
|
|
error:
|
|
if (ret != NULL) close(ret->socket_fd);
|
|
free(ret);
|
|
LOG_E(MAC, "ERROR in new_link_client (see above), returning NULL\n");
|
|
return NULL;
|
|
}
|
|
|
|
socket_link_t *new_link_udp_server(const char *bind_addr, int bind_port)
|
|
{
|
|
socket_link_t *ret = NULL;
|
|
|
|
struct sockaddr_in si_me;
|
|
int socket_server = -1;
|
|
|
|
ret = calloc(1, sizeof(socket_link_t));
|
|
if (ret == NULL) {
|
|
LOG_D(PROTO_AGENT, "%s:%d: out of memory\n", __FILE__, __LINE__);
|
|
goto error;
|
|
}
|
|
ret->socket_fd = -1;
|
|
|
|
//printf("PROTO_AGENT: create a new udp link server socket at port %d\n", port);
|
|
|
|
//create a UDP socket
|
|
if ((socket_server=socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == -1) {
|
|
goto error;
|
|
}
|
|
|
|
// zero out the structure
|
|
memset((char *) &si_me, 0, sizeof(si_me));
|
|
|
|
si_me.sin_family = AF_INET;
|
|
si_me.sin_port = htons(bind_port);
|
|
if (bind_addr) {
|
|
if (!inet_aton(bind_addr, &si_me.sin_addr))
|
|
goto error;
|
|
} else {
|
|
si_me.sin_addr.s_addr = INADDR_ANY;
|
|
}
|
|
|
|
//bind socket to port
|
|
if( bind(socket_server , (struct sockaddr*)&si_me, sizeof(si_me) ) == -1) {
|
|
fprintf(stderr, "could not bind to address %s: %s\n", bind_addr, strerror(errno));
|
|
goto error;
|
|
}
|
|
ret->socket_fd = socket_server;
|
|
ret->peer_port = 0;
|
|
return ret;
|
|
|
|
error:
|
|
if (socket_server != -1) close(socket_server);
|
|
if (ret != NULL) close(ret->socket_fd);
|
|
free(ret);
|
|
//printf("\n\n\nERROR PROTO_AGENT: ERROR in new_link_udp_server (see above), returning NULL\n");
|
|
return NULL;
|
|
}
|
|
|
|
|
|
socket_link_t *new_link_udp_client(const char *server, int port){
|
|
|
|
socket_link_t *ret = NULL;
|
|
ret = calloc(1, sizeof(socket_link_t));
|
|
if (ret == NULL) {
|
|
LOG_E(MAC, "%s:%d: out of memory\n", __FILE__, __LINE__);
|
|
//goto error;
|
|
}
|
|
ret->socket_fd = -1;
|
|
|
|
struct sockaddr_in si_other;
|
|
int s;
|
|
socklen_t slen;
|
|
|
|
if ( (s=socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == -1){
|
|
goto error;
|
|
}
|
|
|
|
memset((char *) &si_other, 0, sizeof(si_other));
|
|
si_other.sin_family = AF_INET;
|
|
si_other.sin_port = 0; //htons(port);
|
|
|
|
|
|
|
|
if (inet_aton(server, &si_other.sin_addr) == 0){
|
|
fprintf(stderr, "inet_aton() failed\n");
|
|
goto error;
|
|
}
|
|
connect(s, (struct sockaddr *)&si_other, sizeof(si_other));
|
|
|
|
getsockname(s, (struct sockaddr *)&si_other, &slen);
|
|
|
|
ret->socket_fd = s;
|
|
ret->peer_port = port; //ntohs(si_other.sin_port);
|
|
|
|
return ret;
|
|
error:
|
|
if (ret != NULL) close(ret->socket_fd);
|
|
free(ret);
|
|
LOG_E(MAC, "ERROR in new_link_udp_client (see above), returning NULL\n");
|
|
return NULL;
|
|
}
|
|
|
|
|
|
socket_link_t *new_link_sctp_server(int port)
|
|
{
|
|
|
|
socket_link_t *ret = NULL;
|
|
|
|
int listenSock = -1, temp;
|
|
struct sockaddr_in servaddr;
|
|
|
|
listenSock = socket (AF_INET, SOCK_STREAM, IPPROTO_SCTP);
|
|
if(listenSock == -1)
|
|
{
|
|
perror("socket()");
|
|
exit(1);
|
|
}
|
|
|
|
bzero ((void *) &servaddr, sizeof (servaddr));
|
|
servaddr.sin_family = AF_INET;
|
|
servaddr.sin_addr.s_addr = INADDR_ANY;
|
|
servaddr.sin_port = htons(port);
|
|
|
|
temp = bind (listenSock, (struct sockaddr *) &servaddr, sizeof (servaddr));
|
|
|
|
if(temp == -1 )
|
|
{
|
|
perror("bind()");
|
|
close(listenSock);
|
|
exit(1);
|
|
}
|
|
|
|
temp = listen (listenSock, 5);
|
|
if(temp == -1 )
|
|
{
|
|
perror("listen()");
|
|
close(listenSock);
|
|
exit(1);
|
|
}
|
|
ret = calloc(1, sizeof(socket_link_t));
|
|
if (ret == NULL) {
|
|
LOG_D(PROTO_AGENT, "%s:%d: out of memory\n", __FILE__, __LINE__);
|
|
goto error;
|
|
}
|
|
ret->socket_fd = -1;
|
|
|
|
ret->socket_fd = accept (listenSock, NULL, NULL);
|
|
|
|
return ret;
|
|
|
|
error:
|
|
if (listenSock != -1) close(listenSock);
|
|
if (ret != NULL) close(ret->socket_fd);
|
|
free(ret);
|
|
LOG_E(MAC,"ERROR in new_link_sctp_server (see above), returning NULL\n");
|
|
return NULL;
|
|
}
|
|
|
|
socket_link_t *new_link_sctp_client(const char *server, int port)
|
|
{
|
|
|
|
socket_link_t *ret = NULL;
|
|
ret = calloc(1, sizeof(socket_link_t));
|
|
if (ret == NULL) {
|
|
LOG_D(PROTO_AGENT, "%s:%d: out of memory\n", __FILE__, __LINE__);
|
|
goto error;
|
|
}
|
|
ret->socket_fd = -1;
|
|
|
|
int temp;
|
|
struct sockaddr_in servaddr;
|
|
|
|
ret->socket_fd = socket (AF_INET, SOCK_STREAM, IPPROTO_SCTP);
|
|
|
|
if (ret->socket_fd == -1)
|
|
{
|
|
perror("socket()");
|
|
exit(1);
|
|
}
|
|
|
|
bzero ((void *) &servaddr, sizeof (servaddr));
|
|
servaddr.sin_family = AF_INET;
|
|
servaddr.sin_port = htons (port);
|
|
|
|
if (inet_aton(server, &servaddr.sin_addr) == 0) {
|
|
LOG_E(MAC,"invalid IP address '%s', use a.b.c.d notation\n", server);
|
|
goto error;
|
|
}
|
|
|
|
temp = connect (ret->socket_fd, (struct sockaddr *) &servaddr, sizeof (servaddr));
|
|
|
|
if (temp == -1)
|
|
{
|
|
perror("connect()");
|
|
close(ret->socket_fd);
|
|
exit(1);
|
|
}
|
|
|
|
return ret;
|
|
|
|
error:
|
|
if (ret != NULL) close(ret->socket_fd);
|
|
free(ret);
|
|
LOG_E(MAC, "ERROR in new_link_sctp_client (see above), returning NULL\n");
|
|
return NULL;
|
|
}
|
|
|
|
static int socket_udp_send(int socket_fd, void *buf, int size, const char *peer_addr, int port)
|
|
{
|
|
struct sockaddr_in si_other;
|
|
int slen = sizeof(si_other);
|
|
int l;
|
|
int my_socket;
|
|
|
|
LOG_D(PROTO_AGENT,"UDP send\n");
|
|
|
|
my_socket = socket_fd;
|
|
memset((char *) &si_other, 0, sizeof(si_other));
|
|
si_other.sin_family = AF_INET;
|
|
si_other.sin_port = htons(port);
|
|
|
|
if (inet_aton(peer_addr , &si_other.sin_addr) == 0)
|
|
{
|
|
fprintf(stderr, "inet_aton() failed\n");
|
|
exit(1);
|
|
}
|
|
|
|
l = sendto(my_socket, buf, size, 0, (struct sockaddr *) &si_other, slen);
|
|
if (l == -1) goto error;
|
|
|
|
return l;
|
|
error:
|
|
LOG_E(MAC,"socket_udp_send: ERROR: %s\n", strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
static int socket_udp_receive(int socket_fd, void *buf, int size)
|
|
{
|
|
LOG_D(PROTO_AGENT,"UDP RECEIVE\n");
|
|
|
|
struct sockaddr_in client;
|
|
socklen_t slen;
|
|
int l;
|
|
|
|
l = recvfrom(socket_fd, buf, size, 0, (struct sockaddr *) &client, &slen);
|
|
//getsockname(socket_fd, (struct sockaddr *)&client, &slen);
|
|
if (l == -1) goto error;
|
|
if (l == 0) goto socket_closed;
|
|
|
|
return l;
|
|
|
|
error:
|
|
LOG_E(MAC, "socket_udp_receive: ERROR: %s\n", strerror(errno));
|
|
return -1;
|
|
|
|
socket_closed:
|
|
LOG_E(MAC, "socket_udp_receive: socket closed\n");
|
|
return -1;
|
|
}
|
|
|
|
|
|
/*
|
|
* return -1 on error and 0 if the sending was fine
|
|
*/
|
|
static int socket_send(int socket_fd, void *buf, int size)
|
|
{
|
|
char *s = buf;
|
|
int l;
|
|
|
|
while (size) {
|
|
l = send(socket_fd, s, size, MSG_NOSIGNAL);
|
|
if (l == -1) goto error;
|
|
if (l == 0) { LOG_E(MAC, "%s:%d: this cannot happen, normally...\n", __FILE__, __LINE__); abort(); }
|
|
size -= l;
|
|
s += l;
|
|
}
|
|
|
|
return 0;
|
|
|
|
error:
|
|
LOG_E(MAC, "socket_send: ERROR: %s\n", strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* return -1 on error and 0 if the receiving was fine
|
|
*/
|
|
static int socket_receive(int socket_fd, void *buf, int size)
|
|
{
|
|
char *s = buf;
|
|
int l;
|
|
|
|
while (size) {
|
|
l = read(socket_fd, s, size);
|
|
if (l == -1) goto error;
|
|
if (l == 0) goto socket_closed;
|
|
size -= l;
|
|
s += l;
|
|
}
|
|
|
|
return 0;
|
|
|
|
error:
|
|
LOG_E(MAC, "socket_receive: ERROR: %s\n", strerror(errno));
|
|
return -1;
|
|
|
|
socket_closed:
|
|
LOG_E(MAC, "socket_receive: socket closed\n");
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* return -1 on error and 0 if the sending was fine
|
|
*/
|
|
int link_send_packet(socket_link_t *link, void *data, int size, uint16_t proto_type, const char *peer_addr, int peer_port)
|
|
{
|
|
char sizebuf[4];
|
|
int32_t s = size;
|
|
|
|
if ((proto_type == 0) || (proto_type == 2))
|
|
{
|
|
/* send the size first, maximum is 2^31 bytes */
|
|
sizebuf[0] = (s >> 24) & 255;
|
|
sizebuf[1] = (s >> 16) & 255;
|
|
sizebuf[2] = (s >> 8) & 255;
|
|
sizebuf[3] = s & 255;
|
|
if (socket_send(link->socket_fd, sizebuf, 4) == -1)
|
|
goto error;
|
|
|
|
link->bytes_sent += 4;
|
|
|
|
if (socket_send(link->socket_fd, data, size) == -1)
|
|
goto error;
|
|
|
|
}
|
|
else if (proto_type == 1 )
|
|
{
|
|
/* UDP is connectionless -> only send the data */
|
|
if (socket_udp_send(link->socket_fd, data, size, peer_addr, peer_port) == -1)
|
|
goto error;
|
|
|
|
}
|
|
|
|
link->bytes_sent += size;
|
|
link->packets_sent++;
|
|
|
|
return 0;
|
|
|
|
error:
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* return -1 on error and 0 if the sending was fine
|
|
*/
|
|
|
|
int link_receive_packet(socket_link_t *link, void **ret_data, int *ret_size, uint16_t proto_type)
|
|
{
|
|
unsigned char sizebuf[4];
|
|
int32_t size = 0;
|
|
void *data = NULL;
|
|
|
|
/* received the size first, maximum is 2^31 bytes */
|
|
if ((proto_type == 0) || (proto_type == 2))
|
|
{
|
|
if (socket_receive(link->socket_fd, sizebuf, 4) == -1)
|
|
goto error;
|
|
|
|
size = (sizebuf[0] << 24) |
|
|
(sizebuf[1] << 16) |
|
|
(sizebuf[2] << 8) |
|
|
sizebuf[3];
|
|
|
|
link->bytes_received += 4;
|
|
|
|
data = malloc(size);
|
|
if (data == NULL) {
|
|
LOG_E(MAC, "%s:%d: out of memory\n", __FILE__, __LINE__);
|
|
goto error;
|
|
}
|
|
|
|
if (socket_receive(link->socket_fd, data, size) == -1)
|
|
goto error;
|
|
}
|
|
else if (proto_type == 1)
|
|
{
|
|
/* we get a single packet (no size, UDP could lose it). Therefore, prepare
|
|
* for the maximum UDP packet size */
|
|
size = 65535;
|
|
data = malloc(size);
|
|
if (data == NULL) {
|
|
LOG_E(MAC, "%s:%d: out of memory\n", __FILE__, __LINE__);
|
|
goto error;
|
|
}
|
|
|
|
size = socket_udp_receive(link->socket_fd, data, size);
|
|
if (size < 0)
|
|
goto error;
|
|
}
|
|
|
|
link->bytes_received += size;
|
|
link->packets_received++;
|
|
|
|
*ret_data = data;
|
|
*ret_size = size;
|
|
return 0;
|
|
|
|
error:
|
|
free(data);
|
|
*ret_data = NULL;
|
|
*ret_size = 0;
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* return -1 on error, 0 if all is fine
|
|
*/
|
|
int close_link(socket_link_t *link)
|
|
{
|
|
close(link->socket_fd);
|
|
memset(link, 0, sizeof(socket_link_t));
|
|
free(link);
|
|
return 0;
|
|
}
|
|
|
|
#ifdef SERVER_TEST
|
|
|
|
#include <inttypes.h>
|
|
|
|
int main(void)
|
|
{
|
|
void *data;
|
|
int size;
|
|
socket_link_t *l = new_link_server(2210);
|
|
if (l == NULL) { printf("no link created\n"); return 1; }
|
|
printf("link is up\n");
|
|
printf("server starts sleeping...\n");
|
|
/* this sleep is here to test for broken pipe. You can run "nc localhost 2210"
|
|
* and interrupt it quickly so that the server gets a 'broken' pipe on the
|
|
* following link_send_packet.
|
|
*/
|
|
sleep(1);
|
|
printf("... done\n");
|
|
if (link_send_packet(l, "hello\n", 6+1) ||
|
|
link_send_packet(l, "world\n", 6+1)) return 1;
|
|
if (link_receive_packet(l, &data, &size)) return 1; printf("%s", (char *)data); free(data);
|
|
if (link_receive_packet(l, &data, &size)) return 1; printf("%s", (char *)data); free(data);
|
|
printf("stats:\n");
|
|
printf(" sent packets %"PRIu64"\n", l->packets_sent);
|
|
printf(" sent bytes %"PRIu64"\n", l->bytes_sent);
|
|
printf(" received packets %"PRIu64"\n", l->packets_received);
|
|
printf(" received bytes %"PRIu64"\n", l->bytes_received);
|
|
if (close_link(l)) return 1;
|
|
printf("link is down\n");
|
|
return 0;
|
|
}
|
|
|
|
#endif
|
|
|
|
#ifdef CLIENT_TEST
|
|
|
|
#include <inttypes.h>
|
|
|
|
int main(void)
|
|
{
|
|
void *data;
|
|
int size;
|
|
socket_link_t *l = new_link_client("127.0.0.1", 2210);
|
|
if (l == NULL) { printf("no link created\n"); return 1; }
|
|
printf("link is up\n");
|
|
if (link_receive_packet(l, &data, &size)) return 1; printf("%s", (char *)data); free(data);
|
|
if (link_receive_packet(l, &data, &size)) return 1; printf("%s", (char *)data); free(data);
|
|
if (link_send_packet(l, "bye\n", 4+1) ||
|
|
link_send_packet(l, "server\n", 7+1)) return 1;
|
|
printf("stats:\n");
|
|
printf(" sent packets %"PRIu64"\n", l->packets_sent);
|
|
printf(" sent bytes %"PRIu64"\n", l->bytes_sent);
|
|
printf(" received packets %"PRIu64"\n", l->packets_received);
|
|
printf(" received bytes %"PRIu64"\n", l->bytes_received);
|
|
if (close_link(l)) return 1;
|
|
printf("link is down\n");
|
|
return 0;
|
|
}
|
|
|
|
#endif
|