Organize host and robot streaming releases
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
try:
|
||||
from ._omnisocket import (
|
||||
MSG_TYPE_BINARY,
|
||||
MSG_TYPE_ERROR,
|
||||
MSG_TYPE_FILE,
|
||||
MSG_TYPE_REGISTER,
|
||||
MSG_TYPE_TEXT,
|
||||
Session,
|
||||
UdpSession,
|
||||
)
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"omnisocket extension is not built; run `make python-ext` on a Linux host first"
|
||||
) from exc
|
||||
|
||||
CONTROL_DEFAULTS = {
|
||||
"nodelay": 1,
|
||||
"interval_ms": 5,
|
||||
"resend": 2,
|
||||
"nc": 1,
|
||||
"sndwnd": 32,
|
||||
"rcvwnd": 32,
|
||||
"mtu": 1400,
|
||||
}
|
||||
|
||||
VIDEO_DEFAULTS = {
|
||||
"nodelay": 1,
|
||||
"interval_ms": 10,
|
||||
"resend": 2,
|
||||
"nc": 1,
|
||||
"sndwnd": 256,
|
||||
"rcvwnd": 256,
|
||||
"mtu": 1400,
|
||||
}
|
||||
|
||||
TELEMETRY_DEFAULTS = {
|
||||
"nodelay": 0,
|
||||
"interval_ms": 50,
|
||||
"resend": 0,
|
||||
"nc": 0,
|
||||
"sndwnd": 64,
|
||||
"rcvwnd": 64,
|
||||
"mtu": 1400,
|
||||
}
|
||||
|
||||
__all__ = [
|
||||
"CONTROL_DEFAULTS",
|
||||
"TELEMETRY_DEFAULTS",
|
||||
"VIDEO_DEFAULTS",
|
||||
"MSG_TYPE_BINARY",
|
||||
"MSG_TYPE_ERROR",
|
||||
"MSG_TYPE_FILE",
|
||||
"MSG_TYPE_REGISTER",
|
||||
"MSG_TYPE_TEXT",
|
||||
"Session",
|
||||
"UdpSession",
|
||||
]
|
||||
@@ -0,0 +1,684 @@
|
||||
#define PY_SSIZE_T_CLEAN
|
||||
#include <Python.h>
|
||||
|
||||
#include "omnisocket_client.h"
|
||||
|
||||
typedef struct PyOmniSession {
|
||||
PyObject_HEAD
|
||||
omnisocket_session_t session;
|
||||
} PyOmniSession;
|
||||
|
||||
typedef struct PyOmniUdpSession {
|
||||
PyObject_HEAD
|
||||
omnisocket_udp_session_t session;
|
||||
} PyOmniUdpSession;
|
||||
|
||||
PyDoc_STRVAR(
|
||||
PyOmniSession_recv_doc,
|
||||
"recv(timeout_ms=-1) -> (from_peer, msg_type, payload) | None"
|
||||
);
|
||||
|
||||
PyDoc_STRVAR(
|
||||
PyOmniSession_recv_into_doc,
|
||||
"recv_into(buffer, timeout_ms=-1) -> dict | None\n"
|
||||
"\n"
|
||||
"The writable buffer must be large enough for the full message body.\n"
|
||||
"If it is too small, BufferError reports the required size but the\n"
|
||||
"current frame has already been consumed and is lost."
|
||||
);
|
||||
|
||||
static PyObject *build_recv_result(const message_t *msg) {
|
||||
PyObject *body = NULL;
|
||||
PyObject *result = NULL;
|
||||
|
||||
body = PyBytes_FromStringAndSize((const char *) msg->body, (Py_ssize_t) msg->body_len);
|
||||
if (body == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
result = Py_BuildValue("(siO)", msg->from, (int) msg->type, body);
|
||||
Py_DECREF(body);
|
||||
return result;
|
||||
}
|
||||
|
||||
static PyObject *build_recv_meta_dict(
|
||||
const char *from_peer,
|
||||
const char *to_peer,
|
||||
const char *file_name,
|
||||
int msg_type,
|
||||
unsigned long long message_id,
|
||||
unsigned long long body_len
|
||||
) {
|
||||
return Py_BuildValue(
|
||||
"{s:s,s:s,s:s,s:i,s:K,s:K}",
|
||||
"from",
|
||||
from_peer,
|
||||
"to",
|
||||
to_peer,
|
||||
"file_name",
|
||||
file_name,
|
||||
"msg_type",
|
||||
msg_type,
|
||||
"message_id",
|
||||
message_id,
|
||||
"body_len",
|
||||
body_len
|
||||
);
|
||||
}
|
||||
|
||||
static PyObject *build_stats_dict(const omnisocket_session_stats_t *stats) {
|
||||
return Py_BuildValue(
|
||||
"{s:K,s:K,s:K,s:K,s:K,s:K,s:K,s:i,s:i,s:s}",
|
||||
"send_calls",
|
||||
(unsigned long long) stats->send_calls,
|
||||
"send_bytes",
|
||||
(unsigned long long) stats->send_bytes,
|
||||
"send_errors",
|
||||
(unsigned long long) stats->send_errors,
|
||||
"recv_calls",
|
||||
(unsigned long long) stats->recv_calls,
|
||||
"recv_bytes",
|
||||
(unsigned long long) stats->recv_bytes,
|
||||
"recv_timeouts",
|
||||
(unsigned long long) stats->recv_timeouts,
|
||||
"recv_errors",
|
||||
(unsigned long long) stats->recv_errors,
|
||||
"connected",
|
||||
stats->connected,
|
||||
"registered",
|
||||
stats->registered,
|
||||
"last_server_error",
|
||||
stats->last_server_error
|
||||
);
|
||||
}
|
||||
|
||||
static PyObject *build_kcp_stats_dict(const omnisocket_session_kcp_stats_t *stats) {
|
||||
PyObject *dict = PyDict_New();
|
||||
PyObject *value = NULL;
|
||||
|
||||
if (dict == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#define SET_KCP_STAT(key, expr) \
|
||||
do { \
|
||||
value = (expr); \
|
||||
if (value == NULL) { \
|
||||
Py_DECREF(dict); \
|
||||
return NULL; \
|
||||
} \
|
||||
if (PyDict_SetItemString(dict, (key), value) != 0) { \
|
||||
Py_DECREF(value); \
|
||||
Py_DECREF(dict); \
|
||||
return NULL; \
|
||||
} \
|
||||
Py_DECREF(value); \
|
||||
value = NULL; \
|
||||
} while (0)
|
||||
|
||||
SET_KCP_STAT("connected", PyLong_FromLong(stats->connected));
|
||||
SET_KCP_STAT("conv", PyLong_FromUnsignedLong(stats->conv));
|
||||
SET_KCP_STAT("rto_ms", PyLong_FromUnsignedLong(stats->rto_ms));
|
||||
SET_KCP_STAT("srtt_ms", PyLong_FromLong(stats->srtt_ms));
|
||||
SET_KCP_STAT("min_srtt_ms", PyLong_FromLong(stats->min_srtt_ms));
|
||||
SET_KCP_STAT("srttvar_ms", PyLong_FromLong(stats->srttvar_ms));
|
||||
SET_KCP_STAT("last_feedback_age_ms", PyLong_FromUnsignedLong(stats->last_feedback_age_ms));
|
||||
SET_KCP_STAT("snd_wnd", PyLong_FromUnsignedLong(stats->snd_wnd));
|
||||
SET_KCP_STAT("rmt_wnd", PyLong_FromUnsignedLong(stats->rmt_wnd));
|
||||
SET_KCP_STAT("inflight", PyLong_FromUnsignedLong(stats->inflight));
|
||||
SET_KCP_STAT("window_limit", PyLong_FromUnsignedLong(stats->window_limit));
|
||||
SET_KCP_STAT("window_pressure_pct", PyFloat_FromDouble(stats->window_pressure_pct));
|
||||
SET_KCP_STAT("snd_queue", PyLong_FromUnsignedLong(stats->snd_queue));
|
||||
SET_KCP_STAT("rcv_queue", PyLong_FromUnsignedLong(stats->rcv_queue));
|
||||
SET_KCP_STAT("snd_buffer", PyLong_FromUnsignedLong(stats->snd_buffer));
|
||||
SET_KCP_STAT("out_segs_total", PyLong_FromUnsignedLongLong((unsigned long long) stats->out_segs_total));
|
||||
SET_KCP_STAT("retrans_total", PyLong_FromUnsignedLongLong((unsigned long long) stats->retrans_total));
|
||||
SET_KCP_STAT("fast_retrans_total", PyLong_FromUnsignedLongLong((unsigned long long) stats->fast_retrans_total));
|
||||
SET_KCP_STAT("lost_total", PyLong_FromUnsignedLongLong((unsigned long long) stats->lost_total));
|
||||
SET_KCP_STAT("repeat_total", PyLong_FromUnsignedLongLong((unsigned long long) stats->repeat_total));
|
||||
SET_KCP_STAT("xmit_total", PyLong_FromUnsignedLong(stats->xmit_total));
|
||||
|
||||
#undef SET_KCP_STAT
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
static PyObject *PyOmniSession_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) {
|
||||
PyOmniSession *self;
|
||||
(void) args;
|
||||
(void) kwargs;
|
||||
|
||||
self = (PyOmniSession *) type->tp_alloc(type, 0);
|
||||
if (self == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
if (omnisocket_session_init(&self->session) != 0) {
|
||||
type->tp_free((PyObject *) self);
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
}
|
||||
return (PyObject *) self;
|
||||
}
|
||||
|
||||
static void PyOmniSession_dealloc(PyOmniSession *self) {
|
||||
omnisocket_session_destroy(&self->session);
|
||||
Py_TYPE(self)->tp_free((PyObject *) self);
|
||||
}
|
||||
|
||||
static PyObject *PyOmniSession_connect(PyOmniSession *self, PyObject *args, PyObject *kwargs) {
|
||||
const char *server_addr;
|
||||
const char *peer_id;
|
||||
const char *relay_via = "";
|
||||
const char *bind_ip = "";
|
||||
const char *bind_device = "";
|
||||
int nodelay = KCP_DEFAULT_NODELAY;
|
||||
int interval_ms = KCP_DEFAULT_INTERVAL_MS;
|
||||
int resend = KCP_DEFAULT_RESEND;
|
||||
int nc = KCP_DEFAULT_NC;
|
||||
int sndwnd = KCP_DEFAULT_SND_WND;
|
||||
int rcvwnd = KCP_DEFAULT_RCV_WND;
|
||||
int mtu = KCP_DEFAULT_MTU;
|
||||
int stats_interval_ms = KCP_DEFAULT_STATS_INTERVAL_MS;
|
||||
kcp_conn_options_t options;
|
||||
int rc;
|
||||
|
||||
static char *kwlist[] = {
|
||||
"server_addr",
|
||||
"peer_id",
|
||||
"relay_via",
|
||||
"bind_ip",
|
||||
"bind_device",
|
||||
"nodelay",
|
||||
"interval_ms",
|
||||
"resend",
|
||||
"nc",
|
||||
"sndwnd",
|
||||
"rcvwnd",
|
||||
"mtu",
|
||||
"stats_interval_ms",
|
||||
NULL
|
||||
};
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(
|
||||
args,
|
||||
kwargs,
|
||||
"ss|sssiiiiiiii",
|
||||
kwlist,
|
||||
&server_addr,
|
||||
&peer_id,
|
||||
&relay_via,
|
||||
&bind_ip,
|
||||
&bind_device,
|
||||
&nodelay,
|
||||
&interval_ms,
|
||||
&resend,
|
||||
&nc,
|
||||
&sndwnd,
|
||||
&rcvwnd,
|
||||
&mtu,
|
||||
&stats_interval_ms)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
kcp_conn_options_init(&options);
|
||||
options.nodelay = nodelay;
|
||||
options.interval_ms = interval_ms;
|
||||
options.resend = resend;
|
||||
options.nc = nc;
|
||||
options.sndwnd = sndwnd;
|
||||
options.rcvwnd = rcvwnd;
|
||||
options.mtu = mtu;
|
||||
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
rc = omnisocket_session_connect(
|
||||
&self->session,
|
||||
server_addr,
|
||||
relay_via,
|
||||
peer_id,
|
||||
bind_ip,
|
||||
bind_device,
|
||||
&options,
|
||||
stats_interval_ms
|
||||
);
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
if (rc != 0) {
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject *PyOmniSession_close(PyOmniSession *self, PyObject *Py_UNUSED(ignored)) {
|
||||
int rc;
|
||||
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
rc = omnisocket_session_close(&self->session);
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
if (rc != 0) {
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject *PyOmniSession_send(PyOmniSession *self, PyObject *args, PyObject *kwargs) {
|
||||
const char *to;
|
||||
Py_buffer payload;
|
||||
int rc;
|
||||
static char *kwlist[] = {"to", "data", NULL};
|
||||
|
||||
memset(&payload, 0, sizeof(payload));
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sy*", kwlist, &to, &payload)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
rc = omnisocket_session_send(&self->session, to, payload.buf, (size_t) payload.len);
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
PyBuffer_Release(&payload);
|
||||
if (rc != 0) {
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject *PyOmniSession_send_with_id(PyOmniSession *self, PyObject *args, PyObject *kwargs) {
|
||||
const char *to;
|
||||
Py_buffer payload;
|
||||
int rc;
|
||||
uint64_t message_id = 0;
|
||||
static char *kwlist[] = {"to", "data", NULL};
|
||||
|
||||
memset(&payload, 0, sizeof(payload));
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sy*", kwlist, &to, &payload)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
rc = omnisocket_session_send_with_id(&self->session, to, payload.buf, (size_t) payload.len, &message_id);
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
PyBuffer_Release(&payload);
|
||||
if (rc != 0) {
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
}
|
||||
return PyLong_FromUnsignedLongLong((unsigned long long) message_id);
|
||||
}
|
||||
|
||||
static PyObject *PyOmniSession_recv(PyOmniSession *self, PyObject *args, PyObject *kwargs) {
|
||||
int timeout_ms = -1;
|
||||
int rc;
|
||||
message_t msg;
|
||||
PyObject *result = NULL;
|
||||
static char *kwlist[] = {"timeout_ms", NULL};
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|i", kwlist, &timeout_ms)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
protocol_message_init(&msg);
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
rc = omnisocket_session_recv(&self->session, &msg, timeout_ms);
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
if (rc == 1) {
|
||||
protocol_message_clear(&msg);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
if (rc != 0) {
|
||||
protocol_message_clear(&msg);
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
}
|
||||
|
||||
result = build_recv_result(&msg);
|
||||
protocol_message_clear(&msg);
|
||||
return result;
|
||||
}
|
||||
|
||||
static PyObject *PyOmniSession_recv_into(PyOmniSession *self, PyObject *args, PyObject *kwargs) {
|
||||
PyObject *buffer_obj;
|
||||
Py_buffer view;
|
||||
int timeout_ms = -1;
|
||||
int rc;
|
||||
kcp_client_recv_meta_t meta;
|
||||
PyObject *result = NULL;
|
||||
static char *kwlist[] = {"buffer", "timeout_ms", NULL};
|
||||
|
||||
memset(&view, 0, sizeof(view));
|
||||
memset(&meta, 0, sizeof(meta));
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|i", kwlist, &buffer_obj, &timeout_ms)) {
|
||||
return NULL;
|
||||
}
|
||||
if (PyObject_GetBuffer(buffer_obj, &view, PyBUF_WRITABLE) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
rc = omnisocket_session_recv_into(&self->session, view.buf, (size_t) view.len, &meta, timeout_ms);
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
PyBuffer_Release(&view);
|
||||
if (rc == 1) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
if (rc == 2) {
|
||||
PyErr_Format(
|
||||
PyExc_BufferError,
|
||||
"buffer too small: need %zu bytes; current frame was already consumed and dropped",
|
||||
meta.body_len
|
||||
);
|
||||
return NULL;
|
||||
}
|
||||
if (rc != 0) {
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
}
|
||||
|
||||
result = build_recv_meta_dict(
|
||||
meta.from,
|
||||
meta.to,
|
||||
meta.file_name,
|
||||
(int) meta.type,
|
||||
(unsigned long long) meta.id,
|
||||
(unsigned long long) meta.body_len
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
static PyObject *PyOmniSession_stats(PyOmniSession *self, PyObject *Py_UNUSED(ignored)) {
|
||||
omnisocket_session_stats_t stats;
|
||||
|
||||
memset(&stats, 0, sizeof(stats));
|
||||
omnisocket_session_stats_snapshot(&self->session, &stats);
|
||||
return build_stats_dict(&stats);
|
||||
}
|
||||
|
||||
static PyObject *PyOmniSession_kcp_stats(PyOmniSession *self, PyObject *Py_UNUSED(ignored)) {
|
||||
omnisocket_session_kcp_stats_t stats;
|
||||
|
||||
memset(&stats, 0, sizeof(stats));
|
||||
omnisocket_session_kcp_stats_snapshot(&self->session, &stats);
|
||||
return build_kcp_stats_dict(&stats);
|
||||
}
|
||||
|
||||
static PyMethodDef PyOmniSession_methods[] = {
|
||||
{"connect", (PyCFunction) PyOmniSession_connect, METH_VARARGS | METH_KEYWORDS, NULL},
|
||||
{"close", (PyCFunction) PyOmniSession_close, METH_NOARGS, NULL},
|
||||
{"send", (PyCFunction) PyOmniSession_send, METH_VARARGS | METH_KEYWORDS, NULL},
|
||||
{"send_with_id", (PyCFunction) PyOmniSession_send_with_id, METH_VARARGS | METH_KEYWORDS, NULL},
|
||||
{"recv", (PyCFunction) PyOmniSession_recv, METH_VARARGS | METH_KEYWORDS, PyOmniSession_recv_doc},
|
||||
{"recv_into", (PyCFunction) PyOmniSession_recv_into, METH_VARARGS | METH_KEYWORDS, PyOmniSession_recv_into_doc},
|
||||
{"stats", (PyCFunction) PyOmniSession_stats, METH_NOARGS, NULL},
|
||||
{"kcp_stats", (PyCFunction) PyOmniSession_kcp_stats, METH_NOARGS, NULL},
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
static PyTypeObject PyOmniSessionType = {
|
||||
PyVarObject_HEAD_INIT(NULL, 0)
|
||||
};
|
||||
|
||||
static PyObject *PyOmniUdpSession_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) {
|
||||
PyOmniUdpSession *self;
|
||||
(void) args;
|
||||
(void) kwargs;
|
||||
|
||||
self = (PyOmniUdpSession *) type->tp_alloc(type, 0);
|
||||
if (self == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
if (omnisocket_udp_session_init(&self->session) != 0) {
|
||||
type->tp_free((PyObject *) self);
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
}
|
||||
return (PyObject *) self;
|
||||
}
|
||||
|
||||
static void PyOmniUdpSession_dealloc(PyOmniUdpSession *self) {
|
||||
omnisocket_udp_session_destroy(&self->session);
|
||||
Py_TYPE(self)->tp_free((PyObject *) self);
|
||||
}
|
||||
|
||||
static PyObject *PyOmniUdpSession_connect(PyOmniUdpSession *self, PyObject *args, PyObject *kwargs) {
|
||||
const char *server_addr;
|
||||
const char *peer_id;
|
||||
const char *bind_ip = "";
|
||||
const char *bind_device = "";
|
||||
int enable_timestamping = 0;
|
||||
int rc;
|
||||
|
||||
static char *kwlist[] = {
|
||||
"server_addr",
|
||||
"peer_id",
|
||||
"bind_ip",
|
||||
"bind_device",
|
||||
"enable_timestamping",
|
||||
NULL
|
||||
};
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(
|
||||
args,
|
||||
kwargs,
|
||||
"ss|ssi",
|
||||
kwlist,
|
||||
&server_addr,
|
||||
&peer_id,
|
||||
&bind_ip,
|
||||
&bind_device,
|
||||
&enable_timestamping)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
rc = omnisocket_udp_session_connect(
|
||||
&self->session,
|
||||
server_addr,
|
||||
peer_id,
|
||||
bind_ip,
|
||||
bind_device,
|
||||
enable_timestamping
|
||||
);
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
if (rc != 0) {
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject *PyOmniUdpSession_close(PyOmniUdpSession *self, PyObject *Py_UNUSED(ignored)) {
|
||||
int rc;
|
||||
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
rc = omnisocket_udp_session_close(&self->session);
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
if (rc != 0) {
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject *PyOmniUdpSession_send(PyOmniUdpSession *self, PyObject *args, PyObject *kwargs) {
|
||||
const char *to;
|
||||
Py_buffer payload;
|
||||
int rc;
|
||||
static char *kwlist[] = {"to", "data", NULL};
|
||||
|
||||
memset(&payload, 0, sizeof(payload));
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sy*", kwlist, &to, &payload)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
rc = omnisocket_udp_session_send(&self->session, to, payload.buf, (size_t) payload.len);
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
PyBuffer_Release(&payload);
|
||||
if (rc != 0) {
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject *PyOmniUdpSession_recv(PyOmniUdpSession *self, PyObject *args, PyObject *kwargs) {
|
||||
int timeout_ms = -1;
|
||||
int rc;
|
||||
message_t msg;
|
||||
PyObject *result = NULL;
|
||||
static char *kwlist[] = {"timeout_ms", NULL};
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|i", kwlist, &timeout_ms)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
protocol_message_init(&msg);
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
rc = omnisocket_udp_session_recv(&self->session, &msg, timeout_ms);
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
if (rc == 1) {
|
||||
protocol_message_clear(&msg);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
if (rc != 0) {
|
||||
protocol_message_clear(&msg);
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
}
|
||||
|
||||
result = build_recv_result(&msg);
|
||||
protocol_message_clear(&msg);
|
||||
return result;
|
||||
}
|
||||
|
||||
static PyObject *PyOmniUdpSession_recv_into(PyOmniUdpSession *self, PyObject *args, PyObject *kwargs) {
|
||||
PyObject *buffer_obj;
|
||||
Py_buffer view;
|
||||
int timeout_ms = -1;
|
||||
int rc;
|
||||
udp_client_recv_meta_t meta;
|
||||
PyObject *result = NULL;
|
||||
static char *kwlist[] = {"buffer", "timeout_ms", NULL};
|
||||
|
||||
memset(&view, 0, sizeof(view));
|
||||
memset(&meta, 0, sizeof(meta));
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|i", kwlist, &buffer_obj, &timeout_ms)) {
|
||||
return NULL;
|
||||
}
|
||||
if (PyObject_GetBuffer(buffer_obj, &view, PyBUF_WRITABLE) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
rc = omnisocket_udp_session_recv_into(&self->session, view.buf, (size_t) view.len, &meta, timeout_ms);
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
PyBuffer_Release(&view);
|
||||
if (rc == 1) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
if (rc == 2) {
|
||||
PyErr_Format(
|
||||
PyExc_BufferError,
|
||||
"buffer too small: need %zu bytes; current frame was already consumed and dropped",
|
||||
meta.body_len
|
||||
);
|
||||
return NULL;
|
||||
}
|
||||
if (rc != 0) {
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
}
|
||||
|
||||
result = build_recv_meta_dict(
|
||||
meta.from,
|
||||
meta.to,
|
||||
meta.file_name,
|
||||
(int) meta.type,
|
||||
(unsigned long long) meta.id,
|
||||
(unsigned long long) meta.body_len
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
static PyObject *PyOmniUdpSession_stats(PyOmniUdpSession *self, PyObject *Py_UNUSED(ignored)) {
|
||||
omnisocket_session_stats_t stats;
|
||||
|
||||
memset(&stats, 0, sizeof(stats));
|
||||
omnisocket_udp_session_stats_snapshot(&self->session, &stats);
|
||||
return build_stats_dict(&stats);
|
||||
}
|
||||
|
||||
static PyMethodDef PyOmniUdpSession_methods[] = {
|
||||
{"connect", (PyCFunction) PyOmniUdpSession_connect, METH_VARARGS | METH_KEYWORDS, NULL},
|
||||
{"close", (PyCFunction) PyOmniUdpSession_close, METH_NOARGS, NULL},
|
||||
{"send", (PyCFunction) PyOmniUdpSession_send, METH_VARARGS | METH_KEYWORDS, NULL},
|
||||
{"recv", (PyCFunction) PyOmniUdpSession_recv, METH_VARARGS | METH_KEYWORDS, PyOmniSession_recv_doc},
|
||||
{"recv_into", (PyCFunction) PyOmniUdpSession_recv_into, METH_VARARGS | METH_KEYWORDS, PyOmniSession_recv_into_doc},
|
||||
{"stats", (PyCFunction) PyOmniUdpSession_stats, METH_NOARGS, NULL},
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
static PyTypeObject PyOmniUdpSessionType = {
|
||||
PyVarObject_HEAD_INIT(NULL, 0)
|
||||
};
|
||||
|
||||
static PyModuleDef omnisocket_module = {
|
||||
PyModuleDef_HEAD_INIT,
|
||||
.m_name = "_omnisocket",
|
||||
.m_size = -1,
|
||||
};
|
||||
|
||||
PyMODINIT_FUNC PyInit__omnisocket(void) {
|
||||
PyObject *module;
|
||||
|
||||
PyOmniSessionType.tp_name = "omnisocket.Session";
|
||||
PyOmniSessionType.tp_basicsize = sizeof(PyOmniSession);
|
||||
PyOmniSessionType.tp_flags = Py_TPFLAGS_DEFAULT;
|
||||
PyOmniSessionType.tp_new = PyOmniSession_new;
|
||||
PyOmniSessionType.tp_dealloc = (destructor) PyOmniSession_dealloc;
|
||||
PyOmniSessionType.tp_methods = PyOmniSession_methods;
|
||||
|
||||
if (PyType_Ready(&PyOmniSessionType) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyOmniUdpSessionType.tp_name = "omnisocket.UdpSession";
|
||||
PyOmniUdpSessionType.tp_basicsize = sizeof(PyOmniUdpSession);
|
||||
PyOmniUdpSessionType.tp_flags = Py_TPFLAGS_DEFAULT;
|
||||
PyOmniUdpSessionType.tp_new = PyOmniUdpSession_new;
|
||||
PyOmniUdpSessionType.tp_dealloc = (destructor) PyOmniUdpSession_dealloc;
|
||||
PyOmniUdpSessionType.tp_methods = PyOmniUdpSession_methods;
|
||||
|
||||
if (PyType_Ready(&PyOmniUdpSessionType) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
module = PyModule_Create(&omnisocket_module);
|
||||
if (module == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Py_INCREF(&PyOmniSessionType);
|
||||
if (PyModule_AddObject(module, "Session", (PyObject *) &PyOmniSessionType) != 0) {
|
||||
Py_DECREF(&PyOmniSessionType);
|
||||
Py_DECREF(module);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Py_INCREF(&PyOmniUdpSessionType);
|
||||
if (PyModule_AddObject(module, "UdpSession", (PyObject *) &PyOmniUdpSessionType) != 0) {
|
||||
Py_DECREF(&PyOmniUdpSessionType);
|
||||
Py_DECREF(module);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (PyModule_AddIntConstant(module, "MSG_TYPE_TEXT", MSG_TYPE_TEXT) != 0 ||
|
||||
PyModule_AddIntConstant(module, "MSG_TYPE_FILE", MSG_TYPE_FILE) != 0 ||
|
||||
PyModule_AddIntConstant(module, "MSG_TYPE_REGISTER", MSG_TYPE_REGISTER) != 0 ||
|
||||
PyModule_AddIntConstant(module, "MSG_TYPE_ERROR", MSG_TYPE_ERROR) != 0 ||
|
||||
PyModule_AddIntConstant(module, "MSG_TYPE_BINARY", MSG_TYPE_BINARY) != 0) {
|
||||
Py_DECREF(module);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return module;
|
||||
}
|
||||
@@ -0,0 +1,572 @@
|
||||
#include "omnisocket_client.h"
|
||||
|
||||
static void omnisocket_session_sync_client_state_locked(omnisocket_session_t *session, kcp_client_t *client) {
|
||||
kcp_client_state_t client_state;
|
||||
|
||||
if (session == NULL) {
|
||||
return;
|
||||
}
|
||||
memset(&client_state, 0, sizeof(client_state));
|
||||
if (client != NULL) {
|
||||
kcp_client_state_snapshot(client, &client_state);
|
||||
}
|
||||
session->stats.connected = client_state.connected;
|
||||
session->stats.registered = client_state.registered;
|
||||
snprintf(
|
||||
session->stats.last_server_error,
|
||||
sizeof(session->stats.last_server_error),
|
||||
"%s",
|
||||
client_state.last_server_error
|
||||
);
|
||||
}
|
||||
|
||||
static void omnisocket_session_mark_disconnected_locked(omnisocket_session_t *session) {
|
||||
if (session == NULL) {
|
||||
return;
|
||||
}
|
||||
session->stats.connected = 0;
|
||||
session->stats.registered = 0;
|
||||
}
|
||||
|
||||
int omnisocket_session_init(omnisocket_session_t *session) {
|
||||
int rc;
|
||||
|
||||
if (session == NULL) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
memset(session, 0, sizeof(*session));
|
||||
rc = pthread_mutex_init(&session->mutex, NULL);
|
||||
if (rc != 0) {
|
||||
errno = rc;
|
||||
return -1;
|
||||
}
|
||||
rc = pthread_cond_init(&session->idle_cond, NULL);
|
||||
if (rc != 0) {
|
||||
pthread_mutex_destroy(&session->mutex);
|
||||
errno = rc;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void omnisocket_session_destroy(omnisocket_session_t *session) {
|
||||
if (session == NULL) {
|
||||
return;
|
||||
}
|
||||
(void) omnisocket_session_close(session);
|
||||
pthread_cond_destroy(&session->idle_cond);
|
||||
pthread_mutex_destroy(&session->mutex);
|
||||
}
|
||||
|
||||
static int omnisocket_session_begin_client_op(omnisocket_session_t *session, kcp_client_t **out_client) {
|
||||
if (session == NULL || out_client == NULL) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&session->mutex);
|
||||
if (session->closing) {
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
errno = ECANCELED;
|
||||
return -1;
|
||||
}
|
||||
if (session->client == NULL) {
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
errno = ENOTCONN;
|
||||
return -1;
|
||||
}
|
||||
*out_client = session->client;
|
||||
session->active_ops += 1;
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int omnisocket_session_connect(
|
||||
omnisocket_session_t *session,
|
||||
const char *server_addr,
|
||||
const char *relay_via,
|
||||
const char *peer_id,
|
||||
const char *bind_ip,
|
||||
const char *bind_device,
|
||||
const kcp_conn_options_t *options,
|
||||
int stats_interval_ms
|
||||
) {
|
||||
kcp_client_t *client;
|
||||
|
||||
if (session == NULL || server_addr == NULL || peer_id == NULL) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&session->mutex);
|
||||
while (session->closing) {
|
||||
pthread_cond_wait(&session->idle_cond, &session->mutex);
|
||||
}
|
||||
if (session->client != NULL) {
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
errno = EISCONN;
|
||||
return -1;
|
||||
}
|
||||
client = kcp_client_dial_with_options(
|
||||
server_addr,
|
||||
relay_via,
|
||||
peer_id,
|
||||
bind_ip,
|
||||
bind_device,
|
||||
options,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
stats_interval_ms
|
||||
);
|
||||
if (client == NULL) {
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
return -1;
|
||||
}
|
||||
session->client = client;
|
||||
omnisocket_session_sync_client_state_locked(session, client);
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int omnisocket_session_close(omnisocket_session_t *session) {
|
||||
kcp_client_t *client;
|
||||
|
||||
if (session == NULL) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&session->mutex);
|
||||
while (session->closing) {
|
||||
pthread_cond_wait(&session->idle_cond, &session->mutex);
|
||||
}
|
||||
client = session->client;
|
||||
if (client != NULL) {
|
||||
session->closing = 1;
|
||||
session->client = NULL;
|
||||
}
|
||||
omnisocket_session_mark_disconnected_locked(session);
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
|
||||
if (client != NULL) {
|
||||
kcp_client_close(client);
|
||||
pthread_mutex_lock(&session->mutex);
|
||||
while (session->active_ops > 0) {
|
||||
pthread_cond_wait(&session->idle_cond, &session->mutex);
|
||||
}
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
kcp_client_free(client);
|
||||
pthread_mutex_lock(&session->mutex);
|
||||
session->closing = 0;
|
||||
pthread_cond_broadcast(&session->idle_cond);
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int omnisocket_session_send(omnisocket_session_t *session, const char *to, const void *data, size_t data_len) {
|
||||
return omnisocket_session_send_with_id(session, to, data, data_len, NULL);
|
||||
}
|
||||
|
||||
int omnisocket_session_send_with_id(
|
||||
omnisocket_session_t *session,
|
||||
const char *to,
|
||||
const void *data,
|
||||
size_t data_len,
|
||||
uint64_t *out_message_id
|
||||
) {
|
||||
kcp_client_t *client;
|
||||
int rc;
|
||||
|
||||
if (session == NULL || to == NULL || (data == NULL && data_len > 0)) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (omnisocket_session_begin_client_op(session, &client) != 0) {
|
||||
return -1;
|
||||
}
|
||||
rc = kcp_client_send_binary_with_id(client, to, data, data_len, out_message_id);
|
||||
pthread_mutex_lock(&session->mutex);
|
||||
if (rc == 0) {
|
||||
session->stats.send_calls += 1;
|
||||
session->stats.send_bytes += (uint64_t) data_len;
|
||||
} else {
|
||||
session->stats.send_errors += 1;
|
||||
}
|
||||
omnisocket_session_sync_client_state_locked(session, client);
|
||||
if (session->active_ops > 0) {
|
||||
session->active_ops -= 1;
|
||||
}
|
||||
if (session->closing && session->active_ops == 0) {
|
||||
pthread_cond_broadcast(&session->idle_cond);
|
||||
}
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int omnisocket_session_recv(omnisocket_session_t *session, message_t *out_msg, int timeout_ms) {
|
||||
kcp_client_t *client;
|
||||
int rc;
|
||||
|
||||
if (session == NULL || out_msg == NULL) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (omnisocket_session_begin_client_op(session, &client) != 0) {
|
||||
return -1;
|
||||
}
|
||||
rc = kcp_client_receive_timed(client, out_msg, timeout_ms);
|
||||
pthread_mutex_lock(&session->mutex);
|
||||
if (rc == 0) {
|
||||
session->stats.recv_calls += 1;
|
||||
session->stats.recv_bytes += (uint64_t) out_msg->body_len;
|
||||
} else if (rc == 1) {
|
||||
session->stats.recv_timeouts += 1;
|
||||
} else {
|
||||
session->stats.recv_errors += 1;
|
||||
}
|
||||
omnisocket_session_sync_client_state_locked(session, client);
|
||||
if (session->active_ops > 0) {
|
||||
session->active_ops -= 1;
|
||||
}
|
||||
if (session->closing && session->active_ops == 0) {
|
||||
pthread_cond_broadcast(&session->idle_cond);
|
||||
}
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int omnisocket_session_recv_into(
|
||||
omnisocket_session_t *session,
|
||||
void *buffer,
|
||||
size_t buffer_len,
|
||||
kcp_client_recv_meta_t *out_meta,
|
||||
int timeout_ms
|
||||
) {
|
||||
kcp_client_t *client;
|
||||
int rc;
|
||||
|
||||
if (session == NULL || out_meta == NULL || (buffer == NULL && buffer_len > 0)) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (omnisocket_session_begin_client_op(session, &client) != 0) {
|
||||
return -1;
|
||||
}
|
||||
rc = kcp_client_receive_binary_into(client, buffer, buffer_len, out_meta, timeout_ms);
|
||||
pthread_mutex_lock(&session->mutex);
|
||||
if (rc == 0) {
|
||||
session->stats.recv_calls += 1;
|
||||
session->stats.recv_bytes += (uint64_t) out_meta->body_len;
|
||||
} else if (rc == 1) {
|
||||
session->stats.recv_timeouts += 1;
|
||||
} else {
|
||||
session->stats.recv_errors += 1;
|
||||
}
|
||||
omnisocket_session_sync_client_state_locked(session, client);
|
||||
if (session->active_ops > 0) {
|
||||
session->active_ops -= 1;
|
||||
}
|
||||
if (session->closing && session->active_ops == 0) {
|
||||
pthread_cond_broadcast(&session->idle_cond);
|
||||
}
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
return rc;
|
||||
}
|
||||
|
||||
void omnisocket_session_stats_snapshot(omnisocket_session_t *session, omnisocket_session_stats_t *out_stats) {
|
||||
if (session == NULL || out_stats == NULL) {
|
||||
return;
|
||||
}
|
||||
pthread_mutex_lock(&session->mutex);
|
||||
*out_stats = session->stats;
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
}
|
||||
|
||||
void omnisocket_session_kcp_stats_snapshot(omnisocket_session_t *session, omnisocket_session_kcp_stats_t *out_stats) {
|
||||
kcp_runtime_stats_t runtime_stats;
|
||||
|
||||
if (session == NULL || out_stats == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
memset(&runtime_stats, 0, sizeof(runtime_stats));
|
||||
pthread_mutex_lock(&session->mutex);
|
||||
if (session->client != NULL) {
|
||||
kcp_client_runtime_stats_snapshot(session->client, &runtime_stats);
|
||||
}
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
|
||||
memset(out_stats, 0, sizeof(*out_stats));
|
||||
out_stats->connected = runtime_stats.connected;
|
||||
out_stats->conv = runtime_stats.conv;
|
||||
out_stats->rto_ms = runtime_stats.rto_ms;
|
||||
out_stats->srtt_ms = runtime_stats.srtt_ms;
|
||||
out_stats->min_srtt_ms = runtime_stats.min_srtt_ms;
|
||||
out_stats->srttvar_ms = runtime_stats.srttvar_ms;
|
||||
out_stats->last_feedback_age_ms = runtime_stats.last_feedback_age_ms;
|
||||
out_stats->snd_wnd = runtime_stats.snd_wnd;
|
||||
out_stats->rmt_wnd = runtime_stats.rmt_wnd;
|
||||
out_stats->inflight = runtime_stats.inflight;
|
||||
out_stats->window_limit = runtime_stats.window_limit;
|
||||
out_stats->window_pressure_pct = runtime_stats.window_pressure_pct;
|
||||
out_stats->snd_queue = runtime_stats.snd_queue;
|
||||
out_stats->rcv_queue = runtime_stats.rcv_queue;
|
||||
out_stats->snd_buffer = runtime_stats.snd_buffer;
|
||||
out_stats->out_segs_total = runtime_stats.out_segs_total;
|
||||
out_stats->retrans_total = runtime_stats.retrans_total;
|
||||
out_stats->fast_retrans_total = runtime_stats.fast_retrans_total;
|
||||
out_stats->lost_total = runtime_stats.lost_total;
|
||||
out_stats->repeat_total = runtime_stats.repeat_total;
|
||||
out_stats->xmit_total = runtime_stats.xmit_total;
|
||||
}
|
||||
|
||||
int omnisocket_udp_session_init(omnisocket_udp_session_t *session) {
|
||||
int rc;
|
||||
|
||||
if (session == NULL) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
memset(session, 0, sizeof(*session));
|
||||
rc = pthread_mutex_init(&session->mutex, NULL);
|
||||
if (rc != 0) {
|
||||
errno = rc;
|
||||
return -1;
|
||||
}
|
||||
rc = pthread_cond_init(&session->idle_cond, NULL);
|
||||
if (rc != 0) {
|
||||
pthread_mutex_destroy(&session->mutex);
|
||||
errno = rc;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void omnisocket_udp_session_destroy(omnisocket_udp_session_t *session) {
|
||||
if (session == NULL) {
|
||||
return;
|
||||
}
|
||||
(void) omnisocket_udp_session_close(session);
|
||||
pthread_cond_destroy(&session->idle_cond);
|
||||
pthread_mutex_destroy(&session->mutex);
|
||||
}
|
||||
|
||||
static int omnisocket_udp_session_begin_client_op(omnisocket_udp_session_t *session, udp_client_t **out_client) {
|
||||
if (session == NULL || out_client == NULL) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&session->mutex);
|
||||
if (session->closing) {
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
errno = ECANCELED;
|
||||
return -1;
|
||||
}
|
||||
if (session->client == NULL) {
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
errno = ENOTCONN;
|
||||
return -1;
|
||||
}
|
||||
*out_client = session->client;
|
||||
session->active_ops += 1;
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int omnisocket_udp_session_connect(
|
||||
omnisocket_udp_session_t *session,
|
||||
const char *server_addr,
|
||||
const char *peer_id,
|
||||
const char *bind_ip,
|
||||
const char *bind_device,
|
||||
int enable_timestamping
|
||||
) {
|
||||
udp_client_t *client;
|
||||
|
||||
if (session == NULL || server_addr == NULL || peer_id == NULL) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&session->mutex);
|
||||
while (session->closing) {
|
||||
pthread_cond_wait(&session->idle_cond, &session->mutex);
|
||||
}
|
||||
if (session->client != NULL) {
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
errno = EISCONN;
|
||||
return -1;
|
||||
}
|
||||
client = udp_client_dial_with_options(
|
||||
server_addr,
|
||||
peer_id,
|
||||
bind_ip,
|
||||
bind_device,
|
||||
NULL,
|
||||
NULL,
|
||||
enable_timestamping
|
||||
);
|
||||
if (client == NULL) {
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
return -1;
|
||||
}
|
||||
session->client = client;
|
||||
session->stats.connected = 1;
|
||||
session->stats.registered = 1;
|
||||
session->stats.last_server_error[0] = '\0';
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int omnisocket_udp_session_close(omnisocket_udp_session_t *session) {
|
||||
udp_client_t *client;
|
||||
|
||||
if (session == NULL) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&session->mutex);
|
||||
while (session->closing) {
|
||||
pthread_cond_wait(&session->idle_cond, &session->mutex);
|
||||
}
|
||||
client = session->client;
|
||||
if (client != NULL) {
|
||||
session->closing = 1;
|
||||
session->client = NULL;
|
||||
}
|
||||
session->stats.connected = 0;
|
||||
session->stats.registered = 0;
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
|
||||
if (client != NULL) {
|
||||
udp_client_close(client);
|
||||
pthread_mutex_lock(&session->mutex);
|
||||
while (session->active_ops > 0) {
|
||||
pthread_cond_wait(&session->idle_cond, &session->mutex);
|
||||
}
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
udp_client_free(client);
|
||||
pthread_mutex_lock(&session->mutex);
|
||||
session->closing = 0;
|
||||
pthread_cond_broadcast(&session->idle_cond);
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int omnisocket_udp_session_send(omnisocket_udp_session_t *session, const char *to, const void *data, size_t data_len) {
|
||||
udp_client_t *client;
|
||||
int rc;
|
||||
|
||||
if (session == NULL || to == NULL || (data == NULL && data_len > 0)) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (omnisocket_udp_session_begin_client_op(session, &client) != 0) {
|
||||
return -1;
|
||||
}
|
||||
rc = udp_client_send_binary(client, to, data, data_len);
|
||||
pthread_mutex_lock(&session->mutex);
|
||||
if (rc == 0) {
|
||||
session->stats.send_calls += 1;
|
||||
session->stats.send_bytes += (uint64_t) data_len;
|
||||
} else {
|
||||
session->stats.send_errors += 1;
|
||||
}
|
||||
if (session->active_ops > 0) {
|
||||
session->active_ops -= 1;
|
||||
}
|
||||
if (session->closing && session->active_ops == 0) {
|
||||
pthread_cond_broadcast(&session->idle_cond);
|
||||
}
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int omnisocket_udp_session_recv(omnisocket_udp_session_t *session, message_t *out_msg, int timeout_ms) {
|
||||
udp_client_t *client;
|
||||
int rc;
|
||||
|
||||
if (session == NULL || out_msg == NULL) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (omnisocket_udp_session_begin_client_op(session, &client) != 0) {
|
||||
return -1;
|
||||
}
|
||||
rc = udp_client_receive_timed(client, out_msg, timeout_ms);
|
||||
pthread_mutex_lock(&session->mutex);
|
||||
if (rc == 0) {
|
||||
session->stats.recv_calls += 1;
|
||||
session->stats.recv_bytes += (uint64_t) out_msg->body_len;
|
||||
} else if (rc == 1) {
|
||||
session->stats.recv_timeouts += 1;
|
||||
} else {
|
||||
session->stats.recv_errors += 1;
|
||||
}
|
||||
if (session->active_ops > 0) {
|
||||
session->active_ops -= 1;
|
||||
}
|
||||
if (session->closing && session->active_ops == 0) {
|
||||
pthread_cond_broadcast(&session->idle_cond);
|
||||
}
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int omnisocket_udp_session_recv_into(
|
||||
omnisocket_udp_session_t *session,
|
||||
void *buffer,
|
||||
size_t buffer_len,
|
||||
udp_client_recv_meta_t *out_meta,
|
||||
int timeout_ms
|
||||
) {
|
||||
udp_client_t *client;
|
||||
int rc;
|
||||
|
||||
if (session == NULL || out_meta == NULL || (buffer == NULL && buffer_len > 0)) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (omnisocket_udp_session_begin_client_op(session, &client) != 0) {
|
||||
return -1;
|
||||
}
|
||||
rc = udp_client_receive_into(client, buffer, buffer_len, out_meta, timeout_ms);
|
||||
pthread_mutex_lock(&session->mutex);
|
||||
if (rc == 0) {
|
||||
session->stats.recv_calls += 1;
|
||||
session->stats.recv_bytes += (uint64_t) out_meta->body_len;
|
||||
} else if (rc == 1) {
|
||||
session->stats.recv_timeouts += 1;
|
||||
} else {
|
||||
session->stats.recv_errors += 1;
|
||||
}
|
||||
if (session->active_ops > 0) {
|
||||
session->active_ops -= 1;
|
||||
}
|
||||
if (session->closing && session->active_ops == 0) {
|
||||
pthread_cond_broadcast(&session->idle_cond);
|
||||
}
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
return rc;
|
||||
}
|
||||
|
||||
void omnisocket_udp_session_stats_snapshot(omnisocket_udp_session_t *session, omnisocket_session_stats_t *out_stats) {
|
||||
if (session == NULL || out_stats == NULL) {
|
||||
return;
|
||||
}
|
||||
pthread_mutex_lock(&session->mutex);
|
||||
*out_stats = session->stats;
|
||||
pthread_mutex_unlock(&session->mutex);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
#ifndef OMNISOCKET_PY_CLIENT_H
|
||||
#define OMNISOCKET_PY_CLIENT_H
|
||||
|
||||
#include "peer_kcp_client.h"
|
||||
#include "peer_udp_client.h"
|
||||
|
||||
typedef struct omnisocket_session_stats {
|
||||
uint64_t send_calls;
|
||||
uint64_t send_bytes;
|
||||
uint64_t send_errors;
|
||||
uint64_t recv_calls;
|
||||
uint64_t recv_bytes;
|
||||
uint64_t recv_timeouts;
|
||||
uint64_t recv_errors;
|
||||
int connected;
|
||||
int registered;
|
||||
char last_server_error[256];
|
||||
} omnisocket_session_stats_t;
|
||||
|
||||
typedef struct omnisocket_session_kcp_stats {
|
||||
int connected;
|
||||
uint32_t conv;
|
||||
uint32_t rto_ms;
|
||||
int32_t srtt_ms;
|
||||
int32_t min_srtt_ms;
|
||||
int32_t srttvar_ms;
|
||||
uint32_t last_feedback_age_ms;
|
||||
uint32_t snd_wnd;
|
||||
uint32_t rmt_wnd;
|
||||
uint32_t inflight;
|
||||
uint32_t window_limit;
|
||||
double window_pressure_pct;
|
||||
uint32_t snd_queue;
|
||||
uint32_t rcv_queue;
|
||||
uint32_t snd_buffer;
|
||||
uint64_t out_segs_total;
|
||||
uint64_t retrans_total;
|
||||
uint64_t fast_retrans_total;
|
||||
uint64_t lost_total;
|
||||
uint64_t repeat_total;
|
||||
uint32_t xmit_total;
|
||||
} omnisocket_session_kcp_stats_t;
|
||||
|
||||
typedef struct omnisocket_session {
|
||||
pthread_mutex_t mutex;
|
||||
pthread_cond_t idle_cond;
|
||||
kcp_client_t *client;
|
||||
size_t active_ops;
|
||||
int closing;
|
||||
omnisocket_session_stats_t stats;
|
||||
} omnisocket_session_t;
|
||||
|
||||
typedef struct omnisocket_udp_session {
|
||||
pthread_mutex_t mutex;
|
||||
pthread_cond_t idle_cond;
|
||||
udp_client_t *client;
|
||||
size_t active_ops;
|
||||
int closing;
|
||||
omnisocket_session_stats_t stats;
|
||||
} omnisocket_udp_session_t;
|
||||
|
||||
int omnisocket_session_init(omnisocket_session_t *session);
|
||||
void omnisocket_session_destroy(omnisocket_session_t *session);
|
||||
|
||||
int omnisocket_session_connect(
|
||||
omnisocket_session_t *session,
|
||||
const char *server_addr,
|
||||
const char *relay_via,
|
||||
const char *peer_id,
|
||||
const char *bind_ip,
|
||||
const char *bind_device,
|
||||
const kcp_conn_options_t *options,
|
||||
int stats_interval_ms
|
||||
);
|
||||
int omnisocket_session_close(omnisocket_session_t *session);
|
||||
int omnisocket_session_send(omnisocket_session_t *session, const char *to, const void *data, size_t data_len);
|
||||
int omnisocket_session_send_with_id(
|
||||
omnisocket_session_t *session,
|
||||
const char *to,
|
||||
const void *data,
|
||||
size_t data_len,
|
||||
uint64_t *out_message_id
|
||||
);
|
||||
int omnisocket_session_recv(omnisocket_session_t *session, message_t *out_msg, int timeout_ms);
|
||||
int omnisocket_session_recv_into(
|
||||
omnisocket_session_t *session,
|
||||
void *buffer,
|
||||
size_t buffer_len,
|
||||
kcp_client_recv_meta_t *out_meta,
|
||||
int timeout_ms
|
||||
);
|
||||
void omnisocket_session_stats_snapshot(omnisocket_session_t *session, omnisocket_session_stats_t *out_stats);
|
||||
void omnisocket_session_kcp_stats_snapshot(omnisocket_session_t *session, omnisocket_session_kcp_stats_t *out_stats);
|
||||
|
||||
int omnisocket_udp_session_init(omnisocket_udp_session_t *session);
|
||||
void omnisocket_udp_session_destroy(omnisocket_udp_session_t *session);
|
||||
|
||||
int omnisocket_udp_session_connect(
|
||||
omnisocket_udp_session_t *session,
|
||||
const char *server_addr,
|
||||
const char *peer_id,
|
||||
const char *bind_ip,
|
||||
const char *bind_device,
|
||||
int enable_timestamping
|
||||
);
|
||||
int omnisocket_udp_session_close(omnisocket_udp_session_t *session);
|
||||
int omnisocket_udp_session_send(omnisocket_udp_session_t *session, const char *to, const void *data, size_t data_len);
|
||||
int omnisocket_udp_session_recv(omnisocket_udp_session_t *session, message_t *out_msg, int timeout_ms);
|
||||
int omnisocket_udp_session_recv_into(
|
||||
omnisocket_udp_session_t *session,
|
||||
void *buffer,
|
||||
size_t buffer_len,
|
||||
udp_client_recv_meta_t *out_meta,
|
||||
int timeout_ms
|
||||
);
|
||||
void omnisocket_udp_session_stats_snapshot(omnisocket_udp_session_t *session, omnisocket_session_stats_t *out_stats);
|
||||
|
||||
#endif
|
||||
58
robot/ros2/OmniSocketGo_robot_ros/python/setup.py
Normal file
58
robot/ros2/OmniSocketGo_robot_ros/python/setup.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from setuptools import Extension, setup
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
PY_ROOT = Path(__file__).resolve().parent
|
||||
|
||||
if sys.platform != "linux":
|
||||
raise RuntimeError("omnisocket Python extension can only be built on Linux")
|
||||
|
||||
|
||||
COMMON_SOURCES = [
|
||||
ROOT / "src" / "omni_common.c",
|
||||
ROOT / "src" / "protocol.c",
|
||||
ROOT / "src" / "latencylog.c",
|
||||
ROOT / "src" / "tx_timestamp_debug.c",
|
||||
ROOT / "src" / "kcp_packet_debug.c",
|
||||
ROOT / "src" / "kcp_session_stats.c",
|
||||
ROOT / "src" / "linux_timestamping.c",
|
||||
ROOT / "src" / "interactive.c",
|
||||
ROOT / "src" / "transport_udp.c",
|
||||
ROOT / "src" / "transport_kcp.c",
|
||||
ROOT / "src" / "server_udp_relay.c",
|
||||
ROOT / "src" / "server_udp_hub.c",
|
||||
ROOT / "src" / "server_kcp_hub.c",
|
||||
ROOT / "src" / "peer_udp_client.c",
|
||||
ROOT / "src" / "peer_kcp_client.c",
|
||||
ROOT / "third_party" / "cjson" / "cJSON.c",
|
||||
ROOT / "third_party" / "kcp" / "ikcp.c",
|
||||
]
|
||||
|
||||
|
||||
setup(
|
||||
name="omnisocket",
|
||||
version="0.1.0",
|
||||
packages=["omnisocket"],
|
||||
ext_modules=[
|
||||
Extension(
|
||||
"omnisocket._omnisocket",
|
||||
sources=[
|
||||
str(PY_ROOT / "omnisocket" / "_omnisocket.c"),
|
||||
str(PY_ROOT / "omnisocket" / "omnisocket_client.c"),
|
||||
*[str(path) for path in COMMON_SOURCES],
|
||||
],
|
||||
include_dirs=[
|
||||
str(ROOT / "include"),
|
||||
str(ROOT / "third_party" / "cjson"),
|
||||
str(ROOT / "third_party" / "kcp"),
|
||||
str(PY_ROOT / "omnisocket"),
|
||||
],
|
||||
define_macros=[("_GNU_SOURCE", None)],
|
||||
extra_compile_args=["-std=c11", "-O2", "-pthread"],
|
||||
extra_link_args=["-pthread"],
|
||||
)
|
||||
],
|
||||
)
|
||||
318
robot/ros2/OmniSocketGo_robot_ros/python/tests/test_sessions.py
Normal file
318
robot/ros2/OmniSocketGo_robot_ros/python/tests/test_sessions.py
Normal file
@@ -0,0 +1,318 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skipif(sys.platform != 'linux', reason='Linux-only OmniSocket extension')
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
PYTHON_ROOT = ROOT / 'python'
|
||||
if str(PYTHON_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PYTHON_ROOT))
|
||||
|
||||
omnisocket = pytest.importorskip('omnisocket')
|
||||
|
||||
CONTROL_DEFAULTS = omnisocket.CONTROL_DEFAULTS
|
||||
MSG_TYPE_BINARY = omnisocket.MSG_TYPE_BINARY
|
||||
Session = omnisocket.Session
|
||||
UdpSession = omnisocket.UdpSession
|
||||
|
||||
|
||||
def _reserve_port() -> int:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.bind(('127.0.0.1', 0))
|
||||
return int(sock.getsockname()[1])
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _run_server(binary_name: str, listen_addr: str):
|
||||
binary = ROOT / 'bin' / binary_name
|
||||
if not binary.exists():
|
||||
pytest.skip(f'{binary} is not built')
|
||||
|
||||
process = subprocess.Popen(
|
||||
[str(binary), '-listen', listen_addr],
|
||||
cwd=str(ROOT),
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
try:
|
||||
time.sleep(0.2)
|
||||
yield process
|
||||
finally:
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=2.0)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait(timeout=2.0)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _run_relay(listen_addr: str, remote_addr: str):
|
||||
binary = ROOT / 'bin' / 'kcpserver'
|
||||
if not binary.exists():
|
||||
pytest.skip(f'{binary} is not built')
|
||||
|
||||
process = subprocess.Popen(
|
||||
[str(binary), '-mode', 'relay', '-listen', listen_addr, '-relay-remote', remote_addr],
|
||||
cwd=str(ROOT),
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
try:
|
||||
time.sleep(0.2)
|
||||
yield process
|
||||
finally:
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=2.0)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait(timeout=2.0)
|
||||
|
||||
|
||||
def _connect_with_retry(session_cls, *, transport: str, server_addr: str, peer_id: str, relay_via: str = ''):
|
||||
deadline = time.monotonic() + 3.0
|
||||
last_error: Exception | None = None
|
||||
|
||||
while time.monotonic() < deadline:
|
||||
session = session_cls()
|
||||
try:
|
||||
kwargs: dict[str, object] = {
|
||||
'server_addr': server_addr,
|
||||
'peer_id': peer_id,
|
||||
}
|
||||
if transport == 'kcp':
|
||||
kwargs.update(CONTROL_DEFAULTS)
|
||||
if relay_via:
|
||||
kwargs['relay_via'] = relay_via
|
||||
else:
|
||||
kwargs['enable_timestamping'] = False
|
||||
session.connect(**kwargs)
|
||||
return session
|
||||
except OSError as exc:
|
||||
last_error = exc
|
||||
time.sleep(0.1)
|
||||
|
||||
raise AssertionError(f'failed to connect {peer_id} to {server_addr}: {last_error}')
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('transport', 'binary_name', 'session_cls'),
|
||||
[
|
||||
('udp', 'udpserver', UdpSession),
|
||||
('kcp', 'kcpserver', Session),
|
||||
],
|
||||
)
|
||||
def test_control_sessions_smoke(transport: str, binary_name: str, session_cls) -> None:
|
||||
port = _reserve_port()
|
||||
listen_addr = f'127.0.0.1:{port}'
|
||||
sender_id = f'pytest-{transport}-sender'
|
||||
receiver_id = f'pytest-{transport}-receiver'
|
||||
|
||||
with _run_server(binary_name, listen_addr):
|
||||
sender = _connect_with_retry(session_cls, transport=transport, server_addr=listen_addr, peer_id=sender_id)
|
||||
receiver = _connect_with_retry(session_cls, transport=transport, server_addr=listen_addr, peer_id=receiver_id)
|
||||
|
||||
try:
|
||||
assert receiver.recv(timeout_ms=20) is None
|
||||
|
||||
payload = b'control-packet-1'
|
||||
sender.send(to=receiver_id, data=payload)
|
||||
from_peer, msg_type, recv_payload = receiver.recv(timeout_ms=1000)
|
||||
assert from_peer == sender_id
|
||||
assert msg_type == MSG_TYPE_BINARY
|
||||
assert recv_payload == payload
|
||||
|
||||
payload2 = b'control-packet-2'
|
||||
sender.send(to=receiver_id, data=payload2)
|
||||
recv_buffer = bytearray(128)
|
||||
meta = receiver.recv_into(buffer=recv_buffer, timeout_ms=1000)
|
||||
assert meta is not None
|
||||
assert meta['from'] == sender_id
|
||||
assert meta['msg_type'] == MSG_TYPE_BINARY
|
||||
assert meta['body_len'] == len(payload2)
|
||||
assert bytes(recv_buffer[: meta['body_len']]) == payload2
|
||||
|
||||
sender_stats = sender.stats()
|
||||
receiver_stats = receiver.stats()
|
||||
assert sender_stats['connected'] == 1
|
||||
assert receiver_stats['connected'] == 1
|
||||
assert sender_stats['registered'] == 1
|
||||
assert receiver_stats['registered'] == 1
|
||||
assert sender_stats['send_calls'] >= 2
|
||||
assert receiver_stats['recv_calls'] >= 2
|
||||
if transport == 'kcp':
|
||||
sender_kcp_stats = sender.kcp_stats()
|
||||
receiver_kcp_stats = receiver.kcp_stats()
|
||||
assert sender_kcp_stats['connected'] == 1
|
||||
assert receiver_kcp_stats['connected'] == 1
|
||||
assert 'srtt_ms' in sender_kcp_stats
|
||||
assert 'snd_queue' in receiver_kcp_stats
|
||||
finally:
|
||||
sender.close()
|
||||
receiver.close()
|
||||
|
||||
|
||||
def test_kcp_duplicate_peer_new_instance_wins() -> None:
|
||||
port = _reserve_port()
|
||||
listen_addr = f'127.0.0.1:{port}'
|
||||
shared_peer_id = 'pytest-kcp-shared-peer'
|
||||
sender_id = 'pytest-kcp-unique-sender'
|
||||
|
||||
with _run_server('kcpserver', listen_addr):
|
||||
original = _connect_with_retry(Session, transport='kcp', server_addr=listen_addr, peer_id=shared_peer_id)
|
||||
sender = _connect_with_retry(Session, transport='kcp', server_addr=listen_addr, peer_id=sender_id)
|
||||
replacement = None
|
||||
|
||||
try:
|
||||
replacement = _connect_with_retry(Session, transport='kcp', server_addr=listen_addr, peer_id=shared_peer_id)
|
||||
replacement_stats = replacement.stats()
|
||||
assert replacement_stats['connected'] == 1
|
||||
assert replacement_stats['registered'] == 1
|
||||
|
||||
with pytest.raises(OSError):
|
||||
original.recv(timeout_ms=1000)
|
||||
|
||||
payload = b'registered-replacement'
|
||||
sender.send(to=shared_peer_id, data=payload)
|
||||
from_peer, msg_type, recv_payload = replacement.recv(timeout_ms=1000)
|
||||
assert from_peer == sender_id
|
||||
assert msg_type == MSG_TYPE_BINARY
|
||||
assert recv_payload == payload
|
||||
finally:
|
||||
original.close()
|
||||
sender.close()
|
||||
if replacement is not None:
|
||||
replacement.close()
|
||||
|
||||
|
||||
def test_kcp_idle_video_peers_survive_without_receive_loop() -> None:
|
||||
port = _reserve_port()
|
||||
listen_addr = f'127.0.0.1:{port}'
|
||||
sender_id = 'peer-b-video'
|
||||
receiver_id = 'pytest-kcp-video-idle-receiver'
|
||||
|
||||
with _run_server('kcpserver', listen_addr):
|
||||
sender = _connect_with_retry(Session, transport='kcp', server_addr=listen_addr, peer_id=sender_id)
|
||||
receiver = _connect_with_retry(Session, transport='kcp', server_addr=listen_addr, peer_id=receiver_id)
|
||||
|
||||
try:
|
||||
time.sleep(5.0)
|
||||
|
||||
payload = b'idle-video-session-still-alive'
|
||||
sender.send(to=receiver_id, data=payload)
|
||||
from_peer, msg_type, recv_payload = receiver.recv(timeout_ms=1000)
|
||||
assert from_peer == sender_id
|
||||
assert msg_type == MSG_TYPE_BINARY
|
||||
assert recv_payload == payload
|
||||
finally:
|
||||
sender.close()
|
||||
receiver.close()
|
||||
|
||||
|
||||
def test_kcp_peer_a_video_stale_receiver_is_evicted() -> None:
|
||||
port = _reserve_port()
|
||||
listen_addr = f'127.0.0.1:{port}'
|
||||
receiver_id = 'peer-a-video'
|
||||
|
||||
with _run_server('kcpserver', listen_addr):
|
||||
receiver = _connect_with_retry(Session, transport='kcp', server_addr=listen_addr, peer_id=receiver_id)
|
||||
|
||||
try:
|
||||
time.sleep(5.0)
|
||||
with pytest.raises(OSError):
|
||||
receiver.recv(timeout_ms=1000)
|
||||
finally:
|
||||
receiver.close()
|
||||
|
||||
|
||||
def test_kcp_relay_routes_multiple_sessions_by_conv() -> None:
|
||||
hub_port = _reserve_port()
|
||||
relay_port = _reserve_port()
|
||||
hub_addr = f'127.0.0.1:{hub_port}'
|
||||
relay_addr = f'127.0.0.1:{relay_port}'
|
||||
|
||||
with _run_server('kcpserver', hub_addr):
|
||||
with _run_relay(relay_addr, hub_addr):
|
||||
sender = _connect_with_retry(Session, transport='kcp', server_addr=hub_addr, peer_id='pytest-relay-sender', relay_via=relay_addr)
|
||||
receiver = _connect_with_retry(Session, transport='kcp', server_addr=hub_addr, peer_id='pytest-relay-receiver', relay_via=relay_addr)
|
||||
chatter = _connect_with_retry(Session, transport='kcp', server_addr=hub_addr, peer_id='pytest-relay-chatter', relay_via=relay_addr)
|
||||
|
||||
try:
|
||||
chatter.send(to='pytest-relay-sender', data=b'chatter-primes-last-client')
|
||||
from_peer, msg_type, recv_payload = sender.recv(timeout_ms=1000)
|
||||
assert from_peer == 'pytest-relay-chatter'
|
||||
assert msg_type == MSG_TYPE_BINARY
|
||||
assert recv_payload == b'chatter-primes-last-client'
|
||||
|
||||
sender.send(to='pytest-relay-receiver', data=b'relay-video-frame')
|
||||
from_peer, msg_type, recv_payload = receiver.recv(timeout_ms=1000)
|
||||
assert from_peer == 'pytest-relay-sender'
|
||||
assert msg_type == MSG_TYPE_BINARY
|
||||
assert recv_payload == b'relay-video-frame'
|
||||
finally:
|
||||
sender.close()
|
||||
receiver.close()
|
||||
chatter.close()
|
||||
|
||||
|
||||
def test_udp_session_close_interrupts_blocking_recv() -> None:
|
||||
port = _reserve_port()
|
||||
listen_addr = f'127.0.0.1:{port}'
|
||||
receiver_id = 'pytest-udp-blocking-recv'
|
||||
|
||||
with _run_server('udpserver', listen_addr):
|
||||
receiver = _connect_with_retry(
|
||||
UdpSession,
|
||||
transport='udp',
|
||||
server_addr=listen_addr,
|
||||
peer_id=receiver_id,
|
||||
)
|
||||
|
||||
recv_error: list[BaseException] = []
|
||||
close_error: list[BaseException] = []
|
||||
recv_started = threading.Event()
|
||||
recv_done = threading.Event()
|
||||
close_done = threading.Event()
|
||||
|
||||
def recv_worker() -> None:
|
||||
recv_started.set()
|
||||
try:
|
||||
receiver.recv()
|
||||
except BaseException as exc: # pragma: no cover - assertion is on thread completion
|
||||
recv_error.append(exc)
|
||||
finally:
|
||||
recv_done.set()
|
||||
|
||||
def close_worker() -> None:
|
||||
try:
|
||||
receiver.close()
|
||||
except BaseException as exc: # pragma: no cover - assertion is on thread completion
|
||||
close_error.append(exc)
|
||||
finally:
|
||||
close_done.set()
|
||||
|
||||
recv_thread = threading.Thread(target=recv_worker, daemon=True)
|
||||
recv_thread.start()
|
||||
assert recv_started.wait(timeout=1.0)
|
||||
time.sleep(0.05)
|
||||
|
||||
close_thread = threading.Thread(target=close_worker, daemon=True)
|
||||
close_thread.start()
|
||||
|
||||
assert close_done.wait(timeout=1.0), 'UdpSession.close() blocked while recv() was waiting'
|
||||
assert recv_done.wait(timeout=1.0), 'UdpSession.recv() stayed blocked after close()'
|
||||
assert not close_thread.is_alive()
|
||||
assert not recv_thread.is_alive()
|
||||
assert not close_error
|
||||
assert not recv_error or isinstance(recv_error[0], OSError)
|
||||
Reference in New Issue
Block a user