Merge pull request #5938 from miri64/pkg/feat/lwip-sock-tcp
pkg: provide sock_tcp support for lwip
This commit is contained in:
commit
c0b9b0fb56
@ -436,6 +436,10 @@ ifneq (,$(filter lwip_sock_ip,$(USEMODULE)))
|
|||||||
USEMODULE += lwip_raw
|
USEMODULE += lwip_raw
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
ifneq (,$(filter lwip_sock_tcp,$(USEMODULE)))
|
||||||
|
USEMODULE += lwip_tcp
|
||||||
|
endif
|
||||||
|
|
||||||
ifneq (,$(filter lwip_sock_udp,$(USEMODULE)))
|
ifneq (,$(filter lwip_sock_udp,$(USEMODULE)))
|
||||||
USEMODULE += lwip_udp
|
USEMODULE += lwip_udp
|
||||||
endif
|
endif
|
||||||
|
|||||||
@ -25,6 +25,9 @@ endif
|
|||||||
ifneq (,$(filter lwip_sock_ip,$(USEMODULE)))
|
ifneq (,$(filter lwip_sock_ip,$(USEMODULE)))
|
||||||
DIRS += $(RIOTBASE)/pkg/lwip/contrib/sock/ip
|
DIRS += $(RIOTBASE)/pkg/lwip/contrib/sock/ip
|
||||||
endif
|
endif
|
||||||
|
ifneq (,$(filter lwip_sock_tcp,$(USEMODULE)))
|
||||||
|
DIRS += $(RIOTBASE)/pkg/lwip/contrib/sock/tcp
|
||||||
|
endif
|
||||||
ifneq (,$(filter lwip_sock_udp,$(USEMODULE)))
|
ifneq (,$(filter lwip_sock_udp,$(USEMODULE)))
|
||||||
DIRS += $(RIOTBASE)/pkg/lwip/contrib/sock/udp
|
DIRS += $(RIOTBASE)/pkg/lwip/contrib/sock/udp
|
||||||
endif
|
endif
|
||||||
|
|||||||
@ -295,6 +295,11 @@ int lwip_sock_create(struct netconn **conn, const struct _sock_tl_ep *local,
|
|||||||
* local->port) demand binding */
|
* local->port) demand binding */
|
||||||
if (bind) {
|
if (bind) {
|
||||||
switch (netconn_bind(*conn, &local_addr, local_port)) {
|
switch (netconn_bind(*conn, &local_addr, local_port)) {
|
||||||
|
#if LWIP_TCP
|
||||||
|
case ERR_BUF:
|
||||||
|
res = -ENOMEM;
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
case ERR_USE:
|
case ERR_USE:
|
||||||
res = -EADDRINUSE;
|
res = -EADDRINUSE;
|
||||||
break;
|
break;
|
||||||
@ -311,6 +316,24 @@ int lwip_sock_create(struct netconn **conn, const struct _sock_tl_ep *local,
|
|||||||
}
|
}
|
||||||
if (remote != NULL) {
|
if (remote != NULL) {
|
||||||
switch (netconn_connect(*conn, &remote_addr, remote_port)) {
|
switch (netconn_connect(*conn, &remote_addr, remote_port)) {
|
||||||
|
#if LWIP_TCP
|
||||||
|
case ERR_BUF:
|
||||||
|
res = -ENOMEM;
|
||||||
|
break;
|
||||||
|
case ERR_INPROGRESS:
|
||||||
|
res = -EINPROGRESS;
|
||||||
|
break;
|
||||||
|
case ERR_ISCONN:
|
||||||
|
res = -EISCONN;
|
||||||
|
break;
|
||||||
|
case ERR_IF:
|
||||||
|
case ERR_RTE:
|
||||||
|
res = -ENETUNREACH;
|
||||||
|
break;
|
||||||
|
case ERR_ABRT:
|
||||||
|
res = -ETIMEDOUT;
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
case ERR_USE:
|
case ERR_USE:
|
||||||
res = -EADDRINUSE;
|
res = -EADDRINUSE;
|
||||||
break;
|
break;
|
||||||
@ -403,6 +426,7 @@ int lwip_sock_get_addr(struct netconn *conn, struct _sock_tl_ep *ep, u8_t local)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if defined(MODULE_LWIP_SOCK_UDP) || defined(MODULE_LWIP_SOCK_IP)
|
||||||
int lwip_sock_recv(struct netconn *conn, uint32_t timeout, struct netbuf **buf)
|
int lwip_sock_recv(struct netconn *conn, uint32_t timeout, struct netbuf **buf)
|
||||||
{
|
{
|
||||||
int res;
|
int res;
|
||||||
@ -441,6 +465,7 @@ int lwip_sock_recv(struct netconn *conn, uint32_t timeout, struct netbuf **buf)
|
|||||||
#endif
|
#endif
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
#endif /* defined(MODULE_LWIP_SOCK_UDP) || defined(MODULE_LWIP_SOCK_IP) */
|
||||||
|
|
||||||
ssize_t lwip_sock_send(struct netconn **conn, const void *data, size_t len,
|
ssize_t lwip_sock_send(struct netconn **conn, const void *data, size_t len,
|
||||||
int proto, const struct _sock_tl_ep *remote, int type)
|
int proto, const struct _sock_tl_ep *remote, int type)
|
||||||
@ -494,15 +519,20 @@ ssize_t lwip_sock_send(struct netconn **conn, const void *data, size_t len,
|
|||||||
netbuf_delete(buf);
|
netbuf_delete(buf);
|
||||||
return -ENOTCONN;
|
return -ENOTCONN;
|
||||||
}
|
}
|
||||||
|
res = len; /* set for non-TCP calls */
|
||||||
if (remote != NULL) {
|
if (remote != NULL) {
|
||||||
err = netconn_sendto(tmp, buf, &remote_addr, remote_port);
|
err = netconn_sendto(tmp, buf, &remote_addr, remote_port);
|
||||||
}
|
}
|
||||||
|
#if LWIP_TCP
|
||||||
|
else if (tmp->type & NETCONN_TCP) {
|
||||||
|
err = netconn_write_partly(tmp, data, len, 0, (size_t *)(&res));
|
||||||
|
}
|
||||||
|
#endif /* LWIP_TCP */
|
||||||
else {
|
else {
|
||||||
err = netconn_send(tmp, buf);
|
err = netconn_send(tmp, buf);
|
||||||
}
|
}
|
||||||
switch (err) {
|
switch (err) {
|
||||||
case ERR_OK:
|
case ERR_OK:
|
||||||
res = len;
|
|
||||||
if (conn != NULL) {
|
if (conn != NULL) {
|
||||||
*conn = tmp;
|
*conn = tmp;
|
||||||
}
|
}
|
||||||
|
|||||||
3
pkg/lwip/contrib/sock/tcp/Makefile
Normal file
3
pkg/lwip/contrib/sock/tcp/Makefile
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
MODULE := lwip_sock_tcp
|
||||||
|
|
||||||
|
include $(RIOTBASE)/Makefile.base
|
||||||
376
pkg/lwip/contrib/sock/tcp/lwip_sock_tcp.c
Normal file
376
pkg/lwip/contrib/sock/tcp/lwip_sock_tcp.c
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Freie Universität Berlin
|
||||||
|
*
|
||||||
|
* This file is subject to the terms and conditions of the GNU Lesser
|
||||||
|
* General Public License v2.1. See the file LICENSE in the top level
|
||||||
|
* directory for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @{
|
||||||
|
*
|
||||||
|
* @file
|
||||||
|
* @author Martine Lenders <m.lenders@fu-berlin.de>
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "mutex.h"
|
||||||
|
|
||||||
|
#include "net/sock/tcp.h"
|
||||||
|
#include "timex.h"
|
||||||
|
|
||||||
|
#include "lwip/sock_internal.h"
|
||||||
|
#include "lwip/api.h"
|
||||||
|
#include "lwip/opt.h"
|
||||||
|
|
||||||
|
static inline void _tcp_sock_init(sock_tcp_t *sock, struct netconn *conn,
|
||||||
|
sock_tcp_queue_t *queue)
|
||||||
|
{
|
||||||
|
mutex_init(&sock->mutex);
|
||||||
|
mutex_lock(&sock->mutex);
|
||||||
|
netconn_set_noautorecved(conn, 1);
|
||||||
|
sock->conn = conn;
|
||||||
|
sock->queue = queue;
|
||||||
|
sock->last_buf = NULL;
|
||||||
|
sock->last_offset = 0;
|
||||||
|
mutex_unlock(&sock->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
int sock_tcp_connect(sock_tcp_t *sock, const sock_tcp_ep_t *remote,
|
||||||
|
uint16_t local_port, uint16_t flags)
|
||||||
|
{
|
||||||
|
assert(sock != NULL);
|
||||||
|
assert((remote != NULL) && (remote->port != 0));
|
||||||
|
|
||||||
|
int res;
|
||||||
|
struct netconn *tmp = NULL;
|
||||||
|
struct _sock_tl_ep local = { .family = remote->family,
|
||||||
|
.netif = remote->netif,
|
||||||
|
.port = local_port };
|
||||||
|
|
||||||
|
if ((res = lwip_sock_create(&tmp, &local, (struct _sock_tl_ep *)remote, 0,
|
||||||
|
flags, NETCONN_TCP)) == 0) {
|
||||||
|
_tcp_sock_init(sock, tmp, NULL);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sock_tcp_listen(sock_tcp_queue_t *queue, const sock_tcp_ep_t *local,
|
||||||
|
sock_tcp_t *queue_array, unsigned queue_len,
|
||||||
|
uint16_t flags)
|
||||||
|
{
|
||||||
|
assert(queue != NULL);
|
||||||
|
assert((local != NULL) && (local->port != 0));
|
||||||
|
assert((queue_array != NULL) && (queue_len != 0));
|
||||||
|
|
||||||
|
int res;
|
||||||
|
struct netconn *tmp = NULL;
|
||||||
|
|
||||||
|
if (queue_len > USHRT_MAX) {
|
||||||
|
return -EFAULT;
|
||||||
|
}
|
||||||
|
if ((res = lwip_sock_create(&tmp, (struct _sock_tl_ep *)local, NULL, 0,
|
||||||
|
flags, NETCONN_TCP)) < 0) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
assert(tmp != NULL); /* just in case lwIP is trolling */
|
||||||
|
mutex_init(&queue->mutex);
|
||||||
|
mutex_lock(&queue->mutex);
|
||||||
|
queue->conn = tmp;
|
||||||
|
queue->array = queue_array;
|
||||||
|
queue->len = queue_len;
|
||||||
|
queue->used = 0;
|
||||||
|
memset(queue->array, 0, sizeof(sock_tcp_t) * queue_len);
|
||||||
|
mutex_unlock(&queue->mutex);
|
||||||
|
switch (netconn_listen_with_backlog(queue->conn, queue->len)) {
|
||||||
|
case ERR_OK:
|
||||||
|
break;
|
||||||
|
case ERR_MEM:
|
||||||
|
return -ENOMEM;
|
||||||
|
case ERR_USE:
|
||||||
|
return -EADDRINUSE;
|
||||||
|
case ERR_VAL:
|
||||||
|
return -EINVAL;
|
||||||
|
default:
|
||||||
|
assert(false); /* should not happen since queue->conn is not closed
|
||||||
|
* and we have a TCP conn */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sock_tcp_disconnect(sock_tcp_t *sock)
|
||||||
|
{
|
||||||
|
assert(sock != NULL);
|
||||||
|
mutex_lock(&sock->mutex);
|
||||||
|
if (sock->conn != NULL) {
|
||||||
|
netconn_close(sock->conn);
|
||||||
|
netconn_delete(sock->conn);
|
||||||
|
sock->conn = NULL;
|
||||||
|
/* if sock came from a sock_tcp_queue_t: since sock is a pointer in it's
|
||||||
|
* array it is also deleted from there, but we need to decrement the used
|
||||||
|
* counter */
|
||||||
|
if (sock->queue != NULL) {
|
||||||
|
assert(sock->queue->used > 0);
|
||||||
|
sock->queue->used--;
|
||||||
|
sock->queue = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mutex_unlock(&sock->mutex);
|
||||||
|
memset(&sock->mutex, 0, sizeof(mutex_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
void sock_tcp_stop_listen(sock_tcp_queue_t *queue)
|
||||||
|
{
|
||||||
|
assert(queue != NULL);
|
||||||
|
mutex_lock(&queue->mutex);
|
||||||
|
if (queue->conn != NULL) {
|
||||||
|
netconn_close(queue->conn);
|
||||||
|
netconn_delete(queue->conn);
|
||||||
|
queue->conn = NULL;
|
||||||
|
/* sever connections established through this queue */
|
||||||
|
for (unsigned i = 0; i < queue->len; i++) {
|
||||||
|
sock_tcp_disconnect(&queue->array[i]);
|
||||||
|
}
|
||||||
|
queue->array = NULL;
|
||||||
|
queue->len = 0;
|
||||||
|
queue->used = 0;
|
||||||
|
}
|
||||||
|
mutex_unlock(&queue->mutex);
|
||||||
|
memset(&queue->mutex, 0, sizeof(mutex_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
int sock_tcp_get_local(sock_tcp_t *sock, sock_tcp_ep_t *ep)
|
||||||
|
{
|
||||||
|
int res = 0;
|
||||||
|
assert(sock != NULL);
|
||||||
|
mutex_lock(&sock->mutex);
|
||||||
|
if ((sock->conn == NULL) || lwip_sock_get_addr(sock->conn,
|
||||||
|
(struct _sock_tl_ep *)ep,
|
||||||
|
1)) {
|
||||||
|
res = -EADDRNOTAVAIL;
|
||||||
|
}
|
||||||
|
mutex_unlock(&sock->mutex);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sock_tcp_get_remote(sock_tcp_t *sock, sock_tcp_ep_t *ep)
|
||||||
|
{
|
||||||
|
int res = 0;
|
||||||
|
assert(sock != NULL);
|
||||||
|
mutex_lock(&sock->mutex);
|
||||||
|
if ((sock->conn == NULL) || lwip_sock_get_addr(sock->conn,
|
||||||
|
(struct _sock_tl_ep *)ep,
|
||||||
|
0)) {
|
||||||
|
res = -ENOTCONN;
|
||||||
|
}
|
||||||
|
mutex_unlock(&sock->mutex);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sock_tcp_queue_get_local(sock_tcp_queue_t *queue, sock_tcp_ep_t *ep)
|
||||||
|
{
|
||||||
|
int res = 0;
|
||||||
|
|
||||||
|
assert(queue != NULL);
|
||||||
|
mutex_lock(&queue->mutex);
|
||||||
|
if ((queue->conn == NULL) || lwip_sock_get_addr(queue->conn,
|
||||||
|
(struct _sock_tl_ep *)ep,
|
||||||
|
1)) {
|
||||||
|
res = -EADDRNOTAVAIL;
|
||||||
|
}
|
||||||
|
mutex_unlock(&queue->mutex);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sock_tcp_accept(sock_tcp_queue_t *queue, sock_tcp_t **sock,
|
||||||
|
uint32_t timeout)
|
||||||
|
{
|
||||||
|
struct netconn *tmp = NULL;
|
||||||
|
int res = 0;
|
||||||
|
|
||||||
|
assert((queue != NULL) && (sock != NULL));
|
||||||
|
if (queue->conn == NULL) {
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
if (timeout == 0) {
|
||||||
|
if (!mutex_trylock(&queue->mutex)) {
|
||||||
|
return -EAGAIN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (timeout != 0) {
|
||||||
|
mutex_lock(&queue->mutex);
|
||||||
|
}
|
||||||
|
if (queue->used < queue->len) {
|
||||||
|
#if LWIP_SO_RCVTIMEO
|
||||||
|
if ((timeout != 0) && (timeout != SOCK_NO_TIMEOUT)) {
|
||||||
|
netconn_set_recvtimeout(queue->conn, timeout / US_PER_MS);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
#endif
|
||||||
|
if ((timeout == 0) && !cib_avail(&queue->conn->acceptmbox.mbox.cib)) {
|
||||||
|
mutex_unlock(&queue->mutex);
|
||||||
|
return -EAGAIN;
|
||||||
|
}
|
||||||
|
switch (netconn_accept(queue->conn, &tmp)) {
|
||||||
|
case ERR_OK:
|
||||||
|
for (unsigned short i = 0; i < queue->len; i++) {
|
||||||
|
sock_tcp_t *s = &queue->array[i];
|
||||||
|
if (s->conn == NULL) {
|
||||||
|
_tcp_sock_init(s, tmp, queue);
|
||||||
|
queue->used++;
|
||||||
|
assert(queue->used > 0);
|
||||||
|
*sock = s;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ERR_ABRT:
|
||||||
|
res = -ECONNABORTED;
|
||||||
|
break;
|
||||||
|
case ERR_MEM:
|
||||||
|
res = -ENOMEM;
|
||||||
|
break;
|
||||||
|
#if LWIP_SO_RCVTIMEO
|
||||||
|
case ERR_TIMEOUT:
|
||||||
|
res = -ETIMEDOUT;
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
default:
|
||||||
|
assert(false);
|
||||||
|
res = -1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
res = -ENOMEM;
|
||||||
|
}
|
||||||
|
#if LWIP_SO_RCVTIMEO
|
||||||
|
netconn_set_recvtimeout(queue->conn, 0);
|
||||||
|
#endif
|
||||||
|
mutex_unlock(&queue->mutex);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t sock_tcp_read(sock_tcp_t *sock, void *data, size_t max_len,
|
||||||
|
uint32_t timeout)
|
||||||
|
{
|
||||||
|
uint8_t *data_ptr = data;
|
||||||
|
struct pbuf *buf;
|
||||||
|
ssize_t offset = 0, res = 0;
|
||||||
|
bool done = false;
|
||||||
|
|
||||||
|
assert((sock != NULL) && (data != NULL) && (max_len > 0));
|
||||||
|
if (sock->conn == NULL) {
|
||||||
|
return -ENOTCONN;
|
||||||
|
}
|
||||||
|
if (timeout == 0) {
|
||||||
|
if (!mutex_trylock(&sock->mutex)) {
|
||||||
|
return -EAGAIN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
mutex_lock(&sock->mutex);
|
||||||
|
}
|
||||||
|
#if LWIP_SO_RCVTIMEO
|
||||||
|
if ((timeout != 0) && (timeout != SOCK_NO_TIMEOUT)) {
|
||||||
|
netconn_set_recvtimeout(sock->conn, timeout / US_PER_MS);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
#endif
|
||||||
|
if ((timeout == 0) && !cib_avail(&sock->conn->recvmbox.mbox.cib)) {
|
||||||
|
mutex_unlock(&sock->mutex);
|
||||||
|
return -EAGAIN;
|
||||||
|
}
|
||||||
|
while (!done) {
|
||||||
|
uint16_t copylen, buf_len;
|
||||||
|
if (sock->last_buf != NULL) {
|
||||||
|
buf = sock->last_buf;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
err_t err;
|
||||||
|
if ((err = netconn_recv_tcp_pbuf(sock->conn, &buf)) < 0) {
|
||||||
|
switch (err) {
|
||||||
|
case ERR_ABRT:
|
||||||
|
res = -ECONNABORTED;
|
||||||
|
break;
|
||||||
|
case ERR_CONN:
|
||||||
|
res = -EADDRNOTAVAIL;
|
||||||
|
break;
|
||||||
|
case ERR_RST:
|
||||||
|
case ERR_CLSD:
|
||||||
|
res = -ECONNRESET;
|
||||||
|
break;
|
||||||
|
case ERR_MEM:
|
||||||
|
res = -ENOMEM;
|
||||||
|
break;
|
||||||
|
#if LWIP_SO_RCVTIMEO
|
||||||
|
case ERR_TIMEOUT:
|
||||||
|
res = -ETIMEDOUT;
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
default:
|
||||||
|
/* no applicable error */
|
||||||
|
res = -1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sock->last_buf = buf;
|
||||||
|
}
|
||||||
|
buf_len = buf->tot_len - sock->last_offset;
|
||||||
|
copylen = (buf_len > max_len) ? (uint16_t)max_len : buf_len;
|
||||||
|
pbuf_copy_partial(buf, data_ptr + offset, copylen, sock->last_offset);
|
||||||
|
offset += copylen;
|
||||||
|
max_len -= copylen; /* should be 0 at minimum due to copylen setting above */
|
||||||
|
if (max_len == 0) {
|
||||||
|
done = true;
|
||||||
|
res = offset; /* in case offset == 0 */
|
||||||
|
}
|
||||||
|
/* post-process buf */
|
||||||
|
if (buf_len > copylen) {
|
||||||
|
/* there is still data in the buffer */
|
||||||
|
sock->last_buf = buf;
|
||||||
|
sock->last_offset = copylen;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sock->last_buf = NULL;
|
||||||
|
sock->last_offset = 0;
|
||||||
|
pbuf_free(buf);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (offset > 0) {
|
||||||
|
/* inform lwIP how much we receive*/
|
||||||
|
netconn_recved(sock->conn, (u32_t)offset);
|
||||||
|
res = offset; /* we received data so return it */
|
||||||
|
}
|
||||||
|
/* unset flags */
|
||||||
|
#if LWIP_SO_RCVTIMEO
|
||||||
|
netconn_set_recvtimeout(sock->conn, 0);
|
||||||
|
#endif
|
||||||
|
netconn_set_nonblocking(sock->conn, false);
|
||||||
|
mutex_unlock(&sock->mutex);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t sock_tcp_write(sock_tcp_t *sock, const void *data, size_t len)
|
||||||
|
{
|
||||||
|
struct netconn *conn;
|
||||||
|
int res = 0;
|
||||||
|
|
||||||
|
assert(sock != NULL);
|
||||||
|
assert((len == 0) || (data != NULL)); /* (len != 0) => (data != NULL) */
|
||||||
|
mutex_lock(&sock->mutex);
|
||||||
|
if (sock->conn == NULL) {
|
||||||
|
mutex_unlock(&sock->mutex);
|
||||||
|
return -ENOTCONN;
|
||||||
|
}
|
||||||
|
conn = sock->conn;
|
||||||
|
mutex_unlock(&sock->mutex); /* we won't change anything to sock here
|
||||||
|
(lwip_sock_send neither, since it remote is
|
||||||
|
NULL) so we can leave the mutex */
|
||||||
|
res = lwip_sock_send(&conn, data, len, 0, NULL, NETCONN_TCP);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @} */
|
||||||
@ -34,6 +34,14 @@
|
|||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Configures @ref sock_tcp_accept() timeout in milliseconds
|
||||||
|
* (0 by default, which means no timeout)
|
||||||
|
*/
|
||||||
|
#ifndef LWIP_SOCK_TCP_ACCEPT_TIMEOUT
|
||||||
|
#define LWIP_SOCK_TCP_ACCEPT_TIMEOUT (0)
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Internal helper functions for lwIP
|
* @brief Internal helper functions for lwIP
|
||||||
* @internal
|
* @internal
|
||||||
@ -44,7 +52,9 @@ int lwip_sock_create(struct netconn **conn, const struct _sock_tl_ep *local,
|
|||||||
uint16_t flags, int type);
|
uint16_t flags, int type);
|
||||||
uint16_t lwip_sock_bind_addr_to_netif(const ip_addr_t *bind_addr);
|
uint16_t lwip_sock_bind_addr_to_netif(const ip_addr_t *bind_addr);
|
||||||
int lwip_sock_get_addr(struct netconn *conn, struct _sock_tl_ep *ep, u8_t local);
|
int lwip_sock_get_addr(struct netconn *conn, struct _sock_tl_ep *ep, u8_t local);
|
||||||
|
#if defined(MODULE_LWIP_SOCK_UDP) || defined(MODULE_LWIP_SOCK_IP)
|
||||||
int lwip_sock_recv(struct netconn *conn, uint32_t timeout, struct netbuf **buf);
|
int lwip_sock_recv(struct netconn *conn, uint32_t timeout, struct netbuf **buf);
|
||||||
|
#endif
|
||||||
ssize_t lwip_sock_send(struct netconn **conn, const void *data, size_t len,
|
ssize_t lwip_sock_send(struct netconn **conn, const void *data, size_t len,
|
||||||
int proto, const struct _sock_tl_ep *remote, int type);
|
int proto, const struct _sock_tl_ep *remote, int type);
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -33,6 +33,29 @@ struct sock_ip {
|
|||||||
struct netconn *conn;
|
struct netconn *conn;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief TCP sock type
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
struct sock_tcp {
|
||||||
|
struct netconn *conn;
|
||||||
|
struct sock_tcp_queue *queue;
|
||||||
|
mutex_t mutex;
|
||||||
|
struct pbuf *last_buf;
|
||||||
|
ssize_t last_offset;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief TCP queue type
|
||||||
|
*/
|
||||||
|
struct sock_tcp_queue {
|
||||||
|
struct netconn *conn;
|
||||||
|
struct sock_tcp *array;
|
||||||
|
mutex_t mutex;
|
||||||
|
unsigned short len;
|
||||||
|
unsigned short used;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief UDP sock type
|
* @brief UDP sock type
|
||||||
* @internal
|
* @internal
|
||||||
|
|||||||
49
tests/lwip_sock_tcp/Makefile
Normal file
49
tests/lwip_sock_tcp/Makefile
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
APPLICATION = lwip_sock_tcp
|
||||||
|
|
||||||
|
include ../Makefile.tests_common
|
||||||
|
|
||||||
|
# lwIP's memory management doesn't seem to work on non 32-bit platforms at the
|
||||||
|
# moment.
|
||||||
|
BOARD_BLACKLIST := arduino-uno arduino-duemilanove arduino-mega2560 chronos \
|
||||||
|
msb-430 msb-430h telosb waspmote-pro wsn430-v1_3b \
|
||||||
|
wsn430-v1_4 z1
|
||||||
|
BOARD_INSUFFICIENT_MEMORY = nucleo-f030 nucleo32-f042 nucleo-f334 \
|
||||||
|
stm32f0discovery weio
|
||||||
|
|
||||||
|
LWIP_IPV4 ?= 0
|
||||||
|
|
||||||
|
ifneq (0, $(LWIP_IPV4))
|
||||||
|
USEMODULE += ipv4_addr
|
||||||
|
USEMODULE += lwip_arp
|
||||||
|
USEMODULE += lwip_ipv4
|
||||||
|
CFLAGS += -DETHARP_SUPPORT_STATIC_ENTRIES=1
|
||||||
|
LWIP_IPV6 ?= 0
|
||||||
|
else
|
||||||
|
LWIP_IPV6 ?= 1
|
||||||
|
endif
|
||||||
|
|
||||||
|
ifneq (0, $(LWIP_IPV6))
|
||||||
|
USEMODULE += ipv6_addr
|
||||||
|
USEMODULE += lwip_ipv6_autoconfig
|
||||||
|
endif
|
||||||
|
|
||||||
|
USEMODULE += inet_csum
|
||||||
|
USEMODULE += lwip_ethernet lwip_netdev2
|
||||||
|
USEMODULE += lwip_sock_tcp
|
||||||
|
USEMODULE += netdev2_eth
|
||||||
|
USEMODULE += netdev2_test
|
||||||
|
USEMODULE += ps
|
||||||
|
|
||||||
|
DISABLE_MODULE += auto_init
|
||||||
|
|
||||||
|
CFLAGS += -DDEVELHELP
|
||||||
|
CFLAGS += -DSO_REUSE
|
||||||
|
CFLAGS += -DLWIP_SO_RCVTIMEO
|
||||||
|
CFLAGS += -DLWIP_SOCK_TCP_ACCEPT_TIMEOUT=500
|
||||||
|
CFLAGS += -DLWIP_NETIF_LOOPBACK=1
|
||||||
|
CFLAGS += -DLWIP_HAVE_LOOPIF=1
|
||||||
|
|
||||||
|
include $(RIOTBASE)/Makefile.include
|
||||||
|
|
||||||
|
test:
|
||||||
|
./tests/01-run.py
|
||||||
33
tests/lwip_sock_tcp/README.md
Normal file
33
tests/lwip_sock_tcp/README.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
Tests for lwIP's sock_tcp port
|
||||||
|
==============================
|
||||||
|
|
||||||
|
This tests the `sock_tcp` port of lwIP. There is no network device needed since
|
||||||
|
a [virtual device](http://doc.riot-os.org/group__sys__netdev2__test.html) is
|
||||||
|
provided at the backend.
|
||||||
|
|
||||||
|
These tests test both IPv4 and IPv6 capabilities. They can be activated by
|
||||||
|
the `LWIP_IPV4` and `LWIP_IPV6` environment variables to a non-zero value.
|
||||||
|
IPv6 is activated by default:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make all test
|
||||||
|
# or
|
||||||
|
LWIP_IPV6=1 make all test
|
||||||
|
```
|
||||||
|
|
||||||
|
To just test IPv4 set the `LWIP_IPV4` to a non-zero value (IPv6 will be
|
||||||
|
deactivated automatically):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
LWIP_IPV4=1 make all test
|
||||||
|
```
|
||||||
|
|
||||||
|
To test both set the `LWIP_IPV4` and `LWIP_IPV6` to a non-zero value:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
LWIP_IPV4=1 LWIP_IPV6=1 make all test
|
||||||
|
```
|
||||||
|
|
||||||
|
Since lwIP uses a lot of macro magic to activate/deactivate these capabilities
|
||||||
|
it is advisable to **test all three configurations individually** (just IPv4,
|
||||||
|
just IPv6, IPv4/IPv6 dual stack mode).
|
||||||
49
tests/lwip_sock_tcp/constants.h
Normal file
49
tests/lwip_sock_tcp/constants.h
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Freie Universität Berlin
|
||||||
|
*
|
||||||
|
* This file is subject to the terms and conditions of the GNU Lesser
|
||||||
|
* General Public License v2.1. See the file LICENSE in the top level
|
||||||
|
* directory for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @defgroup
|
||||||
|
* @ingroup
|
||||||
|
* @brief
|
||||||
|
* @{
|
||||||
|
*
|
||||||
|
* @file
|
||||||
|
* @brief
|
||||||
|
*
|
||||||
|
* @author Martine Lenders <m.lenders@fu-berlin.de>
|
||||||
|
*/
|
||||||
|
#ifndef CONSTANTS_H_
|
||||||
|
#define CONSTANTS_H_
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define _TEST_PORT_LOCAL (0x2c94)
|
||||||
|
#define _TEST_PORT_REMOTE (0xa615)
|
||||||
|
#define _TEST_NETIF (1)
|
||||||
|
#define _TEST_TIMEOUT (1000000U)
|
||||||
|
#define _TEST_ADDR4_LOCAL (0xc0a84f96U) /* 192.168.79.150 */
|
||||||
|
#define _TEST_ADDR4_REMOTE (0x7f000001U) /* 127.0.0.1 */
|
||||||
|
#define _TEST_ADDR4_WRONG (0x254c6b4cU)
|
||||||
|
#define _TEST_ADDR4_MASK (0xffffff00U) /* 255.255.255.0 */
|
||||||
|
#define _TEST_ADDR4_GW (0UL) /* so we can test unreachability */
|
||||||
|
#define _TEST_ADDR6_LOCAL { 0x2f, 0xc4, 0x11, 0x5a, 0xe6, 0x91, 0x8d, 0x5d, \
|
||||||
|
0x8c, 0xd1, 0x47, 0x07, 0xb7, 0x6f, 0x9b, 0x48 }
|
||||||
|
#define _TEST_ADDR6_REMOTE { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }
|
||||||
|
#define _TEST_ADDR6_WRONG { 0x2a, 0xce, 0x5d, 0x4e, 0xc8, 0xbf, 0x86, 0xf7, \
|
||||||
|
0x85, 0x49, 0xb4, 0x19, 0xf2, 0x28, 0xde, 0x9b }
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* CONSTANTS_H_ */
|
||||||
|
/** @} */
|
||||||
1192
tests/lwip_sock_tcp/main.c
Normal file
1192
tests/lwip_sock_tcp/main.c
Normal file
File diff suppressed because it is too large
Load Diff
30
tests/lwip_sock_tcp/stack.c
Normal file
30
tests/lwip_sock_tcp/stack.c
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Freie Universität Berlin
|
||||||
|
*
|
||||||
|
* This file is subject to the terms and conditions of the GNU Lesser
|
||||||
|
* General Public License v2.1. See the file LICENSE in the top level
|
||||||
|
* directory for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @{
|
||||||
|
*
|
||||||
|
* @file
|
||||||
|
* @author Martine Lenders <mlenders@inf.fu-berlin.de>
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
#include "xtimer.h"
|
||||||
|
|
||||||
|
#include "lwip.h"
|
||||||
|
#include "lwip/netif.h"
|
||||||
|
|
||||||
|
#include "stack.h"
|
||||||
|
|
||||||
|
void _net_init(void)
|
||||||
|
{
|
||||||
|
xtimer_init();
|
||||||
|
lwip_bootstrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @} */
|
||||||
37
tests/lwip_sock_tcp/stack.h
Normal file
37
tests/lwip_sock_tcp/stack.h
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 Freie Universität Berlin
|
||||||
|
*
|
||||||
|
* This file is subject to the terms and conditions of the GNU Lesser
|
||||||
|
* General Public License v2.1. See the file LICENSE in the top level
|
||||||
|
* directory for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @defgroup
|
||||||
|
* @ingroup
|
||||||
|
* @brief
|
||||||
|
* @{
|
||||||
|
*
|
||||||
|
* @file
|
||||||
|
* @brief
|
||||||
|
*
|
||||||
|
* @author Martine Lenders <mlenders@inf.fu-berlin.de>
|
||||||
|
*/
|
||||||
|
#ifndef STACK_H_
|
||||||
|
#define STACK_H_
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initializes networking for tests
|
||||||
|
*/
|
||||||
|
void _net_init(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* STACK_H_ */
|
||||||
|
/** @} */
|
||||||
141
tests/lwip_sock_tcp/tests/01-run.py
Executable file
141
tests/lwip_sock_tcp/tests/01-run.py
Executable file
@ -0,0 +1,141 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Copyright (C) 2016 Freie Universität Berlin
|
||||||
|
#
|
||||||
|
# This file is subject to the terms and conditions of the GNU Lesser
|
||||||
|
# General Public License v2.1. See the file LICENSE in the top level
|
||||||
|
# directory for more details.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
sys.path.append(os.path.join(os.environ['RIOTBASE'], 'dist/tools/testrunner'))
|
||||||
|
import testrunner
|
||||||
|
|
||||||
|
class InvalidTimeout(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _reuse_tests(code):
|
||||||
|
return code & 1
|
||||||
|
|
||||||
|
def _ipv6_tests(code):
|
||||||
|
return code & (1 << 6)
|
||||||
|
|
||||||
|
def _ipv4_tests(code):
|
||||||
|
return code & (1 << 4)
|
||||||
|
|
||||||
|
def testfunc(child):
|
||||||
|
child.expect(u"code (0x[0-9a-f]{2})")
|
||||||
|
code = int(child.match.group(1), base=16)
|
||||||
|
if _ipv4_tests(code):
|
||||||
|
if _reuse_tests(code):
|
||||||
|
child.expect_exact("Calling test_tcp_connect4__EADDRINUSE()")
|
||||||
|
child.expect_exact("Calling test_tcp_connect4__EAFNOSUPPORT()")
|
||||||
|
child.expect_exact("Calling test_tcp_connect4__EINVAL_addr()")
|
||||||
|
child.expect_exact("Calling test_tcp_connect4__EINVAL_netif()")
|
||||||
|
child.expect_exact("Calling test_tcp_connect4__success_without_port()")
|
||||||
|
child.expect_exact("Calling test_tcp_connect4__success_local_port()")
|
||||||
|
if _reuse_tests(code):
|
||||||
|
child.expect_exact("Calling test_tcp_listen4__EADDRINUSE()")
|
||||||
|
child.expect_exact("Calling test_tcp_listen4__EAFNOSUPPORT()")
|
||||||
|
child.expect_exact("Calling test_tcp_listen4__EINVAL()")
|
||||||
|
child.expect_exact("Calling test_tcp_listen4__success_any_netif()")
|
||||||
|
child.expect_exact("Calling test_tcp_listen4__success_spec_netif()")
|
||||||
|
child.expect_exact("Calling test_tcp_accept4__EAGAIN()")
|
||||||
|
child.expect_exact("Calling test_tcp_accept4__EINVAL()")
|
||||||
|
child.expect_exact("Calling test_tcp_accept4__ETIMEDOUT()")
|
||||||
|
start = datetime.now()
|
||||||
|
child.expect_exact(" * Calling sock_tcp_accept()")
|
||||||
|
child.expect(u" \\* \\(timed out with timeout (\\d+)\\)")
|
||||||
|
exp_diff = int(child.match.group(1))
|
||||||
|
stop = datetime.now()
|
||||||
|
diff = (stop - start)
|
||||||
|
diff = (diff.seconds * 1000000) + diff.microseconds
|
||||||
|
# fail within 5% of expected
|
||||||
|
if diff > (exp_diff + (exp_diff * 0.05)) or \
|
||||||
|
diff < (exp_diff - (exp_diff * 0.05)):
|
||||||
|
raise InvalidTimeout("Invalid timeout %d (expected %d)" % (diff, exp_diff));
|
||||||
|
else:
|
||||||
|
print("Timed out correctly: %d (expected %d)" % (diff, exp_diff))
|
||||||
|
child.expect_exact("Calling test_tcp_accept4__success()")
|
||||||
|
child.expect_exact("Calling test_tcp_read4__EAGAIN()")
|
||||||
|
child.expect_exact("Calling test_tcp_read4__ECONNRESET()")
|
||||||
|
child.expect_exact("Calling test_tcp_read4__ENOTCONN()")
|
||||||
|
child.expect_exact("Calling test_tcp_read4__ETIMEDOUT()")
|
||||||
|
start = datetime.now()
|
||||||
|
child.expect_exact(" * Calling sock_tcp_read()")
|
||||||
|
child.expect(u" \\* \\(timed out with timeout (\\d+)\\)")
|
||||||
|
exp_diff = int(child.match.group(1))
|
||||||
|
stop = datetime.now()
|
||||||
|
diff = (stop - start)
|
||||||
|
diff = (diff.seconds * 1000000) + diff.microseconds
|
||||||
|
# fail within 5% of expected
|
||||||
|
if diff > (exp_diff + (exp_diff * 0.05)) or \
|
||||||
|
diff < (exp_diff - (exp_diff * 0.05)):
|
||||||
|
raise InvalidTimeout("Invalid timeout %d (expected %d)" % (diff, exp_diff));
|
||||||
|
else:
|
||||||
|
print("Timed out correctly: %d (expected %d)" % (diff, exp_diff))
|
||||||
|
child.expect_exact("Calling test_tcp_read4__success()")
|
||||||
|
child.expect_exact("Calling test_tcp_read4__success_with_timeout()")
|
||||||
|
child.expect_exact("Calling test_tcp_read4__success_non_blocking()")
|
||||||
|
child.expect_exact("Calling test_tcp_write4__ENOTCONN()")
|
||||||
|
child.expect_exact("Calling test_tcp_write4__success()")
|
||||||
|
if _ipv6_tests(code):
|
||||||
|
if _reuse_tests(code):
|
||||||
|
child.expect_exact("Calling test_tcp_connect6__EADDRINUSE()")
|
||||||
|
child.expect_exact("Calling test_tcp_connect6__EAFNOSUPPORT()")
|
||||||
|
child.expect_exact("Calling test_tcp_connect6__EINVAL_addr()")
|
||||||
|
child.expect_exact("Calling test_tcp_connect6__EINVAL_netif()")
|
||||||
|
child.expect_exact("Calling test_tcp_connect6__success_without_port()")
|
||||||
|
child.expect_exact("Calling test_tcp_connect6__success_local_port()")
|
||||||
|
if _reuse_tests(code):
|
||||||
|
child.expect_exact("Calling test_tcp_listen6__EADDRINUSE()")
|
||||||
|
child.expect_exact("Calling test_tcp_listen6__EAFNOSUPPORT()")
|
||||||
|
child.expect_exact("Calling test_tcp_listen6__EINVAL()")
|
||||||
|
child.expect_exact("Calling test_tcp_listen6__success_any_netif()")
|
||||||
|
child.expect_exact("Calling test_tcp_listen6__success_spec_netif()")
|
||||||
|
child.expect_exact("Calling test_tcp_accept6__EAGAIN()")
|
||||||
|
child.expect_exact("Calling test_tcp_accept6__EINVAL()")
|
||||||
|
child.expect_exact("Calling test_tcp_accept6__ETIMEDOUT()")
|
||||||
|
start = datetime.now()
|
||||||
|
child.expect_exact(" * Calling sock_tcp_accept()")
|
||||||
|
child.expect(u" \\* \\(timed out with timeout (\\d+)\\)")
|
||||||
|
exp_diff = int(child.match.group(1))
|
||||||
|
stop = datetime.now()
|
||||||
|
diff = (stop - start)
|
||||||
|
diff = (diff.seconds * 1000000) + diff.microseconds
|
||||||
|
# fail within 5% of expected
|
||||||
|
if diff > (exp_diff + (exp_diff * 0.05)) or \
|
||||||
|
diff < (exp_diff - (exp_diff * 0.05)):
|
||||||
|
raise InvalidTimeout("Invalid timeout %d (expected %d)" % (diff, exp_diff));
|
||||||
|
else:
|
||||||
|
print("Timed out correctly: %d (expected %d)" % (diff, exp_diff))
|
||||||
|
child.expect_exact("Calling test_tcp_accept6__success()")
|
||||||
|
child.expect_exact("Calling test_tcp_read6__EAGAIN()")
|
||||||
|
child.expect_exact("Calling test_tcp_read6__ECONNRESET()")
|
||||||
|
child.expect_exact("Calling test_tcp_read6__ENOTCONN()")
|
||||||
|
child.expect_exact("Calling test_tcp_read6__ETIMEDOUT()")
|
||||||
|
start = datetime.now()
|
||||||
|
child.expect_exact(" * Calling sock_tcp_read()")
|
||||||
|
child.expect(u" \\* \\(timed out with timeout (\\d+)\\)")
|
||||||
|
exp_diff = int(child.match.group(1))
|
||||||
|
stop = datetime.now()
|
||||||
|
diff = (stop - start)
|
||||||
|
diff = (diff.seconds * 1000000) + diff.microseconds
|
||||||
|
# fail within 5% of expected
|
||||||
|
if diff > (exp_diff + (exp_diff * 0.05)) or \
|
||||||
|
diff < (exp_diff - (exp_diff * 0.05)):
|
||||||
|
raise InvalidTimeout("Invalid timeout %d (expected %d)" % (diff, exp_diff));
|
||||||
|
else:
|
||||||
|
print("Timed out correctly: %d (expected %d)" % (diff, exp_diff))
|
||||||
|
child.expect_exact("Calling test_tcp_read6__success()")
|
||||||
|
child.expect_exact("Calling test_tcp_read6__success_with_timeout()")
|
||||||
|
child.expect_exact("Calling test_tcp_read6__success_non_blocking()")
|
||||||
|
child.expect_exact("Calling test_tcp_write6__ENOTCONN()")
|
||||||
|
child.expect_exact("Calling test_tcp_write6__success()")
|
||||||
|
child.expect_exact(u"ALL TESTS SUCCESSFUL")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(testrunner.run(testfunc, timeout=60))
|
||||||
Loading…
x
Reference in New Issue
Block a user