610 lines
15 KiB
C
Executable file
610 lines
15 KiB
C
Executable file
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Copyright (C) 2021 Samsung Electronics.
|
|
*
|
|
* This software is licensed under the terms of the GNU General Public
|
|
* License version 2, as published by the Free Software Foundation, and
|
|
* may be copied, distributed, and modified under those terms.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
*/
|
|
|
|
#include <linux/kernel.h>
|
|
#include <linux/init.h>
|
|
#include <linux/sched.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/poll.h>
|
|
#include <linux/if_arp.h>
|
|
#include <linux/ip.h>
|
|
#include <linux/if_ether.h>
|
|
#include <linux/etherdevice.h>
|
|
#include <linux/device.h>
|
|
#include <linux/module.h>
|
|
#include <trace/events/napi.h>
|
|
#include <net/ip.h>
|
|
#include <linux/ip.h>
|
|
#include <linux/tcp.h>
|
|
#include <linux/netdevice.h>
|
|
#include <net/tcp.h>
|
|
#include <linux/uio.h>
|
|
|
|
#include <soc/samsung/exynos-modem-ctrl.h>
|
|
|
|
#include "modem_prj.h"
|
|
#include "modem_utils.h"
|
|
#include "modem_dump.h"
|
|
#if IS_ENABLED(CONFIG_MODEM_IF_LEGACY_QOS)
|
|
#include "cpif_qos_info.h"
|
|
#endif
|
|
#include "link_usnet_pktproc.h"
|
|
#include "modem_klat.h"
|
|
|
|
/* Enables when it needs to restore pktproc ptrs back before loopback. */
|
|
/* #define FEATURE_LOOPBACK_RESTORE_Q_PTR */
|
|
|
|
#define USNET_MISC_DEV "usnet"
|
|
#define US_NET_CLIENT_PORT_BASE 61000
|
|
#define US_NET_CLIENT_PORT_SIZE 1000
|
|
|
|
static struct us_net *usnet;
|
|
|
|
#define iov_for_each(iov, iter, start) \
|
|
if (!((start).type & (ITER_BVEC | ITER_PIPE))) \
|
|
for (iter = (start); \
|
|
(iter).count && \
|
|
((iov = iov_iter_iovec(&(iter))), 1); \
|
|
iov_iter_advance(&(iter), (iov).iov_len))
|
|
|
|
static ssize_t usnet_chr_write_iter(struct kiocb *iocb, struct iov_iter *from)
|
|
{
|
|
struct file *filp = iocb->ki_filp;
|
|
struct us_net_iod *usnet_iod = (struct us_net_iod *)filp->private_data;
|
|
struct us_net *usnet_obj = usnet_iod->usnet_obj;
|
|
struct us_net_stack *usnet_client = usnet_iod->usnet_client;
|
|
struct link_device *ld = (struct link_device *)(usnet_obj->mld);
|
|
|
|
struct io_device *iod;
|
|
struct net_device *ndev;
|
|
struct modem_ctl *mc;
|
|
unsigned int count = 0;
|
|
|
|
unsigned int tx_bytes;
|
|
int total_count = 0;
|
|
int ret;
|
|
|
|
struct iov_iter *base = from, iter;
|
|
struct iovec iov;
|
|
struct mif_iov *p_iov;
|
|
int i = 0;
|
|
|
|
if (!usnet_client || !ld->send_iov)
|
|
return -EPERM;
|
|
|
|
iod = usnet_client->iod;
|
|
ndev = iod->ndev;
|
|
mc = iod->mc;
|
|
|
|
iov_for_each(iov, iter, *base) {
|
|
/* this iov will be freed at
|
|
* tx_iov_frames_to_rb, sbd_iov_tx_func
|
|
* after sbd_iov_pio_tx
|
|
*/
|
|
p_iov = kmalloc(sizeof(struct mif_iov), GFP_KERNEL
|
|
);
|
|
|
|
if (!p_iov) {
|
|
/* USNET_TODO: in case of allocation failure,
|
|
* 1. drop?
|
|
* 2. retry?
|
|
*/
|
|
|
|
continue;
|
|
}
|
|
retry:
|
|
p_iov->iov = iov;
|
|
p_iov->skb = NULL;
|
|
p_iov->ch = iod->ch;
|
|
count = p_iov->iov.iov_len;
|
|
tx_bytes = count;
|
|
|
|
INIT_LIST_HEAD(&p_iov->list);
|
|
|
|
ret = ld->send_iov(ld, iod, p_iov);
|
|
|
|
if (unlikely(ret < 0)) {
|
|
static DEFINE_RATELIMIT_STATE(_rs, HZ, 100);
|
|
|
|
if (ret != -EBUSY) {
|
|
mif_err_limited("%s->%s: ERR! %s->send fail:%d (tx_bytes:%d len:%d)\n",
|
|
iod->name, mc->name, ld->name, ret,
|
|
tx_bytes, count);
|
|
goto drop;
|
|
}
|
|
|
|
/* do 100-retry for every 1sec */
|
|
if (__ratelimit(&_rs))
|
|
goto retry;
|
|
goto drop;
|
|
}
|
|
|
|
if (ret != tx_bytes) {
|
|
mif_info("%s->%s: WARN! %s->send ret:%d (tx_bytes:%d len:%d)\n",
|
|
iod->name, mc->name, ld->name, ret, tx_bytes, count);
|
|
}
|
|
|
|
ndev->stats.tx_packets++;
|
|
ndev->stats.tx_bytes += count;
|
|
|
|
total_count += count;
|
|
|
|
i++;
|
|
}
|
|
|
|
return total_count;
|
|
drop:
|
|
ndev->stats.tx_dropped++;
|
|
return total_count;
|
|
|
|
}
|
|
|
|
static bool usnet_queue_empty(struct pktproc_queue_usnet *q)
|
|
{
|
|
return circ_get_usage(q->q_info->num_desc,
|
|
q->done_ptr, q->q_info->rear_ptr) == 0;
|
|
}
|
|
|
|
static bool usnet_q_active(struct pktproc_queue_usnet *q)
|
|
{
|
|
return pktproc_check_usnet_q_active(q);
|
|
}
|
|
|
|
static __poll_t usnet_chr_poll(struct file *filp, poll_table *wait)
|
|
{
|
|
struct us_net_iod *usnet_iod = (struct us_net_iod *)filp->private_data;
|
|
struct us_net *usnet_obj;
|
|
struct us_net_stack *usnet_client;
|
|
struct modem_ctl *mc;
|
|
struct io_device *iod;
|
|
struct pktproc_queue_usnet *q;
|
|
|
|
if (!usnet_iod)
|
|
return POLLERR;
|
|
|
|
usnet_obj = usnet_iod->usnet_obj;
|
|
usnet_client = usnet_iod->usnet_client;
|
|
|
|
if (!usnet_client || !usnet_client->q)
|
|
return POLLERR;
|
|
|
|
q = usnet_client->q;
|
|
iod = usnet_client->iod;
|
|
mc = usnet_obj->mc;
|
|
|
|
if (!usnet_q_active(q))
|
|
return POLLERR;
|
|
|
|
if (usnet_queue_empty(q))
|
|
poll_wait(filp, &usnet_client->wq, wait);
|
|
|
|
switch (mc->phone_state) {
|
|
case STATE_BOOTING:
|
|
case STATE_ONLINE:
|
|
if (!usnet_queue_empty(q)) {
|
|
int rcvd = circ_get_usage(q->q_info->num_desc,
|
|
q->done_ptr, q->q_info->rear_ptr);
|
|
|
|
pktproc_usnet_update_fore_ptr(q , rcvd);
|
|
|
|
/* usnet need POLLPRI, POLLRDNORM; */
|
|
return POLLIN | POLLPRI;
|
|
}
|
|
else /* wq is waken up without rx, return for wait */
|
|
return 0;
|
|
/* fall through, if sim_state has been changed */
|
|
case STATE_CRASH_EXIT:
|
|
case STATE_CRASH_RESET:
|
|
case STATE_NV_REBUILDING:
|
|
case STATE_CRASH_WATCHDOG:
|
|
{
|
|
mif_err("%s: %s.state == %s\n", iod->name, mc->name,
|
|
mc_state(mc));
|
|
|
|
/* give delay to prevent infinite sys_poll call from
|
|
* select() in APP layer without 'sleep' user call takes
|
|
* almost 100% cpu usage when it is looked up by 'top'
|
|
* command.
|
|
*/
|
|
msleep(20);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int __usnet_get_unused_client(struct us_net *usnet_obj)
|
|
{
|
|
int i;
|
|
struct us_net_stack *usnet_client;
|
|
|
|
for (i = 0; i < US_NET_CLIENTS_MAX; i++) {
|
|
usnet_client = usnet_obj->usnet_clients[i];
|
|
|
|
if (!usnet_client)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static long usnet_chr_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
|
|
{
|
|
struct us_net_iod *usnet_iod = (struct us_net_iod *)filp->private_data;
|
|
struct us_net *usnet_obj = usnet_iod->usnet_obj;
|
|
struct us_net_stack *usnet_client = usnet_iod->usnet_client;
|
|
|
|
mif_info("cmd: 0x%x by %s(%d)\n", cmd, current->comm, current->pid);
|
|
|
|
switch (cmd) {
|
|
case IOCTL_USNET_PKTPROC_INFO:
|
|
{
|
|
struct pktproc_adaptor_info pai;
|
|
struct usnet_umem_reg mr;
|
|
struct usnet_umem *umem;
|
|
int err;
|
|
int client_idx;
|
|
|
|
if (usnet_client) {
|
|
mif_err("ERR! already created by %s(%d)\n", current->comm, current->pid);
|
|
return -EEXIST;
|
|
}
|
|
|
|
if (copy_from_user(&pai, (void __user *)arg, sizeof(pai))) {
|
|
mif_err("ERR! can't get client info\n");
|
|
return -EFAULT;
|
|
}
|
|
|
|
if (atomic_read(&usnet_obj->clients) >= US_NET_CLIENTS_MAX) {
|
|
mif_err("ERR! can't assign new usnet client\n");
|
|
return -EBUSY;
|
|
}
|
|
|
|
if (!usnet_obj->inet_pdn_iod) {
|
|
mif_err("ERR! there's no inet pdn iod\n");
|
|
return -ENODEV;
|
|
}
|
|
|
|
client_idx = __usnet_get_unused_client(usnet_obj);
|
|
|
|
if (client_idx < 0) {
|
|
mif_err("ERR! no empty client slot\n");
|
|
return -EBUSY;
|
|
}
|
|
|
|
usnet_client = (struct us_net_stack *)kzalloc(sizeof(struct us_net_stack),
|
|
GFP_KERNEL);
|
|
|
|
if (!usnet_client) {
|
|
mif_err("ERR! can't alloc client\n");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
atomic_inc(&usnet_obj->clients);
|
|
usnet_obj->usnet_clients[client_idx] = usnet_client;
|
|
|
|
/* USNET_TODO:
|
|
* temp. create umem */
|
|
mr.addr = pai.addr;
|
|
mr.len = pai.len;
|
|
mr.chunk_size = pai.chunk_size;
|
|
mr.headroom = pai.headroom;
|
|
mr.type = pai.type;
|
|
|
|
umem = usnet_umem_create(&mr);
|
|
if (IS_ERR(umem)) {
|
|
kfree(usnet_client);
|
|
atomic_dec(&usnet_obj->clients);
|
|
mif_err("ERR! can't create usnet memory(%ld)\n", PTR_ERR(umem));
|
|
return -ENOMEM;
|
|
}
|
|
|
|
usnet_client->umem = umem;
|
|
usnet_client->usnet_obj = usnet_obj;
|
|
|
|
smp_wmb();
|
|
|
|
err = pktproc_create_usnet(usnet_obj->dev, umem, &pai, usnet_client);
|
|
if (err < 0) {
|
|
kfree(usnet_client);
|
|
usnet_obj->usnet_clients[client_idx] = NULL;
|
|
atomic_dec(&usnet_obj->clients);
|
|
|
|
/* USNET_TODO: need destroy umem?? */
|
|
|
|
mif_err("ERR! pktproc_create_ul() error %d\n", err);
|
|
return -EINVAL;
|
|
}
|
|
|
|
usnet_client->iod = usnet_obj->inet_pdn_iod;
|
|
usnet_client->user_pid = current->pid;
|
|
usnet_client->us_net_index = client_idx;
|
|
usnet_client->reserved_port_range[US_NET_PORT_MIN] =
|
|
US_NET_CLIENT_PORT_BASE +
|
|
US_NET_CLIENT_PORT_SIZE * client_idx;
|
|
usnet_client->reserved_port_range[US_NET_PORT_MAX] =
|
|
usnet_client->reserved_port_range[US_NET_PORT_MIN] +
|
|
US_NET_CLIENT_PORT_SIZE - 1;
|
|
|
|
usnet_client->usnet_iod = usnet_iod;
|
|
usnet_iod->usnet_client = usnet_client;
|
|
|
|
init_waitqueue_head(&usnet_client->wq);
|
|
|
|
mif_info("pktproc usnet created (idx=%d, ch=%d) by %s(%d)\n",
|
|
client_idx, usnet_client->iod->ch,
|
|
current->comm, current->pid);
|
|
break;
|
|
}
|
|
|
|
case IOCTL_USNET_GET_PORT_RANGE:
|
|
{
|
|
struct usnet_port_range range;
|
|
|
|
if (!usnet_client) {
|
|
mif_err("no usnet_client created by %s(%d)\n",
|
|
current->comm, current->pid);
|
|
return -ENODEV;
|
|
}
|
|
|
|
range.port_min = usnet_client->reserved_port_range[US_NET_PORT_MIN];
|
|
range.port_max = usnet_client->reserved_port_range[US_NET_PORT_MAX];
|
|
|
|
mif_info("GET_PORT_RANGE(%d): value(%d, %d)\n",
|
|
usnet_client->us_net_index,
|
|
range.port_min, range.port_max);
|
|
|
|
if (copy_to_user((void __user *)arg, &range, sizeof(range)))
|
|
return -EFAULT;
|
|
break;
|
|
}
|
|
|
|
case IOCTL_USNET_GET_XLAT_INFO:
|
|
{
|
|
if (copy_to_user((void __user *)arg, &klat_obj, sizeof(struct klat))) {
|
|
mif_err("failed to get xlat_info\n");
|
|
return -EFAULT;
|
|
}
|
|
|
|
mif_info("copied xlat_info to user\n");
|
|
break;
|
|
}
|
|
|
|
case IOCTL_USNET_SET_LOOPBACK:
|
|
{
|
|
int idx = usnet_client->us_net_index;
|
|
struct us_net_loopback_data *usnet_lb = &usnet_obj->loopback[idx];
|
|
#ifdef FEATURE_LOOPBACK_RESTORE_Q_PTR
|
|
struct us_net_loopback_pktproc_pointers *pktproc_pointers =
|
|
&usnet_obj->pktproc_pointers;
|
|
#endif
|
|
struct mem_link_device *mld = usnet_obj->mld;
|
|
struct pktproc_queue *q = mld->pktproc.q[0];
|
|
|
|
if (copy_from_user(usnet_lb,
|
|
(void __user *)arg,
|
|
sizeof(struct us_net_loopback))) {
|
|
mif_err("failed to get loopback info\n");
|
|
return -EFAULT;
|
|
}
|
|
/*
|
|
* EXECUTE NORMALLY WHEN clients == 1
|
|
*/
|
|
|
|
usnet_lb->ch = usnet_client->iod->ch;;
|
|
|
|
if (usnet_lb->do_loopback)
|
|
mif_info("lkl loopback test\n");
|
|
|
|
#ifdef FEATURE_LOOPBACK_RESTORE_Q_PTR
|
|
pktproc_pointers->fore_ptr = *q->fore_ptr;
|
|
pktproc_pointers->rear_ptr = *q->rear_ptr;
|
|
pktproc_pointers->done_ptr = q->done_ptr;
|
|
#endif
|
|
|
|
q->do_loopback = usnet_lb->do_loopback;
|
|
q->usnet_lb = usnet_lb;
|
|
|
|
skb_queue_head_init(&usnet_lb->loopback_skb_q);
|
|
#ifdef CONFIG_USNET_TIMER_LOOPBACK
|
|
hrtimer_init(&usnet_lb->loopback_q_timer, CLOCK_MONOTONIC,
|
|
HRTIMER_MODE_REL);
|
|
#endif
|
|
|
|
mif_err("usnet_loopback start : num_desc:%d fore:%d done:%d rear:%d\n",
|
|
q->q_info_ptr->num_desc, q->q_info_ptr->fore_ptr,
|
|
q->done_ptr, q->q_info_ptr->rear_ptr);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
mif_info("ERR! undefined cmd 0x%X\n", cmd);
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int usnet_chr_open(struct inode *inode, struct file *file)
|
|
{
|
|
struct us_net *usnet_obj = container_of(file->private_data, struct us_net, ndev_miscdev);
|
|
struct us_net_iod *usnet_iod = NULL;
|
|
|
|
mif_info("+++\n");
|
|
|
|
usnet_iod = (struct us_net_iod *)kzalloc(sizeof(struct us_net_iod), GFP_KERNEL);
|
|
|
|
if (!usnet_iod) {
|
|
mif_err("can't alloc usnet_iod\n");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
usnet_iod->usnet_obj = usnet_obj;
|
|
file->private_data = (void *)usnet_iod;
|
|
|
|
mif_err("usnet opened by %s(%d) ---\n", current->comm, current->pid);
|
|
return 0;
|
|
}
|
|
|
|
static void usnet_close_loopback(struct us_net *usnet_obj, int client_idx)
|
|
{
|
|
struct mem_link_device *mld = usnet_obj->mld;
|
|
struct us_net_loopback_data *usnet_lb;
|
|
#ifdef FEATURE_LOOPBACK_RESTORE_Q_PTR
|
|
struct us_net_loopback_pktproc_pointers *pktproc_pointers =
|
|
&usnet_obj->pktproc_pointers;
|
|
unsigned long flags = 0;
|
|
#endif
|
|
|
|
if (!usnet_obj->loopback[client_idx].do_loopback)
|
|
return;
|
|
|
|
/* close loopback */
|
|
usnet_lb = &usnet_obj->loopback[client_idx];
|
|
|
|
usnet_obj->loopback[client_idx].do_loopback = false;
|
|
skb_queue_purge(&usnet_lb->loopback_skb_q);
|
|
#ifdef CONFIG_USNET_TIMER_LOOPBACK
|
|
hrtimer_cancel(&usnet_lb->loopback_q_timer);
|
|
#endif
|
|
|
|
#ifdef FEATURE_LOOPBACK_RESTORE_Q_PTR
|
|
spin_lock_irqsave(&mld->pktproc.q[0]->lock, flags);
|
|
*mld->pktproc.q[0]->fore_ptr = pktproc_pointers->fore_ptr;
|
|
*mld->pktproc.q[0]->rear_ptr = pktproc_pointers->rear_ptr;
|
|
mld->pktproc.q[0]->done_ptr = pktproc_pointers->done_ptr;
|
|
spin_unlock_irqrestore(&mld->pktproc.q[0]->lock, flags);
|
|
#endif
|
|
|
|
mif_info("usent_loopback after close : num_desc:%d fore:%d done:%d rear:%d\n",
|
|
mld->pktproc.q[0]->q_info_ptr->num_desc,
|
|
mld->pktproc.q[0]->q_info_ptr->fore_ptr,
|
|
mld->pktproc.q[0]->done_ptr,
|
|
mld->pktproc.q[0]->q_info_ptr->rear_ptr);
|
|
}
|
|
|
|
static int usnet_chr_close(struct inode *inode, struct file *filp)
|
|
{
|
|
struct us_net_iod *usnet_iod = (struct us_net_iod *)filp->private_data;
|
|
struct us_net_stack *usnet_client = usnet_iod->usnet_client;
|
|
struct us_net *usnet_obj = usnet_iod->usnet_obj;
|
|
int err;
|
|
int client_idx;
|
|
|
|
mif_info("+++\n");
|
|
|
|
if (usnet_client) {
|
|
client_idx = usnet_client->us_net_index;
|
|
|
|
usnet_close_loopback(usnet_obj, client_idx);
|
|
err = pktproc_destroy_usnet(usnet_client);
|
|
|
|
usnet_obj->usnet_clients[client_idx] = NULL;
|
|
atomic_dec(&usnet_obj->clients);
|
|
kfree(usnet_client);
|
|
|
|
mif_info("usnet client destoryed\n");
|
|
}
|
|
|
|
kfree(usnet_iod);
|
|
|
|
mif_err("usnet closed by %s(%d) ---\n", current->comm, current->pid);
|
|
return 0;
|
|
}
|
|
|
|
const struct file_operations usnet_io_fops = {
|
|
.owner = THIS_MODULE,
|
|
.llseek = no_llseek,
|
|
.write_iter = usnet_chr_write_iter,
|
|
.poll = usnet_chr_poll,
|
|
.unlocked_ioctl = usnet_chr_ioctl,
|
|
#ifdef CONFIG_COMPAT
|
|
/* USNET: do we need to support this? */
|
|
#endif
|
|
.open = usnet_chr_open,
|
|
.release = usnet_chr_close,
|
|
};
|
|
|
|
static int usnet_pdn_notifier(struct notifier_block *nb,
|
|
unsigned long action, void *nb_data)
|
|
{
|
|
struct us_net *usnet_obj = container_of(nb, struct us_net, inet_pdn_nb);
|
|
struct us_net_stack *usnet_client;
|
|
enum pdn_type evt = (enum pdn_type)action;
|
|
struct io_device *iod = (struct io_device *)nb_data;
|
|
int i;
|
|
|
|
if (evt != PDN_DEFAULT)
|
|
return 0;
|
|
|
|
/* this backup is needed when usnet created */
|
|
usnet_obj->inet_pdn_iod = iod;
|
|
|
|
mif_info("inet pdn notified (%s)\n", iod->name);
|
|
|
|
for (i = 0; i < US_NET_CLIENTS_MAX; i++) {
|
|
usnet_client = usnet_obj->usnet_clients[i];
|
|
|
|
if (!usnet_client)
|
|
continue;
|
|
|
|
/* USNET_TODO: need some lock here! */
|
|
|
|
usnet_client->iod = iod;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int init_usnet_io_device(struct device *dev, struct link_device *ld, struct modem_ctl *mc)
|
|
{
|
|
int ret = 0;
|
|
struct mem_link_device *mld = ld_to_mem_link_device(ld);
|
|
|
|
usnet = (struct us_net *)kzalloc(sizeof(struct us_net), GFP_KERNEL);
|
|
|
|
if (!usnet) {
|
|
mif_err("ERR! can't allocate usnet\n");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
atomic_set(&usnet->clients, 0);
|
|
usnet->mc = mc;
|
|
usnet->mld = mld;
|
|
usnet->dev = dev;
|
|
|
|
usnet->ndev_miscdev.minor = MISC_DYNAMIC_MINOR;
|
|
usnet->ndev_miscdev.name = USNET_MISC_DEV; // iod->name;
|
|
usnet->ndev_miscdev.nodename = USNET_MISC_DEV; // /dev/usnet0
|
|
usnet->ndev_miscdev.fops = &usnet_io_fops;
|
|
|
|
ret = misc_register(&usnet->ndev_miscdev);
|
|
|
|
if (ret) {
|
|
mif_info("ERR! usnet misc_register failed\n");
|
|
|
|
kfree(usnet);
|
|
usnet = NULL;
|
|
}
|
|
|
|
usnet->inet_pdn_nb.notifier_call = usnet_pdn_notifier;
|
|
register_pdn_event_notifier(&usnet->inet_pdn_nb);
|
|
|
|
ld->usnet_obj = usnet;
|
|
|
|
return ret;
|
|
}
|
|
|