Commit 30abbdf8 authored by frtabu's avatar frtabu

prepare for telnet server integration

parent 4ceb5916
......@@ -54,7 +54,7 @@ char defbool[2]="1";
} else { /* boolean value */
tmpval = defbool;
}
printf("cc 0x%08x, %i\n",tmpval,argok);
switch(cfgoptions->type)
{
case TYPE_STRING:
......
cmake_minimum_required(VERSION 2.8 FATAL_ERROR)
IF(DEFINED ENV{OPENAIR_DIR})
message("...using oai source files in $ENV{OPENAIR_DIR}")
ELSE()
message("OPENAIR_DIR is not defined. You must run \"source oaienv\" from the oai root dir")
# exit early
return()
ENDIF()
set(APPROOT . )
set(OPENAIR_DIR $ENV{OPENAIR_DIR})
set(OPENAIR_BUILD_DIR $ENV{OPENAIR_DIR}/cmake_targets)
set(OPENAIR1_DIR $ENV{OPENAIR1_DIR})
set(OPENAIR2_DIR $ENV{OPENAIR2_DIR})
set(OPENAIR_PHY_DIR $ENV{OPENAIR1_DIR}/PHY)
set(OPENAIR_TARGET_DIR $ENV{OPENAIR_DIR}/targets)
set(OPENAIR_COMMONUTILS_DIR $ENV{OPENAIR_DIR}/common/utils)
set(OPENAIR2_COMMON_DIR $ENV{OPENAIR_DIR}/openair2/COMMON)
set(OPENAIR_ASN1INC ${OPENAIR_BUILD_DIR}/lte_build_oai/build/CMakeFiles/Rel14)
set(OPENAIR_NFAPIINC $ENV{NFAPI_DIR} )
set(CMAKE_INSTALL_PREFIX $ENV{OPENAIR_TARGETS})
add_definitions (-DRel14 -DCMAKER -DENABLE_FXP -DENABLE_ITTI -DENABLE_NAS_UE_LOGGING -DENABLE_SECURITY -DENABLE_USE_CPU_EXECUTION_TIME -DENABLE_USE_MME -DENABLE_VCD -DENB_AGENT -DENB_MODE -DETHERNET=1 -DEXMIMO_IOT -DJUMBO_FRAME -DLINK_ENB_PDCP_TO_GTPV1U -DLOG_NO_THREAD -DMAC_CONTEXT -DMAX_NUM_CCs=1 -DNAS_BUILT_IN_UE -DNAS_UE -DNB_ANTENNAS_RX=2 -DNB_ANTENNAS_TX=2 -DNB_ANTENNAS_TXRX=2 -DNEW_FFT -DNO_RRM -DNone=1 -DOAI_NW_DRIVER_USE_NETLINK -DOPENAIR1 -DOPENAIR2 -DOPENAIR_LTE -DPC_DSP -DPC_TARGET -DPHYSIM -DPHY_CONTEXT -DPUCCH -DRel10=1 -DS1AP_VERSION=R10 -DTRACE_RLC_MUTEX -DUSER_MODE -DX2AP_VERSION=R11 -DXFORMS -mavx2 -msse4.1 -mssse3)
add_compile_options( -fPIC -march=native -Ofast)
include_directories( ./ ${OPENAIR_COMMON_DIR} ${OPENAIR_DIR} ${OPENAIR1_DIR} ${OPENAIR2_DIR} ${OPENAIR2_COMMON_DIR} ${OPENAIR2_DIR}/UTIL/LOG
${OPENAIR_COMMONUTILS_DIR}/msc ${OPENAIR_COMMONUTILS_DIR}/itti ${OPENAIR_ASN1INC} ${OPENAIR_TARGET_DIR}/COMMON ${OPENAIR_TARGET_DIR}/ARCH/COMMON
${OPENAIR_NFAPIINC})
set(TELNETSRV_SOURCE
${APPROOT}/telnetsrv.c
${APPROOT}/telnetsrv_phycmd.c
${APPROOT}/telnetsrv_proccmd.c
)
#set(TELNETSRV_ETHDEVCMD_SOURCE
# ${APPROOT}/telnetsrv/telnetsrv_ethdevcmd.c
# )
add_library(telnetsrv MODULE ${TELNETSRV_SOURCE} )
#add_library(telnetsrv_ethdevcmd MODULE ${TELNETSRV_ETHDEVCMD_SOURCE} )
install(TARGETS telnetsrv DESTINATION bin)
if (EXISTS "${OPENAIR_BUILD_DIR}/lte_build_oai/build" AND IS_DIRECTORY "${OPENAIR_BUILD_DIR}/lte_build_oai/build")
install(TARGETS telnetsrv DESTINATION ${OPENAIR_BUILD_DIR}/lte_build_oai/build)
endif (EXISTS "${OPENAIR_BUILD_DIR}/lte_build_oai/build" AND IS_DIRECTORY "${OPENAIR_BUILD_DIR}/lte_build_oai/build")
/* FT NOKBLF:
* this source is to be linked with the program using the telnet server, it looks for
* the telnet server dynamic library, possibly loads it and calls the telnet server
* init functions
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <dlfcn.h>
#include "telnetsrv.h"
#include "openair1/PHY/defs.h"
int load_telnet(void)
{
void *lib_handle;
initfunc_t fpi;
lib_handle = dlopen(TELNETSRV_SHAREDLIB, RTLD_LAZY|RTLD_NODELETE|RTLD_GLOBAL);
if (!lib_handle)
{
printf("[TELNETSRV] telnet server is not loaded: %s\n", dlerror());
return -1;
}
fpi = dlsym(lib_handle,"init_telnetsrv");
if (fpi != NULL )
{
fpi(cfgfile);
}
else
{
fprintf(stderr,"[TELNETSRV] %s %d Telnet server init function not found %s\n",__FILE__, __LINE__, dlerror());
return -1;
}
dlclose(lib_handle);
return 0;
}
#ifndef TELNET_LOAD_H
#define TELNET_LOAD_H
#include "telnetsrv.h"
extern int load_telnet(void);
#endif
/*
* Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The OpenAirInterface Software Alliance licenses this file to You under
* the OAI Public License, Version 1.0 (the "License"); you may not use this file
* except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.openairinterface.org/?page_id=698
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*-------------------------------------------------------------------------------
* For more information about the OpenAirInterface (OAI) Software Alliance:
* contact@openairinterface.org
*/
/*! \file common/utils/telnetsrv.c
* \brief: implementation of a telnet server
* \author Francois TABURET
* \date 2017
* \version 0.1
* \company NOKIA BellLabs France
* \email: francois.taburet@nokia-bell-labs.com
* \note
* \warning
*/
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <pthread.h>
#include <telnetsrv.h>
#include <string.h>
#include <stdarg.h>
#include <unistd.h>
#include <fcntl.h>
#include <dlfcn.h>
#include <sys/time.h>
#include <sys/resource.h>
#include "common/config/config_userapi.h"
#include "telnetsrv_phycmd.h"
#include "telnetsrv_proccmd.h"
static char* telnet_defstatmod[] = {"softmodem","phy"};
static telnetsrv_params_t telnetparams;
#define TELNETSRV_LISTENADDR 0
#define TELNETSRV_LISTENPORT 1
#define TELNETSRV_PRIORITY 2
#define TELNETSRV_DEBUG 3
#define TELNETSRV_STATICMOD 7
#define TELNETSRV_SHRMOD 8
paramdef_t telnetoptions[] = {
/*--------------------------------------------------------------------------------------------------------------------------------------------------------------------*/
/* configuration parameters for telnet utility */
/* optname helpstr paramflags XXXptr defXXXval type numelt */
/*--------------------------------------------------------------------------------------------------------------------------------------------------------------------*/
{"listenaddr", "<listen ip address>", 0, uptr:&telnetparams.listenaddr, defstrval:"0.0.0.0", TYPE_IPV4ADDR, 0 },
{"listenport", "<local port>", 0, uptr:&(telnetparams.listenport), defuintval:9090, TYPE_UINT, 0 },
{"priority", "<scheduling policy (0-99)", 0, uptr:&telnetparams.priority, defuintval:0, TYPE_INT, 0 },
{"debug", "<debug level>", 0, uptr:NULL, defuintval:0, TYPE_UINT, 0 },
{"loopcount", "<loop command iterations>", 0, uptr:&(telnetparams.loopcount), defuintval:10, TYPE_UINT, 0 },
{"loopdelay", "<loop command delay (ms)>", 0, uptr:&(telnetparams.loopdelay), defuintval:5000, TYPE_UINT, 0 },
{"phypbsize", "<phy dump buff size (bytes)>",0, uptr:&(telnetparams.phyprntbuff_size),defuintval:65000, TYPE_UINT, 0 },
{"staticmod", "<static modules selection>", 0, NULL, defstrlistval:telnet_defstatmod,TYPE_STRINGLIST,1},
{"shrmod", "<static modules selection>", 0, NULL, NULL,TYPE_STRINGLIST,0 },
};
int get_phybsize() {return telnetparams.phyprntbuff_size; };
int add_telnetcmd(char *modulename,telnetshell_vardef_t *var, telnetshell_cmddef_t *cmd );
int setoutput(char *buff, int debug, telnet_printfunc_t prnt);
int setparam(char *buff, int debug, telnet_printfunc_t prnt);
telnetshell_vardef_t telnet_vardef[] = {
{"debug",TELNET_VARTYPE_INT32,&telnetparams.telnetdbg},
{"prio",TELNET_VARTYPE_INT32,&telnetparams.priority},
{"loopc",TELNET_VARTYPE_INT32,&telnetparams.loopcount},
{"loopd",TELNET_VARTYPE_INT32,&telnetparams.loopdelay},
{"phypb",TELNET_VARTYPE_INT32,&telnetparams.phyprntbuff_size},
{"",0,NULL}
};
telnetshell_cmddef_t telnet_cmdarray[] = {
{"redirlog","[here,file,off]",setoutput},
{"param","[prio]",setparam},
{"","",NULL},
};
void client_printf(const char *message, ...)
{
va_list va_args;
va_start(va_args, message);
if (telnetparams.new_socket > 0)
{
vsnprintf(telnetparams.msgbuff,sizeof(telnetparams.msgbuff)-1,message, va_args);
send(telnetparams.new_socket,telnetparams.msgbuff , strlen(telnetparams.msgbuff), MSG_NOSIGNAL);
}
else
{
vprintf(message, va_args);
}
va_end(va_args);
return ;
}
#define NICE_MAX 19
#define NICE_MIN -20
void set_sched(pthread_t tid, int pid, int priority)
{
int rt;
struct sched_param schedp;
int policy;
char strpolicy[10];
//sched_get_priority_max(SCHED_FIFO)
if (priority < NICE_MIN)
{
policy=SCHED_FIFO;
sprintf(strpolicy,"%s","fifo");
schedp.sched_priority= NICE_MIN - priority ;
}
else if (priority > NICE_MAX)
{
policy=SCHED_IDLE;
sprintf(strpolicy,"%s","idle");
schedp.sched_priority=0;
}
else
{
policy=SCHED_OTHER;
sprintf(strpolicy,"%s","other");
schedp.sched_priority=0;
}
if( tid != 0)
{
rt = pthread_setschedparam(tid, policy, &schedp);
}
else if(pid > 0)
{
rt = sched_setscheduler( pid, policy,&schedp);
}
if (rt != 0)
{
client_printf("Error %i: %s modifying sched param to %s:%i, \n",
errno,strerror(errno),strpolicy,schedp.sched_priority);
}
else
{
client_printf("policy set to %s, priority %i\n",strpolicy,schedp.sched_priority);
}
if ( policy == SCHED_OTHER)
{
if ( tid > 0 && tid != pthread_self())
{
client_printf("setting nice value using a thread id not implemented....\n");
}
else if (pid > 0)
{
errno=0;
rt = setpriority(PRIO_PROCESS,pid,priority);
if (rt != 0)
{
client_printf("Error %i: %s calling setpriority, \n",errno,strerror(errno));
}
else
{
client_printf("nice value set to %i\n",priority);
}
}
}
}
void set_affinity(pthread_t tid, int pid, int coreid)
{
cpu_set_t cpuset;
int rt;
CPU_ZERO(&cpuset);
CPU_SET(coreid, &cpuset);
if (tid > 0)
{
rt = pthread_setaffinity_np((pthread_t)tid, sizeof(cpu_set_t), &cpuset);
}
else if (pid > 0)
{
rt = sched_setaffinity((pid_t)pid, sizeof(cpu_set_t), &cpuset);
}
if (rt != 0)
{
client_printf("Error %i: %s calling , xxx_setaffinity...\n",errno,strerror(errno));
}
else
{
client_printf("thread %i affinity set to %i\n",(pid==0)?(int)tid:pid,coreid);
}
}
/*------------------------------------------------------------------------------------*/
/*
function implementing telnet server specific commands, parameters of the
telnet_cmdarray table
*/
void redirstd(char *newfname,telnet_printfunc_t prnt )
{
FILE *fd;
fd=freopen(newfname, "w", stdout);
if (fd == NULL)
{
prnt("ERROR: stdout redir to %s error %s",strerror(errno));
}
fd=freopen(newfname, "w", stderr);
if (fd == NULL)
{
prnt("ERROR: stderr redir to %s error %s",strerror(errno));
}
}
int setoutput(char *buff, int debug, telnet_printfunc_t prnt)
{
char cmds[TELNET_MAX_MSGLENGTH/TELNET_CMD_MAXSIZE][TELNET_CMD_MAXSIZE];
char *logfname;
char stdout_str[64];
#define LOGFILE "logfile.log"
memset(cmds,0,sizeof(cmds));
sscanf(buff,"%9s %32s %9s %9s %9s", cmds[0],cmds[1],cmds[2],cmds[3],cmds[4] );
if (strncasecmp(cmds[0],"here",4) == 0)
{
fflush(stdout);
sprintf(stdout_str,"/proc/%i/fd/%i",getpid(),telnetparams.new_socket);
dup2(telnetparams.new_socket,fileno(stdout));
// freopen(stdout_str, "w", stdout);
// freopen(stdout_str, "w", stderr);
dup2(telnetparams.new_socket,fileno(stderr));
prnt("Log output redirected to this terminal (%s)\n",stdout_str);
}
if (strncasecmp(cmds[0],"file",4) == 0)
{
if (cmds[1][0] == 0)
logfname=LOGFILE;
else
logfname=cmds[1];
fflush(stdout);
redirstd(logfname,prnt);
}
if (strncasecmp(cmds[0],"off",3) == 0)
{
fflush(stdout);
redirstd("/dev/tty",prnt);
}
return CMDSTATUS_FOUND;
} /* setoutput */
int setparam(char *buff, int debug, telnet_printfunc_t prnt)
{
char cmds[TELNET_MAX_MSGLENGTH/TELNET_CMD_MAXSIZE][TELNET_CMD_MAXSIZE];
memset(cmds,0,sizeof(cmds));
sscanf(buff,"%9s %9s %9s %9s %9s", cmds[0],cmds[1],cmds[2],cmds[3],cmds[4] );
if (strncasecmp(cmds[0],"prio",4) == 0)
{
pthread_attr_t attr;
int prio;
prio=(int)strtol(cmds[1],NULL,0);
if (errno == ERANGE)
return CMDSTATUS_VARNOTFOUND;
telnetparams.priority = prio;
set_sched(pthread_self(),0,prio);
return CMDSTATUS_FOUND;
}
if (strncasecmp(cmds[0],"aff",3) == 0)
{
int aff;
aff=(int)strtol(cmds[1],NULL,0);
if (errno == ERANGE)
return CMDSTATUS_VARNOTFOUND;
set_affinity(pthread_self(),0,aff);
return CMDSTATUS_FOUND;
}
return CMDSTATUS_NOTFOUND;
} /* setparam */
/*-------------------------------------------------------------------------------------------------------*/
/*
generic commands available for all modules loaded by the server
*/
int setgetvar(int moduleindex,char getorset,char *params)
{
int n,i;
char varname[TELNET_CMD_MAXSIZE];
char varval[TELNET_CMD_MAXSIZE];
memset(varname,0,sizeof(varname));
memset(varval,0,sizeof(varval));
n = sscanf(params,"%s %s",varname,varval);
for ( i=0 ; telnetparams.CmdParsers[moduleindex].var[i].varvalptr != NULL ; i++)
{
if ( strncasecmp(telnetparams.CmdParsers[moduleindex].var[i].varname,varname,strlen(telnetparams.CmdParsers[moduleindex].var[i].varname)) == 0)
{
if (n > 0 && (getorset == 'g' || getorset == 'G'))
{
client_printf("%s, %s = ", telnetparams.CmdParsers[moduleindex].module,
telnetparams.CmdParsers[moduleindex].var[i].varname );
switch(telnetparams.CmdParsers[moduleindex].var[i].vartype)
{
case TELNET_VARTYPE_INT32:
client_printf("%i\n",*(int *)(telnetparams.CmdParsers[moduleindex].var[i].varvalptr));
break;
case TELNET_VARTYPE_INT16:
client_printf("%hi\n",*(short *)(telnetparams.CmdParsers[moduleindex].var[i].varvalptr));
break;
case TELNET_VARTYPE_DOUBLE:
client_printf("%g\n",*(double *)(telnetparams.CmdParsers[moduleindex].var[i].varvalptr));
break;
case TELNET_VARTYPE_PTR:
client_printf("0x%08x\n",*((unsigned int *)(telnetparams.CmdParsers[moduleindex].var[i].varvalptr)));
break;
default:
client_printf("unknown type\n");
break;
}
}
if (n > 1 && (getorset == 's' || getorset == 'S'))
{
client_printf("%s, %s set to \n", telnetparams.CmdParsers[moduleindex].module,
telnetparams.CmdParsers[moduleindex].var[i].varname);
switch(telnetparams.CmdParsers[moduleindex].var[i].vartype)
{
case TELNET_VARTYPE_INT32:
*(int *)(telnetparams.CmdParsers[moduleindex].var[i].varvalptr) = (int)strtol(varval,NULL,0);
client_printf("%i\n",*(int *)(telnetparams.CmdParsers[moduleindex].var[i].varvalptr));
break;
case TELNET_VARTYPE_INT16:
*(short *)(telnetparams.CmdParsers[moduleindex].var[i].varvalptr) = (short)strtol(varval,NULL,0);
client_printf("%hi\n",*(short *)(telnetparams.CmdParsers[moduleindex].var[i].varvalptr));
break;
case TELNET_VARTYPE_DOUBLE:
*(double *)(telnetparams.CmdParsers[moduleindex].var[i].varvalptr) = strtod(varval,NULL);
client_printf("%g\n",*(double *)(telnetparams.CmdParsers[moduleindex].var[i].varvalptr));
break;
default:
client_printf("unknown type\n");
break;
}
}
}
}
return CMDSTATUS_VARNOTFOUND;
}
/*----------------------------------------------------------------------------------------------------*/
char *get_time(char *buff,int bufflen)
{
struct tm tmstruct;
time_t now = time (0);
strftime (buff, bufflen, "%Y-%m-%d %H:%M:%S.000", localtime_r(&now,&tmstruct));
return buff;
}
int process_command(char *buf)
{
int i,j,k;
char modulename[TELNET_CMD_MAXSIZE];
char cmd[TELNET_CMD_MAXSIZE];
char cmdb[TELNET_MAX_MSGLENGTH];
char *bufbck;
int rt;
memset(modulename,0,sizeof(modulename));
memset(cmd,0,sizeof(cmd));
memset(cmdb,0,sizeof(cmdb));
if (strncasecmp(buf,"ex",2) == 0)
return CMDSTATUS_EXIT;
if (strncasecmp(buf,"help",4) == 0)
{
for (i=0; telnetparams.CmdParsers[i].var != NULL && telnetparams.CmdParsers[i].cmd != NULL; i++)
{
client_printf(" module %i = %s:\n",i,telnetparams.CmdParsers[i].module);
for(j=0; telnetparams.CmdParsers[i].var[j].varvalptr != NULL ; j++)
{
client_printf(" %s [get set] %s <value>\n",
telnetparams.CmdParsers[i].module, telnetparams.CmdParsers[i].var[j].varname);
}
for(j=0; telnetparams.CmdParsers[i].cmd[j].cmdfunc != NULL ; j++)
{
client_printf(" %s %s %s\n",
telnetparams.CmdParsers[i].module,telnetparams.CmdParsers[i].cmd[j].cmdname,
telnetparams.CmdParsers[i].cmd[j].helpstr);
}
}
return CMDSTATUS_FOUND;
}
memset(modulename,0,sizeof(modulename));
memset(cmd,0,sizeof(cmd));
memset(cmdb,0,sizeof(cmdb));
bufbck=strdup(buf);
rt=CMDSTATUS_NOTFOUND;
j = sscanf(buf,"%9s %9s %[^\t\n]",modulename,cmd,cmdb);
if (telnetparams.telnetdbg > 0)
printf("process_command: %i words, module=%s cmd=%s, parameters= %s\n",j,modulename,cmd,cmdb);
for (i=0; j>=2 && telnetparams.CmdParsers[i].var != NULL && telnetparams.CmdParsers[i].cmd != NULL; i++)
{
if ( (strncasecmp(telnetparams.CmdParsers[i].module,modulename,strlen(telnetparams.CmdParsers[i].module)) == 0))
{
if (strncasecmp(cmd,"getall",7) == 0 )
{
for(j=0; telnetparams.CmdParsers[i].var[j].varvalptr != NULL ; j++)
{
setgetvar(i,'g',telnetparams.CmdParsers[i].var[j].varname);
}
rt= CMDSTATUS_FOUND;
}
else if (strncasecmp(cmd,"get",3) == 0 || strncasecmp(cmd,"set",3) == 0)
{
rt= setgetvar(i,cmd[0],cmdb);
}
else
{
for (k=0 ; telnetparams.CmdParsers[i].cmd[k].cmdfunc != NULL ; k++)
{
if (strncasecmp(cmd, telnetparams.CmdParsers[i].cmd[k].cmdname,sizeof(telnetparams.CmdParsers[i].cmd[k].cmdname)) == 0)
{
telnetparams.CmdParsers[i].cmd[k].cmdfunc(cmdb, telnetparams.telnetdbg, client_printf);
rt= CMDSTATUS_FOUND;
}
} /* for k */
}/* else */
}/* strncmp: module name test */
else if (strncasecmp(modulename,"loop",4) == 0 )
{
int lc;
int f = fcntl(telnetparams.new_socket,F_GETFL);
fcntl (telnetparams.new_socket, F_SETFL, O_NONBLOCK | f);
for(lc=0; lc<telnetparams.loopcount; lc++)
{
char dummybuff[20];
char tbuff[64];
int rs;
client_printf(CSI "1J" CSI "1;10H " STDFMT "%s %i/%i\n",
get_time(tbuff,sizeof(tbuff)),lc,telnetparams.loopcount );
process_command(bufbck+strlen("loop")+1);
usleep(telnetparams.loopdelay * 1000);
rs = read(telnetparams.new_socket,dummybuff,sizeof(dummybuff));
if ( rs > 0 )
{
break;
}
}
fcntl (telnetparams.new_socket, F_SETFL, f);
rt= CMDSTATUS_FOUND;
} /* loop */
} /* for i */
free(bufbck);
return rt;
}
void run_telnetsrv(void)
{
int sock;
struct sockaddr_in name;
char buf[TELNET_MAX_MSGLENGTH];
struct sockaddr cli_addr;
unsigned int cli_len = sizeof(cli_addr);
int readc , filled;
int status;
int optval = 1;
pthread_setname_np(pthread_self(), "telnet");
set_sched(pthread_self(),0,telnetparams.priority);
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
fprintf(stderr,"[TELNETSRV] Error %s on socket call\n",strerror(errno));
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof optval);
name.sin_family = AF_INET;
if (telnetparams.listenaddr == 0)
name.sin_addr.s_addr = INADDR_ANY;
else
name.sin_addr.s_addr = telnetparams.listenaddr;
name.sin_port = htons((unsigned short)(telnetparams.listenport));
if(bind(sock, (void*) &name, sizeof(name)))
fprintf(stderr,"[TELNETSRV] Error %s on bind call\n",strerror(errno));
if(listen(sock, 1) == -1)
fprintf(stderr,"[TELNETSRV] Error %s on listen call\n",strerror(errno));
printf("\nInitializing telnet server...\n");
while( (telnetparams.new_socket = accept(sock, &cli_addr, &cli_len)) )
{
printf("[TELNETSRV] Telnet client connected....\n");
if(telnetparams.new_socket < 0)
fprintf(stderr,"[TELNETSRV] Error %s on accept call\n",strerror(errno));
while(telnetparams.new_socket>0)
{
filled = 0;
memset(buf,0,sizeof(buf));
while(filled < ( TELNET_MAX_MSGLENGTH-1))
{
readc = recv(telnetparams.new_socket, buf+filled, TELNET_MAX_MSGLENGTH-filled-1, 0);
if(!readc)
break;
filled += readc;
if(buf[filled-1] == '\n')
{
buf[filled-1] = 0;
break;
}
}
if(!readc)
{
printf ("[TELNETSRV] Telnet Client disconnected.\n");
break;
}
if (telnetparams.telnetdbg > 0)
printf("[TELNETSRV] Command received: readc %i filled %i %s\n", readc, filled ,buf);
if (strlen(buf) >= 2 )
{
status=process_command(buf);
}
else
status=CMDSTATUS_NOCMD;
if (status != CMDSTATUS_EXIT)
{
if (status == CMDSTATUS_NOTFOUND)
{
char msg[TELNET_MAX_MSGLENGTH + 50];
sprintf(msg,"Error: \n %s\n is not a softmodem command\n",buf);
send(telnetparams.new_socket, msg, strlen(msg), MSG_NOSIGNAL);
}
send(telnetparams.new_socket, TELNET_PROMPT, sizeof(TELNET_PROMPT), MSG_NOSIGNAL);
}
else
{
printf ("[TELNETSRV] Closing telnet connection...\n");
break;
}
}
close(telnetparams.new_socket);
printf ("[TELNETSRV] Telnet server waitting for connection...\n");
}
close(sock);
return;
}
/*------------------------------------------------------------------------------------------------*/
/* set_telnetmodule loads the commands delivered with the telnet server
*
*
*
*/
void exec_moduleinit(char *modname)
{
void (*fptr)();
char initfunc[TELNET_CMD_MAXSIZE+9];
if (strlen(modname) > TELNET_CMD_MAXSIZE)
{
fprintf(stderr,"[TELNETSRV] module %s not loaded, name exceeds the %i size limit\n",
modname, TELNET_CMD_MAXSIZE);
return;
}
sprintf(initfunc,"add_%s_cmds",modname);
fptr = dlsym(RTLD_DEFAULT,initfunc);
if ( fptr != NULL)
{
fptr();
}
else
{
fprintf(stderr,"[TELNETSRV] couldn't find %s for module %s \n",initfunc,modname);
}
}
int add_embeddedmodules()
{
for(int i=0; i<telnetoptions[TELNETSRV_STATICMOD].numelt;i++)
{
exec_moduleinit(telnetoptions[TELNETSRV_STATICMOD].strlistptr[i]);
}
}
int add_sharedmodules()
{
char initfunc[TELNET_CMD_MAXSIZE+9];
void (*fptr)();
for(int i=0; i<telnetoptions[TELNETSRV_SHRMOD].numelt;i++)
{
sprintf(initfunc,"add_%s_cmds",telnetoptions[TELNETSRV_SHRMOD].strlistptr[i]);
fptr = dlsym(RTLD_DEFAULT,initfunc);
if ( fptr != NULL)
{
fptr();
}
else
{
fprintf(stderr,"[TELNETSRV] couldn't find %s for module %s \n",initfunc,telnetoptions[TELNETSRV_STATICMOD].strlistptr[i]);
}
}
}
int init_telnetsrv(char *cfgfile)
{
void *lib_handle;
char** moduleslist;
memset(&telnetparams,0,sizeof(telnetparams));
config_get( telnetoptions,sizeof(telnetoptions)/sizeof(paramdef_t),NULL);
if(pthread_create(&telnetparams.telnet_pthread,NULL, (void *(*)(void *))run_telnetsrv, NULL) != 0)
{
fprintf(stderr,"[TELNETSRV] Error %s on pthread_create call\n",strerror(errno));
return -1;
}
add_telnetcmd("telnet", telnet_vardef, telnet_cmdarray);
add_embeddedmodules();
return 0;
}
/*---------------------------------------------------------------------------------------------*/
/* add_telnetcmd is used to add a set of commands to the telnet server. A module calls this
* function at init time. the telnet server is delivered with a set of commands which
* will be loaded or not depending on the telnet section of the config file
*/
int add_telnetcmd(char *modulename, telnetshell_vardef_t *var, telnetshell_cmddef_t *cmd)
{
int i;
if( modulename == NULL || var == NULL || cmd == NULL)
{
fprintf(stderr,"[TELNETSRV] Telnet server, add_telnetcmd: invalid parameters\n");
return -1;
}
for (i=0; i<TELNET_MAXCMD ; i++)
{
if (telnetparams.CmdParsers[i].var == NULL)
{
strncpy(telnetparams.CmdParsers[i].module,modulename,sizeof(telnetparams.CmdParsers[i].module)-1);
telnetparams.CmdParsers[i].cmd = cmd;
telnetparams.CmdParsers[i].var = var;
printf("[TELNETSRV] Telnet server: module %i = %s added to shell\n",
i,telnetparams.CmdParsers[i].module);
break;
}
}
return 0;
}
#ifndef TELNETSRV_H
#define TELNETSRV_H
#define TELNETSRV_SHAREDLIB "libtelnetsrv.so"
#define TELNET_PORT 9090
#define TELNET_MAX_MSGLENGTH 2048
#define TELNET_PROMPT "softmodem> "
#define TELNET_MAXCMD 20
#define TELNET_CMD_MAXSIZE 10
#define TELNET_HELPSTR_SIZE 80
/* status return by the command parser after it analysed user input */
#define CMDSTATUS_NOCMD 0
#define CMDSTATUS_EXIT 1
#define CMDSTATUS_FOUND 2
#define CMDSTATUS_VARNOTFOUND 3
#define CMDSTATUS_NOTFOUND 4
/*----------------------------------------------------------------------------*/
/* structure to be used when adding a module to the telnet server */
/* This is the second parameter of the add_telnetcmd function, which can be used */
/* to add a set of new command to the telnet server shell */
typedef void(*telnet_printfunc_t)(const char* format, ...);
typedef int(*cmdfunc_t)(char*, int, telnet_printfunc_t prnt);
typedef struct cmddef {
char cmdname[TELNET_CMD_MAXSIZE];
char helpstr[TELNET_HELPSTR_SIZE];
cmdfunc_t cmdfunc;
} telnetshell_cmddef_t;
/*----------------------------------------------------------------------------*/
/*structure to be used when adding a module to the telnet server */
/* This is the first parameter of the add_telnetcmd function, which can be used */
/* to add a set of new variables which can be got/set from the telnet server shell */
#define TELNET_VARTYPE_INT32 1
#define TELNET_VARTYPE_INT16 2
#define TELNET_VARTYPE_INT64 3
#define TELNET_VARTYPE_STRING 4
#define TELNET_VARTYPE_DOUBLE 5
#define TELNET_VARTYPE_PTR 6
typedef struct variabledef {
char varname[TELNET_CMD_MAXSIZE];
char vartype;
void *varvalptr;
} telnetshell_vardef_t;
/*----------------------------------------------------------------------------*/
/* internal structure definitions */
/* cmdparser_t is used to store all modules which have been added to the telnet server. */
/* Each time the add_telnetcmd function is used, the internal array cmdparser_t[] of the */
/* telnet server is populated with the new commands and variables */
typedef struct cmdparser {
char module[TELNET_CMD_MAXSIZE]; // module name = first token of the telnet shell command
telnetshell_cmddef_t *cmd; // array of functions added to the shell
telnetshell_vardef_t *var; // array of variables added to the shell
} cmdparser_t;
/* telnetsrv_params_t is an internal structure storing all the current parameters and */
/* global variables used by the telnet server */
typedef struct {
pthread_t telnet_pthread; // thread id of the telnet server
int telnetdbg; // debug level of the server
int priority; // server running priority
int new_socket; // socket of the client connection
int logfilefd; // file id of the log file when log output is redirected to a file
int saved_stdout; // file id of the previous stdout, used to be able to restore original stdout
cmdparser_t CmdParsers[TELNET_MAXCMD]; // array of registered modules.
char msgbuff[TELNET_MAX_MSGLENGTH]; // internal buffer of the client_printf function which is used to print to the client terminal */
unsigned int listenport; // ip port the telnet server is listening on
unsigned int listenaddr; // ip address the telnet server is listening on
unsigned int loopcount; // loop command param: number of loop iteration
unsigned int loopdelay; // loop command param: delay in ms between 2 iterations
unsigned int phyprntbuff_size; // for phy module, dump_eNB_stats function buffer size
} telnetsrv_params_t;
typedef int(*addcmdfunc_t)(char*, telnetshell_vardef_t*, telnetshell_cmddef_t*);
typedef int(*initfunc_t)(char *cfgfile);
typedef void(*settelnetmodule_t)(char *name, void *ptr);
/*-------------------------------------------------------------------------------------------*/
/*
VT escape sequence definition, for smarter display....
*/
#define ESC "\x1b"
#define CSI "\x1b["
#define BOLD "\x1b[1m"
#define RED "\x1b[31m"
#define GREEN "\x1b[32m"
#define BLUE "\x1b[34m"
#define MAGENTA "\x1b[35m"
#define CYAN "\x1b[36m"
#define STDFMT "\x1b[0m"
/*---------------------------------------------------------------------------------------------*/
#ifdef TELNETSERVERCODE
int add_telnetcmd(char *modulename, telnetshell_vardef_t *var, telnetshell_cmddef_t *cmd);
void set_sched(pthread_t tid, int pid,int priority);
void set_affinity(pthread_t tid, int pid, int coreid);
extern int get_phybsize();
#endif
#endif
#define _GNU_SOURCE
#include <string.h>
#include <pthread.h>
#define TELNETSERVERCODE
#include "telnetsrv.h"
#define TELNETSRV_PHYCMD_MAIN
#include "telnetsrv_phycmd.h"
char *prnbuff;
extern int dump_eNB_stats(PHY_VARS_eNB *eNB, char* buffer, int length);
void init_phytelnet()
{
if (PHY_vars_eNB_g != NULL)
printf("init_phytelnet: phy var at 0x%08lx\n",(unsigned long int)PHY_vars_eNB_g);
else
fprintf(stderr,"init_phytelnet: phy var not found...\n");
phy_vardef[TELNETVAR_PHYCC0].varvalptr = &(PHY_vars_eNB_g[0][0]);
phy_vardef[TELNETVAR_PHYCC1].varvalptr = &(PHY_vars_eNB_g[0][1]);
prnbuff=malloc(get_phybsize() );
if (prnbuff == NULL)
{
fprintf(stderr,"Error %s on malloc in init_phytelnet()\n",strerror(errno));
}
}
void display_uestatshead( telnet_printfunc_t prnt)
{
prnt("cc ue rnti Dmcs Umcs tao tau Dbr Dtb \n");
}
void dump_uestats(int debug, telnet_printfunc_t prnt, uint8_t prntflag)
{
int p;
p=dump_eNB_stats(PHY_vars_eNB_g[0][0], prnbuff, 0);
if(prntflag>=1)
prnt("%s\n",prnbuff);
if(debug>=1)
prnt("%i bytes printed\n",p);
}
void display_uestats(int debug, telnet_printfunc_t prnt, int ue)
{
for (int cc=0; cc<1 ; cc++)
{
if ((PHY_vars_eNB_g[0][cc]->dlsch[ue][0]->rnti>0)&&
(PHY_vars_eNB_g[0][cc]->UE_stats[ue].mode == PUSCH))
{
prnt("%02i %04i %04hx %04i %04i %04i %-04i %04i %06i\n",cc, ue,
PHY_vars_eNB_g[0][cc]->UE_stats[ue].crnti,
PHY_vars_eNB_g[0][cc]->dlsch[ue][0]->harq_processes[0]->mcs,0,
// PHY_vars_eNB_g[0][cc]->ulsch[ue]->harq_processes[0]->mcs,
PHY_vars_eNB_g[0][cc]->UE_stats[ue].UE_timing_offset,
PHY_vars_eNB_g[0][cc]->UE_stats[ue].timing_advance_update,
PHY_vars_eNB_g[0][cc]->UE_stats[ue].dlsch_bitrate/1000,
PHY_vars_eNB_g[0][cc]->UE_stats[ue].total_TBS/1000
);
}
}
}
void display_phycounters(char *buf, int debug, telnet_printfunc_t prnt)
{
prnt(" DLSCH kb DLSCH kb/s\n");
dump_uestats(debug, prnt,0);
prnt(" %09i %06i\n",
PHY_vars_eNB_g[0][0]->total_transmitted_bits/1000,
PHY_vars_eNB_g[0][0]->total_dlsch_bitrate/1000);
}
int dump_phyvars(char *buf, int debug, telnet_printfunc_t prnt)
{
if (debug > 0)
prnt("phy interface module received %s\n",buf);
if (strcasestr(buf,"phycnt") != NULL)
{
display_phycounters(buf, debug, prnt);
}
if (strcasestr(buf,"uestat") != NULL)
{
char *cptr=strcasestr(buf+sizeof("uestat"),"UE");
display_uestatshead(prnt);
if (cptr != NULL)
{
int ueidx = strtol( cptr+sizeof("UE"), NULL, 10);
if (ueidx < NUMBER_OF_UE_MAX && ueidx >= 0)
{
display_uestats(debug, prnt,ueidx);
}
} /* if cptr != NULL */
else
{
for (int ue=0; ue<NUMBER_OF_UE_MAX ; ue++)
{
display_uestats(debug, prnt,ue);
}
} /* else cptr != NULL */
} /* uestat */
if (strcasestr(buf,"uedump") != NULL)
{
dump_uestats(debug, prnt,1);
}
return 0;
}
telnetshell_cmddef_t phy_cmdarray[] = {
{"disp","[phycnt,uedump,uestat UE<x>]", dump_phyvars},
{"","",NULL},
};
/*-------------------------------------------------------------------------------------*/
void add_phy_cmds()
{
init_phytelnet();
add_telnetcmd("phy", phy_vardef, phy_cmdarray);
}
#ifdef TELNETSRV_PHYCMD_MAIN
#include "UTIL/LOG/log.h"
#include "openair1/PHY/defs.h"
#define TELNETVAR_PHYCC0 0
#define TELNETVAR_PHYCC1 1
telnetshell_vardef_t phy_vardef[] = {
{"phycc1",TELNET_VARTYPE_PTR,NULL},
{"phycc2",TELNET_VARTYPE_PTR,NULL},
//{"iqmax",TELNET_VARTYPE_INT16,NULL},
//{"iqmin",TELNET_VARTYPE_INT16,NULL},
//{"loglvl",TELNET_VARTYPE_INT32,NULL},
//{"sndslp",TELNET_VARTYPE_INT32,NULL},
//{"rxrescale",TELNET_VARTYPE_INT32,NULL},
//{"txshift",TELNET_VARTYPE_INT32,NULL},
//{"rachemin",TELNET_VARTYPE_INT32,NULL},
//{"rachdmax",TELNET_VARTYPE_INT32,NULL},
{"",0,NULL}
};
extern PHY_VARS_eNB ***PHY_vars_eNB_g;
#else
extern void add_phy_cmds();
#endif
/*-------------------------------------------------------------------------------------*/
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <pthread.h>
#include <string.h>
#include <stdarg.h>
#include <dirent.h>
#define READCFG_DYNLOAD
#define TELNETSERVERCODE
#include "telnetsrv.h"
#define TELNETSRV_PROCCMD_MAIN
#include "log.h"
#include "telnetsrv_proccmd.h"
void decode_procstat(char *record, int debug, telnet_printfunc_t prnt)
{
char prntline[160];
char *procfile_fiels;
char *strtokptr;
char *lptr;
int fieldcnt;
char toksep[2];
fieldcnt=0;
procfile_fiels =strtok_r(record," ",&strtokptr);
lptr= prntline;
/*http://man7.org/linux/man-pages/man5/proc.5.html gives the structure of the stat file */
while( procfile_fiels != NULL && fieldcnt < 42)
{
if (strlen(procfile_fiels) == 0)
continue;
fieldcnt++;
sprintf(toksep," ");
switch(fieldcnt)
{
case 1: /* id */
lptr+=sprintf(lptr,"%9.9s ",procfile_fiels);
sprintf(toksep,")");
break;
case 2: /* name */
lptr+=sprintf(lptr,"%20.20s ",procfile_fiels+1);
break;
case 3: //thread state
lptr+=sprintf(lptr," %c ",procfile_fiels[0]);
break;
case 14: //time in user mode
case 15: //time in kernel mode
lptr+=sprintf(lptr,"%9.9s ",procfile_fiels);
break;
case 18: //priority
case 19: //nice
lptr+=sprintf(lptr,"%3.3s ",procfile_fiels);
break;
case 23: //vsize
lptr+=sprintf(lptr,"%9.9s ",procfile_fiels);
break;
case 39: //processor
lptr+=sprintf(lptr," %2.2s ",procfile_fiels);
break;
case 41: //policy
lptr+=sprintf(lptr,"%3.3s ",procfile_fiels);
break;
default:
break;
}/* switch on fieldcnr */
procfile_fiels =strtok_r(NULL,toksep,&strtokptr);
} /* while on proc_fields != NULL */
prnt("%s\n",prntline);
} /*decode_procstat */
void read_statfile(char *fname,int debug, telnet_printfunc_t prnt)
{
FILE *procfile;
char arecord[1024];
procfile=fopen(fname,"r");
if (procfile == NULL)
{
prnt("Error: Couldn't open %s %i %s\n",fname,errno,strerror(errno));
return;
}
if ( fgets(arecord,sizeof(arecord),procfile) == NULL)
{
prnt("Error: Nothing read from %s %i %s\n",fname,errno,strerror(errno));
fclose(procfile);
return;
}
fclose(procfile);
decode_procstat(arecord, debug, prnt);
}
void print_threads(char *buf, int debug, telnet_printfunc_t prnt)
{
char aname[256];
DIR *proc_dir;
struct dirent *entry;
int rt;
prnt(" id name state USRmod KRNmod prio nice vsize proc pol \n\n");
snprintf(aname, sizeof(aname), "/proc/%d/stat", getpid());
read_statfile(aname,debug,prnt);
prnt("\n");
snprintf(aname, sizeof(aname), "/proc/%d/task", getpid());
proc_dir = opendir(aname);
if (proc_dir == NULL)
{
prnt("Error: Couldn't open %s %i %s\n",aname,errno,strerror(errno));
return;
}
while ((entry = readdir(proc_dir)) != NULL)
{
if(entry->d_name[0] == '.')
continue;
snprintf(aname, sizeof(aname), "/proc/%d/task/%s/stat", getpid(),entry->d_name);
read_statfile(aname,debug,prnt);
} /* while entry != NULL */
closedir(proc_dir);
} /* print_threads */
int proccmd_show(char *buf, int debug, telnet_printfunc_t prnt)
{
extern log_t *g_log;
if (debug > 0)
prnt(" proccmd_show received %s\n",buf);
if (strcasestr(buf,"thread") != NULL)
{
print_threads(buf,debug,prnt);
}
if (strcasestr(buf,"loglvl") != NULL) {
for (int i=MIN_LOG_COMPONENTS; i < MAX_LOG_COMPONENTS; i++){
prnt("\t%s:\t%s\t%s\n",g_log->log_component[i].name, map_int_to_str(log_verbosity_names,g_log->log_component[i].flag),
map_int_to_str(log_level_names,g_log->log_component[i].level));
}
}
return 0;
}
int proccmd_thread(char *buf, int debug, telnet_printfunc_t prnt)
{
int bv1,bv2;
int res;
char sv1[64];
char tname[32];
bv1=0;
bv2=0;
sv1[0]=0;
if (debug > 0)
prnt("proccmd_thread received %s\n",buf);
res=sscanf(buf,"%i %9s %i",&bv1,sv1,&bv2);
if (debug > 0)
prnt(" proccmd_thread: %i params = %i,%s,%i\n",res,bv1,sv1,bv2);
if(res != 3)
{
prnt("softmodem thread needs 3 params, %i received\n",res);
return 0;
}
if (strcasestr(sv1,"prio") != NULL)
{
set_sched(0,bv1, bv2);
}
else if (strcasestr(sv1,"aff") != NULL)
{
set_affinity(0,bv1, bv2);
}
else
{
prnt("%s is not a valid thread command\n",sv1);
}
return 0;
}
int proccmd_exit(char *buf, int debug, telnet_printfunc_t prnt)
{
extern void exit_fun(const char* s);
if (debug > 0)
prnt("process module received %s\n",buf);
exit_fun("telnet server received exit command\n");
return 0;
}
int proccmd_log(char *buf, int debug, telnet_printfunc_t prnt)
{
int idx1=0;
int idx2=NUM_LOG_LEVEL-1;
int s = sscanf(buf,"%*s %i-%i",&idx1,&idx2);
if (debug > 0)
prnt("process module received %s\n",buf);
if (strcasestr(buf,"enable") != NULL)
{
set_glog_onlinelog(1);
}
if (strcasestr(buf,"disable") != NULL)
{
set_glog_onlinelog(0);
}
return 0;
}
/*-------------------------------------------------------------------------------------*/
void add_softmodem_cmds()
{
add_telnetcmd("softmodem",NULL,proc_cmdarray);
}
/*
* Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The OpenAirInterface Software Alliance licenses this file to You under
* the OAI Public License, Version 1.0 (the "License"); you may not use this file
* except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.openairinterface.org/?page_id=698
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*-------------------------------------------------------------------------------
* For more information about the OpenAirInterface (OAI) Software Alliance:
* contact@openairinterface.org
*/
/*! \file common/utils/telnetsrv_proccmd.h
* \brief: Include file defining telnet commands related to this linux process
* \author Francois TABURET
* \date 2017
* \version 0.1
* \company NOKIA BellLabs France
* \email: francois.taburet@nokia-bell-labs.com
* \note
* \warning
*/
#include <dlfcn.h>
#include "telnetsrv.h"
#ifdef TELNETSRV_PROCCMD_MAIN
/* global variable from log.c */
extern mapping *log_level_names;
extern mapping *log_verbosity_names;
extern int proccmd_show(char *buf, int debug, telnet_printfunc_t prnt);
extern int proccmd_thread(char *buf, int debug, telnet_printfunc_t prnt);
extern int proccmd_exit(char *buf, int debug, telnet_printfunc_t prnt);
extern int proccmd_log(char *buf, int debug, telnet_printfunc_t prnt);
telnetshell_cmddef_t proc_cmdarray[] = {
{"show","loglvl|thread", proccmd_show},
{"log","[enable,disable]", proccmd_log},
{"thread","<id> aff|prio <aff|prio>", proccmd_thread},
{"exit","", proccmd_exit},
{"","",NULL},
};
#else
extern void add_proccmd_cmds();
#endif /* TELNETSRV_PROCCMD_MAIN */
......@@ -50,7 +50,7 @@
#include "PHY/defs.h"
#include "common/ran_context.h"
#include "common/config/config_userapi.h"
#include "common/utils/telnetsrv/load_telnet.h"
#undef MALLOC //there are two conflicting definitions, so we better make sure we don't use it at all
//#undef FRAME_LENGTH_COMPLEX_SAMPLES //there are two conflicting definitions, so we better make sure we don't use it at all
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment