509 lines
15 KiB
C
509 lines
15 KiB
C
/*
|
|
Copyright 2025 Quectel Wireless Solutions Co.,Ltd
|
|
|
|
Quectel hereby grants customers of Quectel a license to use, modify,
|
|
distribute and publish the Software in binary form provided that
|
|
customers shall have no right to reverse engineer, reverse assemble,
|
|
decompile or reduce to source code form any portion of the Software.
|
|
Under no circumstances may customers modify, demonstrate, use, deliver
|
|
or disclose any portion of the Software in source code form.
|
|
*/
|
|
|
|
//https://source.codeaurora.org/quic/la/platform/vendor/qcom-opensource/dataservices/tree/rmnetctl
|
|
#include <sys/socket.h>
|
|
#include <stdint.h>
|
|
#include <linux/netlink.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <unistd.h>
|
|
#include <stdlib.h>
|
|
#include <errno.h>
|
|
#include <linux/rtnetlink.h>
|
|
#include <linux/gen_stats.h>
|
|
#include <net/if.h>
|
|
#include <asm/types.h>
|
|
#include <linux/rmnet_data.h>
|
|
#include "QMIThread.h"
|
|
|
|
#undef RMNET_EGRESS_FORMAT_AGGREGATION //QSDK SPF11.4
|
|
#define RMNET_EGRESS_FORMAT_AGGREGATION (1U << 31)
|
|
#ifndef RMNET_FLAGS_INGRESS_MAP_CKSUMV5
|
|
#define RMNET_FLAGS_INGRESS_MAP_CKSUMV5 (1U << 5)
|
|
#define RMNET_FLAGS_EGRESS_MAP_CKSUMV5 (1U << 6)
|
|
#endif
|
|
|
|
#define RMNETCTL_SUCCESS 0
|
|
#define RMNETCTL_LIB_ERR 1
|
|
#define RMNETCTL_KERNEL_ERR 2
|
|
#define RMNETCTL_INVALID_ARG 3
|
|
|
|
#define CONFIG_QRTR_IPQ_NSS_OFFLOAD //defined for ipq nss offload support
|
|
|
|
enum rmnetctl_error_codes_e {
|
|
RMNETCTL_API_SUCCESS = 0,
|
|
|
|
RMNETCTL_API_FIRST_ERR = 1,
|
|
RMNETCTL_API_ERR_MESSAGE_SEND = 3,
|
|
RMNETCTL_API_ERR_MESSAGE_RECEIVE = 4,
|
|
|
|
RMNETCTL_INIT_FIRST_ERR = 5,
|
|
RMNETCTL_INIT_ERR_PROCESS_ID = RMNETCTL_INIT_FIRST_ERR,
|
|
RMNETCTL_INIT_ERR_NETLINK_FD = 6,
|
|
RMNETCTL_INIT_ERR_BIND = 7,
|
|
|
|
RMNETCTL_API_SECOND_ERR = 9,
|
|
RMNETCTL_API_ERR_HNDL_INVALID = RMNETCTL_API_SECOND_ERR,
|
|
RMNETCTL_API_ERR_RETURN_TYPE = 13,
|
|
|
|
RMNETCTL_API_THIRD_ERR = 25,
|
|
/* Failed to copy data into netlink message */
|
|
RMNETCTL_API_ERR_RTA_FAILURE = RMNETCTL_API_THIRD_ERR,
|
|
};
|
|
|
|
struct rmnetctl_hndl_s {
|
|
uint32_t pid;
|
|
uint32_t transaction_id;
|
|
int netlink_fd;
|
|
struct sockaddr_nl src_addr, dest_addr;
|
|
};
|
|
typedef struct rmnetctl_hndl_s rmnetctl_hndl_t;
|
|
|
|
#define NLMSG_TAIL(nmsg) \
|
|
((struct rtattr *) (((char *)(nmsg)) + NLMSG_ALIGN((nmsg)->nlmsg_len)))
|
|
|
|
struct nlmsg {
|
|
struct nlmsghdr nl_addr;
|
|
struct ifinfomsg ifmsg;
|
|
char data[500];
|
|
};
|
|
|
|
#define MIN_VALID_PROCESS_ID 0
|
|
#define MIN_VALID_SOCKET_FD 0
|
|
#define KERNEL_PROCESS_ID 0
|
|
#define UNICAST 0
|
|
|
|
/* IFLA Attributes for the RT RmNet driver */
|
|
enum {
|
|
RMNETCTL_IFLA_UNSPEC,
|
|
RMNETCTL_IFLA_MUX_ID,
|
|
RMNETCTL_IFLA_FLAGS,
|
|
RMNETCTL_IFLA_DFC_QOS,
|
|
RMNETCTL_IFLA_UPLINK_PARAMS,
|
|
RMNETCTL_IFLA_NSS_OFFLOAD,
|
|
__RMNETCTL_IFLA_MAX,
|
|
};
|
|
|
|
struct rmnetctl_uplink_params {
|
|
uint16_t byte_count;
|
|
uint8_t packet_count;
|
|
uint8_t features;
|
|
uint32_t time_limit;
|
|
};
|
|
|
|
static void rta_parse(struct rtattr **tb, int maxtype, struct rtattr *head,
|
|
int len)
|
|
{
|
|
struct rtattr *rta;
|
|
|
|
memset(tb, 0, sizeof(struct rtattr *) * maxtype);
|
|
for (rta = head; RTA_OK(rta, len);
|
|
rta = RTA_NEXT(rta, len)) {
|
|
__u16 type = rta->rta_type & NLA_TYPE_MASK;
|
|
|
|
if (type > 0 && type < maxtype)
|
|
tb[type] = rta;
|
|
}
|
|
}
|
|
|
|
static struct rtattr *rta_find(struct rtattr *rta, int attrlen, uint16_t type)
|
|
{
|
|
for (; RTA_OK(rta, attrlen); rta = RTA_NEXT(rta, attrlen)) {
|
|
if (rta->rta_type == (type & NLA_TYPE_MASK))
|
|
return rta;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static struct rtattr * rta_put(struct nlmsg *req,int type, int len, void *data)
|
|
{
|
|
struct rtattr *attrinfo = NLMSG_TAIL(&req->nl_addr);
|
|
|
|
attrinfo->rta_type = type;
|
|
attrinfo->rta_len = RTA_ALIGN(RTA_LENGTH(len));
|
|
if (data && len)
|
|
memcpy(RTA_DATA(attrinfo), data, len);
|
|
req->nl_addr.nlmsg_len = NLMSG_ALIGN(req->nl_addr.nlmsg_len) +
|
|
RTA_ALIGN(RTA_LENGTH(len));
|
|
return attrinfo;
|
|
}
|
|
|
|
static int rmnet_get_ack(rmnetctl_hndl_t *hndl, uint16_t *error_code)
|
|
{
|
|
struct nlack {
|
|
struct nlmsghdr ackheader;
|
|
struct nlmsgerr ackdata;
|
|
char data[256];
|
|
|
|
} ack;
|
|
int i;
|
|
|
|
if (!hndl || !error_code)
|
|
return RMNETCTL_INVALID_ARG;
|
|
|
|
if ((i = recv(hndl->netlink_fd, &ack, sizeof(ack), 0)) < 0) {
|
|
*error_code = errno;
|
|
return RMNETCTL_API_ERR_MESSAGE_RECEIVE;
|
|
}
|
|
|
|
/*Ack should always be NLMSG_ERROR type*/
|
|
if (ack.ackheader.nlmsg_type == NLMSG_ERROR) {
|
|
if (ack.ackdata.error == 0) {
|
|
*error_code = RMNETCTL_API_SUCCESS;
|
|
return RMNETCTL_SUCCESS;
|
|
} else {
|
|
*error_code = -ack.ackdata.error;
|
|
return RMNETCTL_KERNEL_ERR;
|
|
}
|
|
}
|
|
|
|
*error_code = RMNETCTL_API_ERR_RETURN_TYPE;
|
|
return RMNETCTL_API_FIRST_ERR;
|
|
}
|
|
|
|
static int rtrmnet_ctl_init(rmnetctl_hndl_t **hndl, uint16_t *error_code)
|
|
{
|
|
struct sockaddr_nl __attribute__((__may_alias__)) *saddr_ptr;
|
|
int netlink_fd = -1;
|
|
pid_t pid = 0;
|
|
|
|
if (!hndl || !error_code)
|
|
return RMNETCTL_INVALID_ARG;
|
|
|
|
*hndl = (rmnetctl_hndl_t *)malloc(sizeof(rmnetctl_hndl_t));
|
|
if (!*hndl) {
|
|
*error_code = RMNETCTL_API_ERR_HNDL_INVALID;
|
|
return RMNETCTL_LIB_ERR;
|
|
}
|
|
|
|
memset(*hndl, 0, sizeof(rmnetctl_hndl_t));
|
|
|
|
pid = getpid();
|
|
if (pid < MIN_VALID_PROCESS_ID) {
|
|
free(*hndl);
|
|
*error_code = RMNETCTL_INIT_ERR_PROCESS_ID;
|
|
return RMNETCTL_LIB_ERR;
|
|
}
|
|
(*hndl)->pid = KERNEL_PROCESS_ID;
|
|
netlink_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
|
|
if (netlink_fd < MIN_VALID_SOCKET_FD) {
|
|
free(*hndl);
|
|
*error_code = RMNETCTL_INIT_ERR_NETLINK_FD;
|
|
return RMNETCTL_LIB_ERR;
|
|
}
|
|
|
|
(*hndl)->netlink_fd = netlink_fd;
|
|
|
|
memset(&(*hndl)->src_addr, 0, sizeof(struct sockaddr_nl));
|
|
|
|
(*hndl)->src_addr.nl_family = AF_NETLINK;
|
|
(*hndl)->src_addr.nl_pid = (*hndl)->pid;
|
|
|
|
saddr_ptr = &(*hndl)->src_addr;
|
|
if (bind((*hndl)->netlink_fd,
|
|
(struct sockaddr *)saddr_ptr,
|
|
sizeof(struct sockaddr_nl)) < 0) {
|
|
close((*hndl)->netlink_fd);
|
|
free(*hndl);
|
|
*error_code = RMNETCTL_INIT_ERR_BIND;
|
|
return RMNETCTL_LIB_ERR;
|
|
}
|
|
|
|
memset(&(*hndl)->dest_addr, 0, sizeof(struct sockaddr_nl));
|
|
|
|
(*hndl)->dest_addr.nl_family = AF_NETLINK;
|
|
(*hndl)->dest_addr.nl_pid = KERNEL_PROCESS_ID;
|
|
(*hndl)->dest_addr.nl_groups = UNICAST;
|
|
|
|
return RMNETCTL_SUCCESS;
|
|
}
|
|
|
|
static int rtrmnet_ctl_deinit(rmnetctl_hndl_t *hndl)
|
|
{
|
|
if (!hndl)
|
|
return RMNETCTL_SUCCESS;
|
|
|
|
close(hndl->netlink_fd);
|
|
free(hndl);
|
|
|
|
return RMNETCTL_SUCCESS;
|
|
}
|
|
|
|
static int rtrmnet_ctl_newvnd(rmnetctl_hndl_t *hndl, char *devname, char *vndname,
|
|
uint16_t *error_code, uint8_t index,
|
|
uint32_t flagconfig, uint32_t ul_agg_cnt, uint32_t ul_agg_size)
|
|
{
|
|
struct rtattr *datainfo, *linkinfo;
|
|
struct ifla_vlan_flags flags;
|
|
int devindex;
|
|
char *kind = "rmnet";
|
|
struct nlmsg req;
|
|
short id;
|
|
|
|
if (!hndl || !devname || !vndname || !error_code)
|
|
return RMNETCTL_INVALID_ARG;
|
|
|
|
memset(&req, 0, sizeof(req));
|
|
req.nl_addr.nlmsg_type = RTM_NEWLINK;
|
|
req.nl_addr.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
|
|
req.nl_addr.nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_EXCL |
|
|
NLM_F_ACK;
|
|
req.nl_addr.nlmsg_seq = hndl->transaction_id;
|
|
hndl->transaction_id++;
|
|
|
|
/* Get index of devname*/
|
|
devindex = if_nametoindex(devname);
|
|
if (devindex < 0) {
|
|
*error_code = errno;
|
|
return RMNETCTL_KERNEL_ERR;
|
|
}
|
|
|
|
/* Setup link attr with devindex as data */
|
|
rta_put(&req, IFLA_LINK, sizeof(devindex), &devindex);
|
|
rta_put(&req, IFLA_IFNAME, strlen(vndname) + 1, vndname);
|
|
|
|
/* Set up IFLA info kind RMNET that has linkinfo and type */
|
|
linkinfo = rta_put(&req, IFLA_LINKINFO, 0, NULL);
|
|
rta_put(&req, IFLA_INFO_KIND, strlen(kind), kind);
|
|
|
|
datainfo = rta_put(&req, IFLA_INFO_DATA, 0, NULL);
|
|
id = index;
|
|
rta_put(&req, RMNETCTL_IFLA_MUX_ID, sizeof(id), &id);
|
|
|
|
if (flagconfig != 0) {
|
|
flags.mask = flagconfig;
|
|
flags.flags = flagconfig;
|
|
|
|
rta_put(&req, RMNETCTL_IFLA_FLAGS, sizeof(flags), &flags);
|
|
}
|
|
|
|
#ifdef CONFIG_QRTR_IPQ_NSS_OFFLOAD
|
|
if (!access("/sys/module/rmnet_nss/", F_OK)) {
|
|
uint8_t offload = 1;
|
|
|
|
rta_put(&req, RMNETCTL_IFLA_NSS_OFFLOAD, sizeof(offload), &offload);
|
|
}
|
|
#endif
|
|
|
|
if (ul_agg_cnt > 1) {
|
|
struct rmnetctl_uplink_params ul_agg;
|
|
|
|
ul_agg.byte_count = ul_agg_size;
|
|
ul_agg.packet_count = ul_agg_cnt;
|
|
ul_agg.features = 0;
|
|
ul_agg.time_limit = 3000000;
|
|
|
|
rta_put(&req, RMNETCTL_IFLA_UPLINK_PARAMS, sizeof(ul_agg), &ul_agg);
|
|
}
|
|
|
|
datainfo->rta_len = (char *)NLMSG_TAIL(&req.nl_addr) - (char *)datainfo;
|
|
linkinfo->rta_len = (char *)NLMSG_TAIL(&req.nl_addr) - (char *)linkinfo;
|
|
|
|
if (send(hndl->netlink_fd, &req, req.nl_addr.nlmsg_len, 0) < 0) {
|
|
*error_code = RMNETCTL_API_ERR_MESSAGE_SEND;
|
|
return RMNETCTL_LIB_ERR;
|
|
}
|
|
|
|
return rmnet_get_ack(hndl, error_code);
|
|
}
|
|
|
|
int rtrmnet_ctl_new_vnd(char *devname, char *vndname, uint8_t muxid,
|
|
uint32_t qmap_version, uint32_t ul_agg_cnt, uint32_t ul_agg_size)
|
|
{
|
|
struct rmnetctl_hndl_s *handle;
|
|
uint16_t error_code;
|
|
int return_code;
|
|
uint32_t flagconfig = RMNET_FLAGS_INGRESS_DEAGGREGATION;
|
|
|
|
dbg_time("%s dev: %s, vnd: %s, muxid: %d, qmap_version: %d",
|
|
__func__, devname, vndname, muxid, qmap_version);
|
|
|
|
if (ul_agg_cnt > 1)
|
|
flagconfig |= RMNET_EGRESS_FORMAT_AGGREGATION;
|
|
|
|
if (qmap_version == 9) { //QMAPV5
|
|
flagconfig |= RMNET_FLAGS_INGRESS_MAP_CKSUMV5;
|
|
flagconfig |= RMNET_FLAGS_EGRESS_MAP_CKSUMV5;
|
|
}
|
|
else if (qmap_version == 5) { //QMAPV1
|
|
}
|
|
else {
|
|
dbg_time("%s donot support qmap_version %d\n", __func__, qmap_version);
|
|
return -1001;
|
|
}
|
|
|
|
return_code = rtrmnet_ctl_init(&handle, &error_code);
|
|
if (return_code != RMNETCTL_SUCCESS) {
|
|
dbg_time("rtrmnet_ctl_init error_code: %d, return_code: %d, errno: %d (%s)",
|
|
error_code, return_code, errno, strerror(errno));
|
|
}
|
|
|
|
return_code = rtrmnet_ctl_newvnd(handle, devname, vndname, &error_code,
|
|
muxid, flagconfig, ul_agg_cnt, ul_agg_size);
|
|
if (return_code == RMNETCTL_KERNEL_ERR)
|
|
dbg_time("rtrmnet_ctl_newvnd RMNETCTL_KERNEL_ERR errno: %d (%s)",
|
|
error_code, strerror(error_code));
|
|
else if (return_code != RMNETCTL_SUCCESS) {
|
|
dbg_time("rtrmnet_ctl_newvnd error_code: %d, return_code: %d",
|
|
error_code, return_code);
|
|
}
|
|
|
|
rtrmnet_ctl_deinit(handle);
|
|
|
|
return return_code;
|
|
}
|
|
|
|
static int rtrmnet_ctl_getvnd(rmnetctl_hndl_t *hndl, char *vndname,
|
|
uint16_t *error_code, uint16_t *mux_id,
|
|
uint32_t *flagconfig, struct rmnetctl_uplink_params *ul_agg)
|
|
{
|
|
struct nlmsg req;
|
|
struct nlmsghdr *resp;
|
|
struct rtattr *attrs, *linkinfo, *datainfo;
|
|
struct rtattr *tb[__RMNETCTL_IFLA_MAX];
|
|
unsigned int devindex = 0;
|
|
int resp_len;
|
|
|
|
memset(&req, 0, sizeof(req));
|
|
|
|
if (!hndl || !vndname || !error_code || !(mux_id || flagconfig))
|
|
return RMNETCTL_INVALID_ARG;
|
|
|
|
req.nl_addr.nlmsg_type = RTM_GETLINK;
|
|
req.nl_addr.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
|
|
req.nl_addr.nlmsg_flags = NLM_F_REQUEST;
|
|
req.nl_addr.nlmsg_seq = hndl->transaction_id;
|
|
hndl->transaction_id++;
|
|
|
|
/* Get index of vndname */
|
|
devindex = if_nametoindex(vndname);
|
|
if (devindex == 0) {
|
|
*error_code = errno;
|
|
return RMNETCTL_KERNEL_ERR;
|
|
}
|
|
|
|
req.ifmsg.ifi_index = devindex;
|
|
if (send(hndl->netlink_fd, &req, req.nl_addr.nlmsg_len, 0) < 0) {
|
|
*error_code = RMNETCTL_API_ERR_MESSAGE_SEND;
|
|
return RMNETCTL_LIB_ERR;
|
|
}
|
|
|
|
resp_len = recv(hndl->netlink_fd, NULL, 0, MSG_PEEK | MSG_TRUNC);
|
|
if (resp_len < 0) {
|
|
*error_code = errno;
|
|
return RMNETCTL_API_ERR_MESSAGE_RECEIVE;
|
|
}
|
|
|
|
resp = malloc((size_t)resp_len);
|
|
if (!resp) {
|
|
*error_code = errno;
|
|
return RMNETCTL_LIB_ERR;
|
|
}
|
|
|
|
resp_len = recv(hndl->netlink_fd, (char *)resp, (size_t)resp_len, 0);
|
|
if (resp_len < 0) {
|
|
*error_code = errno;
|
|
free(resp);
|
|
return RMNETCTL_API_ERR_MESSAGE_RECEIVE;
|
|
}
|
|
|
|
/* Parse out the RT attributes */
|
|
attrs = (struct rtattr *)((char *)NLMSG_DATA(resp) + NLMSG_ALIGN(sizeof(req.ifmsg)));
|
|
linkinfo = rta_find(attrs, NLMSG_PAYLOAD(resp, sizeof(req.ifmsg)), IFLA_LINKINFO);
|
|
if (!linkinfo) {
|
|
free(resp);
|
|
*error_code = RMNETCTL_API_ERR_RTA_FAILURE;
|
|
return RMNETCTL_KERNEL_ERR;
|
|
}
|
|
|
|
datainfo = rta_find(RTA_DATA(linkinfo), RTA_PAYLOAD(linkinfo), IFLA_INFO_DATA);
|
|
if (!datainfo) {
|
|
free(resp);
|
|
*error_code = RMNETCTL_API_ERR_RTA_FAILURE;
|
|
return RMNETCTL_KERNEL_ERR;
|
|
}
|
|
|
|
/* Parse all the rmnet-specific information from the kernel */
|
|
rta_parse(tb, __RMNETCTL_IFLA_MAX, RTA_DATA(datainfo), RTA_PAYLOAD(datainfo));
|
|
if (tb[RMNETCTL_IFLA_MUX_ID] && mux_id) {
|
|
*mux_id = *((uint16_t *)RTA_DATA(tb[RMNETCTL_IFLA_MUX_ID]));
|
|
dbg_time("\tMux id: %d", *mux_id);
|
|
}
|
|
|
|
if (tb[RMNETCTL_IFLA_FLAGS] && flagconfig) {
|
|
struct ifla_vlan_flags *flags;
|
|
|
|
flags = (struct ifla_vlan_flags *)RTA_DATA(tb[RMNETCTL_IFLA_FLAGS]);
|
|
*flagconfig = flags->flags;
|
|
dbg_time("\tData format: 0x%x", *flagconfig);
|
|
}
|
|
|
|
if (tb[RMNETCTL_IFLA_UPLINK_PARAMS]) {
|
|
*ul_agg = *(struct rmnetctl_uplink_params *)
|
|
RTA_DATA(tb[RMNETCTL_IFLA_UPLINK_PARAMS]);
|
|
|
|
dbg_time("\tUplink Aggregation parameters:");
|
|
dbg_time("\t\tPacket limit: %u", ul_agg->packet_count);
|
|
dbg_time("\t\tByte limit: %u", ul_agg->byte_count);
|
|
dbg_time("\t\tTime limit (ns): %u", ul_agg->time_limit);
|
|
dbg_time("\t\tFeatures : 0x%x", ul_agg->features);
|
|
}
|
|
|
|
free(resp);
|
|
return RMNETCTL_API_SUCCESS;
|
|
}
|
|
|
|
int rtrmnet_ctl_get_vnd(char *vndname, int *muxid, int *qmap_version)
|
|
{
|
|
struct rmnetctl_hndl_s *handle;
|
|
uint16_t error_code;
|
|
int return_code;
|
|
uint32_t flagconfig = 0;
|
|
uint16_t mux_id = 0;
|
|
struct rmnetctl_uplink_params ul_agg;
|
|
|
|
dbg_time("%s vnd: %s", __func__, vndname);
|
|
|
|
return_code = rtrmnet_ctl_init(&handle, &error_code);
|
|
if (return_code != RMNETCTL_SUCCESS) {
|
|
dbg_time("rtrmnet_ctl_init error_code: %d, return_code: %d, errno: %d (%s)",
|
|
error_code, return_code, errno, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
// rmnetcli -n getlink rmnet_data0
|
|
return_code = rtrmnet_ctl_getvnd(handle, /*devname, */vndname, &error_code,
|
|
&mux_id, &flagconfig, &ul_agg);
|
|
if (return_code == RMNETCTL_KERNEL_ERR)
|
|
dbg_time("rtrmnet_ctl_uplinkparam RMNETCTL_KERNEL_ERR errno: %d (%s)",
|
|
error_code, strerror(error_code));
|
|
else if (return_code != RMNETCTL_SUCCESS) {
|
|
dbg_time("rtrmnet_ctl_uplinkparam error_code: %d, return_code: %d",
|
|
error_code, return_code);
|
|
}
|
|
rtrmnet_ctl_deinit(handle);
|
|
|
|
if (return_code == RMNETCTL_SUCCESS) {
|
|
*muxid = mux_id;
|
|
if (flagconfig&RMNET_FLAGS_INGRESS_DEAGGREGATION) {
|
|
if ((flagconfig&RMNET_FLAGS_INGRESS_MAP_CKSUMV5) && (flagconfig&RMNET_FLAGS_EGRESS_MAP_CKSUMV5))
|
|
*qmap_version = 9; //QMAPV5
|
|
else
|
|
*qmap_version = 5; //QMAPV1
|
|
}
|
|
}
|
|
|
|
return return_code;
|
|
}
|
|
|