- Improvements to Hipadaba
- New chopper driver for MARS Juelich chopper system - New docbook based SANS manual SKIPPED: psi/amorcomp.c psi/amordrive.h psi/amorset.c psi/amorset.h psi/amorset.tex psi/amorset.w psi/julcho.c psi/libpsi.a psi/make_gen psi/psi.c
This commit is contained in:
336
sicshipadaba.c
336
sicshipadaba.c
@@ -13,6 +13,7 @@
|
||||
#include <ctype.h>
|
||||
#include <sicshipadaba.h>
|
||||
#include <lld.h>
|
||||
#include <stptok.h>
|
||||
|
||||
/*== there can be only hipadaba in SICS, some globals to care for that == */
|
||||
static pHdb root = NULL;
|
||||
@@ -27,12 +28,22 @@ static int SICSCheckPermissionCallback(void *userData, void *callData, pHdb node
|
||||
pCon = (SConnection *)callData;
|
||||
testPriv = (int *)userData;
|
||||
|
||||
assert(pCon != NULL && testPriv != NULL);
|
||||
/*
|
||||
* If pCon is NULL, then this is an internal call from some driver
|
||||
* code where no permission check is necessary. However, when called
|
||||
* through the hipadaba tree commands and other upper level code, the
|
||||
* check will be honoured.
|
||||
*/
|
||||
if(pCon == NULL){
|
||||
return 1;
|
||||
}
|
||||
|
||||
assert(testPriv != NULL);
|
||||
|
||||
if(SCMatchRights(pCon,*testPriv) == 1){
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
return SICSCBPERM;
|
||||
}
|
||||
}
|
||||
/*--------------------------------------------------------------------------------------*/
|
||||
@@ -60,10 +71,11 @@ static int SICSReadOnlyCallback(void *userData, void *callData, pHdb node,
|
||||
SConnection *pCon = NULL;
|
||||
|
||||
pCon = (SConnection *)callData;
|
||||
assert(pCon != NULL);
|
||||
|
||||
SCWrite(pCon,"ERROR: parameter is READ-ONLY", eError);
|
||||
return 0;
|
||||
if(pCon != NULL){
|
||||
SCWrite(pCon,"ERROR: parameter is READ-ONLY", eError);
|
||||
}
|
||||
return SICSCBRO;
|
||||
}
|
||||
/*-------------------------------------------------------------------------------------*/
|
||||
static pHdbCallback MakeReadOnlyCallback(){
|
||||
@@ -86,6 +98,31 @@ pHdbCallback MakeSICSDriveCallback(void *sicsObject){
|
||||
return MakeHipadabaCallback(SICSDriveCallback, sicsObject,NULL,-1,-1);
|
||||
}
|
||||
/*---------------------------------------------------------------------------------------*/
|
||||
static int SICSReadDriveCallback(void *userData, void *callData, pHdb node,
|
||||
hdbValue v){
|
||||
SConnection *pCon = NULL;
|
||||
pDummy dum = NULL;
|
||||
pIDrivable pDriv = NULL;
|
||||
float value;
|
||||
|
||||
pCon = (SConnection *)callData;
|
||||
dum = (pDummy)userData;
|
||||
assert(pCon != NULL && dum != NULL);
|
||||
|
||||
pDriv = dum->pDescriptor->GetInterface(dum,DRIVEID);
|
||||
assert(pDriv != NULL);
|
||||
if(pCon != NULL){
|
||||
value = pDriv->GetValue(dum,pCon);
|
||||
node->value.v.doubleValue = (double)value;
|
||||
v.v.doubleValue = (double)value;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
/*--------------------------------------------------------------------------------------*/
|
||||
pHdbCallback MakeSICSReadDriveCallback(void *sicsObject){
|
||||
return MakeHipadabaCallback(SICSReadDriveCallback, sicsObject,NULL,-1,-1);
|
||||
}
|
||||
/*---------------------------------------------------------------------------------------*/
|
||||
typedef struct {
|
||||
SConnection *pCon;
|
||||
commandContext context;
|
||||
@@ -228,11 +265,144 @@ static int SICSScriptReadCallback(void *userData, void *callData, pHdb node,
|
||||
}
|
||||
return status;
|
||||
}
|
||||
/*----------------------------------------------------------------------------------------*/
|
||||
/*----------------------------------------------------------------------------*/
|
||||
static pHdbCallback MakeSICSReadScriptCallback(char *script){
|
||||
return MakeHipadabaCallback(SICSScriptReadCallback, strdup(script),free,-1,-1);
|
||||
return MakeHipadabaCallback(SICSScriptReadCallback, strdup(script),
|
||||
free,-1,-1);
|
||||
}
|
||||
/*============= Parameter Creation =======================================================*/
|
||||
/*---------------------------------------------------------------------------*/
|
||||
typedef struct {
|
||||
int min;
|
||||
int max;
|
||||
}hdbIntRange, *pHdbIntRange;
|
||||
/*---------------------------------------------------------------------------*/
|
||||
static int SICSIntRangeCallback(void *userData, void *callData, pHdb node,
|
||||
hdbValue v){
|
||||
char buffer[256];
|
||||
pHdbIntRange range = NULL;
|
||||
SConnection *pCon = NULL;
|
||||
int status = 1;
|
||||
|
||||
range = (pHdbIntRange)userData;
|
||||
pCon = (SConnection *)callData;
|
||||
|
||||
assert(range != NULL);
|
||||
|
||||
if(v.v.intValue > range->max || v.v.intValue < range->min) {
|
||||
status = SICSCBRANGE;
|
||||
if(pCon != NULL){
|
||||
snprintf(buffer,255,"ERROR: %d is not within permitted range: %d to %d",
|
||||
(int)v.v.intValue, range->min, range->max);
|
||||
SCWrite(pCon,buffer,eError);
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
/*---------------------------------------------------------------------------*/
|
||||
pHdbCallback MakeIntRangeCallback(int min, int max){
|
||||
pHdbIntRange range = NULL;
|
||||
|
||||
range = malloc(sizeof(hdbIntRange));
|
||||
if(range == NULL){
|
||||
return NULL;
|
||||
}
|
||||
range->min = min;
|
||||
range->max = max;
|
||||
return MakeHipadabaCallback(SICSIntRangeCallback, range,
|
||||
free,-1,-1);
|
||||
}
|
||||
/*---------------------------------------------------------------------------*/
|
||||
typedef struct {
|
||||
double min;
|
||||
double max;
|
||||
}hdbFloatRange, *pHdbFloatRange;
|
||||
/*---------------------------------------------------------------------------*/
|
||||
static int SICSFloatRangeCallback(void *userData, void *callData, pHdb node,
|
||||
hdbValue v){
|
||||
char buffer[256];
|
||||
pHdbFloatRange range = NULL;
|
||||
SConnection *pCon = NULL;
|
||||
int status = 1;
|
||||
|
||||
range = (pHdbFloatRange)userData;
|
||||
pCon = (SConnection *)callData;
|
||||
|
||||
assert(range != NULL);
|
||||
|
||||
if(v.v.doubleValue > range->max || v.v.doubleValue < range->min) {
|
||||
status = SICSCBRANGE;
|
||||
if(pCon != NULL){
|
||||
snprintf(buffer,255,"ERROR: %lf is not within permitted range: %lf to %lf",
|
||||
v.v.doubleValue, range->min, range->max);
|
||||
SCWrite(pCon,buffer,eError);
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
/*---------------------------------------------------------------------------*/
|
||||
pHdbCallback MakeFloatRangeCallback(double min, double max){
|
||||
pHdbFloatRange range = NULL;
|
||||
|
||||
range = malloc(sizeof(hdbFloatRange));
|
||||
if(range == NULL){
|
||||
return NULL;
|
||||
}
|
||||
range->min = min;
|
||||
range->max = max;
|
||||
return MakeHipadabaCallback(SICSFloatRangeCallback, range,
|
||||
free,-1,-1);
|
||||
}
|
||||
/*--------------------------------------------------------------------------*/
|
||||
static void killHdbValue(void *pData){
|
||||
hdbValue *v = NULL;
|
||||
|
||||
v = (hdbValue *)pData;
|
||||
if(v == NULL){
|
||||
return;
|
||||
}
|
||||
ReleaseHdbValue(v);
|
||||
free(v);
|
||||
}
|
||||
/*--------------------------------------------------------------------------*/
|
||||
static int SICSIntFixedCallback(void *userData, void *callData, pHdb node,
|
||||
hdbValue v){
|
||||
hdbValue *allowed = NULL;
|
||||
SConnection *pCon = NULL;
|
||||
int i;
|
||||
|
||||
allowed = (hdbValue *)userData;
|
||||
pCon = (SConnection *)callData;
|
||||
assert(allowed != NULL && allowed->dataType == HIPINTAR);
|
||||
for(i = 0; i < allowed->arrayLength; i++){
|
||||
if(v.v.intValue == allowed->v.intArray[i]){
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if(pCon != NULL){
|
||||
SCWrite(pCon,"ERROR: value is not in the list of allowed values",eError);
|
||||
}
|
||||
return SICSCBBADFIXED;
|
||||
}
|
||||
/*---------------------------------------------------------------------------*/
|
||||
pHdbCallback MakeIntFixedCallback(long *data, int length){
|
||||
pHdbCallback result = NULL;
|
||||
hdbValue *v = NULL;
|
||||
|
||||
v = malloc(sizeof(hdbValue));
|
||||
if(v == NULL){
|
||||
return NULL;
|
||||
}
|
||||
v->dataType = HIPINTAR;
|
||||
v->arrayLength = length;
|
||||
v->v.intArray = malloc(length*sizeof(long));
|
||||
if(v->v.intArray == NULL){
|
||||
return NULL;
|
||||
}
|
||||
memcpy(v->v.intArray,data,length*sizeof(long));
|
||||
return MakeHipadabaCallback(SICSIntFixedCallback, v,
|
||||
killHdbValue,-1,-1);
|
||||
}
|
||||
/*============= Parameter Creation ===========================================*/
|
||||
pHdb MakeSICSHdbPar(char *name, int priv, hdbValue v){
|
||||
pHdb result = NULL;
|
||||
pHdbCallback pHcb = NULL;
|
||||
@@ -283,10 +453,17 @@ pHdb MakeSICSHdbDriv(char *name, int priv, void *sicsObject, int dataType){
|
||||
}
|
||||
AppendHipadabaCallback(result,HCBSET,pHcb);
|
||||
|
||||
pHcb = MakeSICSReadDriveCallback(sicsObject);
|
||||
if(pHcb == NULL){
|
||||
DeleteHipadabaNode(result);
|
||||
return NULL;
|
||||
}
|
||||
AppendHipadabaCallback(result,HCBREAD,pHcb);
|
||||
|
||||
return result;
|
||||
}
|
||||
/*----------------------------------------------------------------------------*/
|
||||
pHdb MakeSICSHdbROPar(char *name, int priv, hdbValue v){
|
||||
pHdb MakeSICSROPar(char *name, hdbValue v){
|
||||
pHdb result = NULL;
|
||||
pHdbCallback pHcb = NULL;
|
||||
|
||||
@@ -296,7 +473,7 @@ pHdb MakeSICSHdbROPar(char *name, int priv, hdbValue v){
|
||||
}
|
||||
copyHdbValue(&v,&result->value);
|
||||
|
||||
pHcb = MakeReadOnlyCallback(priv);
|
||||
pHcb = MakeReadOnlyCallback();
|
||||
if(pHcb == NULL){
|
||||
DeleteHipadabaNode(result);
|
||||
return NULL;
|
||||
@@ -498,6 +675,9 @@ int SICSHipadabaTask(void *pData){
|
||||
if(self->iEnd == 1){
|
||||
return 0;
|
||||
}
|
||||
if(LLDcheck(self->updateList) == LIST_EMPTY){
|
||||
return 1;
|
||||
}
|
||||
memset(&old,0,sizeof(hdbValue));
|
||||
memset(&newValue,0,sizeof(hdbValue));
|
||||
|
||||
@@ -561,12 +741,14 @@ pDynString formatValue(hdbValue v){
|
||||
DynStringCopy(result,v.v.text);
|
||||
break;
|
||||
case HIPINTAR:
|
||||
case HIPINTVARAR:
|
||||
for(i = 0; i < v.arrayLength; i++){
|
||||
snprintf(number,30," %ld", v.v.intArray[i]);
|
||||
DynStringConcat(result,number);
|
||||
}
|
||||
break;
|
||||
case HIPFLOATAR:
|
||||
case HIPFLOATVARAR:
|
||||
for(i = 0; i < v.arrayLength; i++){
|
||||
snprintf(number,30," %12.4f", v.v.floatArray[i]);
|
||||
DynStringConcat(result,number);
|
||||
@@ -597,7 +779,39 @@ static char *getNextHdbNumber(char *pStart, char pNumber[80]){
|
||||
pNumber[charCount] = '\0';
|
||||
return pStart;
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------------------------------*/
|
||||
static int adjustDataLength(hdbValue *v, char *data){
|
||||
char number[80];
|
||||
int count = 0;
|
||||
|
||||
while(getNextHdbNumber(data,number) != NULL){
|
||||
count++;
|
||||
}
|
||||
if(count != v->arrayLength){
|
||||
v->arrayLength = count;
|
||||
if(v->dataType == HIPINTVARAR){
|
||||
if(v->v.intArray != NULL){
|
||||
free(v->v.intArray);
|
||||
}
|
||||
v->v.intArray = malloc(count*sizeof(long));
|
||||
if(v->v.intArray == NULL){
|
||||
return 0;
|
||||
}
|
||||
memset(v->v.intArray,0,count*sizeof(long));
|
||||
}
|
||||
if(v->dataType == HIPFLOATVARAR){
|
||||
if(v->v.floatArray != NULL){
|
||||
free(v->v.floatArray);
|
||||
}
|
||||
v->v.floatArray = malloc(count*sizeof(double));
|
||||
if(v->v.floatArray == NULL){
|
||||
return 0;
|
||||
}
|
||||
memset(v->v.floatArray,0,count*sizeof(double));
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
/*---------------------------------------------------------------------------------*/
|
||||
int readHdbValue(hdbValue *v, char *data, char *error, int errlen){
|
||||
int i, status;
|
||||
@@ -633,6 +847,11 @@ int readHdbValue(hdbValue *v, char *data, char *error, int errlen){
|
||||
}
|
||||
v->v.text = strdup(data);
|
||||
break;
|
||||
case HIPINTVARAR:
|
||||
if(!adjustDataLength(v,data)){
|
||||
snprintf(error,errlen,"Out of memory allocating variable length data");
|
||||
return 0;
|
||||
}
|
||||
case HIPINTAR:
|
||||
for(i = 0; i < v->arrayLength; i++){
|
||||
data = getNextHdbNumber(data,number);
|
||||
@@ -650,6 +869,11 @@ int readHdbValue(hdbValue *v, char *data, char *error, int errlen){
|
||||
v->v.intArray[i] = lValue;
|
||||
}
|
||||
break;
|
||||
case HIPFLOATVARAR:
|
||||
if(!adjustDataLength(v,data)){
|
||||
snprintf(error,errlen,"Out of memory allocating variable length data");
|
||||
return 0;
|
||||
}
|
||||
case HIPFLOATAR:
|
||||
for(i = 0; i < v->arrayLength; i++){
|
||||
data = getNextHdbNumber(data,number);
|
||||
@@ -684,6 +908,8 @@ static char *hdbTypes[] = {"none",
|
||||
"text",
|
||||
"intar",
|
||||
"floatar",
|
||||
"intvarar",
|
||||
"floatvarar",
|
||||
NULL};
|
||||
/*-------------------------------------------------------------------------*/
|
||||
static int convertHdbType(char *text){
|
||||
@@ -729,9 +955,9 @@ static int MakeHdbNode(SConnection *pCon, SicsInterp *pSics, void *pData,
|
||||
*/
|
||||
strtolower(argv[3]);
|
||||
type = convertHdbType(argv[3]);
|
||||
if(type >= 5){
|
||||
if(type >= 7){
|
||||
SCWrite(pCon,
|
||||
"ERROR: invalid type requested: none, int, float, text, intar, floatar supported",
|
||||
"ERROR: invalid type requested: none, int, float, text, intar, floatar, intvarar, floatvarar supported",
|
||||
eError);
|
||||
return 0;
|
||||
}
|
||||
@@ -800,9 +1026,9 @@ static int MakeHdbScriptNode(SConnection *pCon, SicsInterp *pSics, void *pData,
|
||||
*/
|
||||
strtolower(argv[4]);
|
||||
type = convertHdbType(argv[4]);
|
||||
if(type >= 5){
|
||||
if(type >= 7){
|
||||
SCWrite(pCon,
|
||||
"ERROR: invalid type requested: none, int, float, text, intar, floatar supported",
|
||||
"ERROR: invalid type requested: none, int, float, text, intar, floatar, intvarar, floatvarar supported",
|
||||
eError);
|
||||
return 0;
|
||||
}
|
||||
@@ -868,7 +1094,46 @@ static int DeleteHdbNode(SConnection *pCon, SicsInterp *pSics, void *pData,
|
||||
SCSendOK(pCon);
|
||||
return 1;
|
||||
}
|
||||
/*------------------------------------------------------------------------------------------*/
|
||||
/*---------------------------------------------------------------------------*/
|
||||
static pHdb locateSICSNode(SicsInterp *pSics, SConnection *pCon, char *path){
|
||||
pHdb result = NULL;
|
||||
char *pPtr = NULL, sicsObj[128], error[256];
|
||||
pDummy pDum = NULL;
|
||||
CommandList *pCom = NULL;
|
||||
|
||||
if(strstr(path,"/sics/") != NULL){
|
||||
pPtr = stptok(path,sicsObj,128,"/");
|
||||
pPtr = stptok(pPtr,sicsObj,128,"/");
|
||||
pPtr = stptok(pPtr,sicsObj,128,"/");
|
||||
strtolower(sicsObj);
|
||||
pCom = FindCommand(pSics,sicsObj);
|
||||
if(pCom == NULL) {
|
||||
snprintf(error,255,"ERROR: object %s not found",sicsObj);
|
||||
SCWrite(pCon,error,eError);
|
||||
return NULL;
|
||||
}
|
||||
pDum = (pDummy)pCom->pData;
|
||||
if(pDum == NULL){
|
||||
snprintf(error,255,"ERROR: object %s has no data",sicsObj);
|
||||
SCWrite(pCon,error,eError);
|
||||
return NULL;
|
||||
}
|
||||
if(pDum->pDescriptor->parNode == NULL){
|
||||
snprintf(error,255,"ERROR: object %s does not use Hipadaba",sicsObj);
|
||||
SCWrite(pCon,error,eError);
|
||||
return NULL;
|
||||
}
|
||||
result = GetHipadabaNode(pDum->pDescriptor->parNode,pPtr);
|
||||
} else {
|
||||
result = GetHipadabaNode(root,path);
|
||||
}
|
||||
if(result == NULL){
|
||||
snprintf(error,255,"ERROR: node %s NOT found",path);
|
||||
SCWrite(pCon,error,eError);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/*---------------------------------------------------------------------------*/
|
||||
static int SetHdbNode(SConnection *pCon, SicsInterp *pSics, void *pData,
|
||||
int argc, char *argv[]){
|
||||
pHdb targetNode = NULL;
|
||||
@@ -887,10 +1152,8 @@ static int SetHdbNode(SConnection *pCon, SicsInterp *pSics, void *pData,
|
||||
return 0;
|
||||
}
|
||||
|
||||
targetNode = GetHipadabaNode(root,argv[1]);
|
||||
targetNode = locateSICSNode(pSics,pCon,argv[1]);
|
||||
if(targetNode == NULL){
|
||||
SCWrite(pCon,"ERROR: node to set not found!",
|
||||
eError);
|
||||
return 0;
|
||||
}
|
||||
if(!cloneHdbValue(&targetNode->value,&newValue)){
|
||||
@@ -936,10 +1199,8 @@ static int GetHdbNode(SConnection *pCon, SicsInterp *pSics, void *pData,
|
||||
}
|
||||
|
||||
strncpy(oriPath,argv[1], 511);
|
||||
targetNode = GetHipadabaNode(root,argv[1]);
|
||||
targetNode = locateSICSNode(pSics,pCon,argv[1]);
|
||||
if(targetNode == NULL){
|
||||
SCWrite(pCon,"ERROR: node to read not found!",
|
||||
eError);
|
||||
return 0;
|
||||
}
|
||||
memset(&newValue,0,sizeof(hdbValue));
|
||||
@@ -1057,6 +1318,7 @@ static pDynString formatClientList(pHdb node){
|
||||
DynStringConcat(result,current->value.v.text);
|
||||
break;
|
||||
case HIPINTAR:
|
||||
case HIPINTVARAR:
|
||||
for(i = 0; i < length; i++){
|
||||
snprintf(number,50,"%ld",current->value.v.intArray[i]);
|
||||
DynStringConcat(result,number);
|
||||
@@ -1066,6 +1328,7 @@ static pDynString formatClientList(pHdb node){
|
||||
}
|
||||
break;
|
||||
case HIPFLOATAR:
|
||||
case HIPFLOATVARAR:
|
||||
for(i = 0; i < length; i++){
|
||||
snprintf(number,50,"%lf",current->value.v.floatArray[i]);
|
||||
DynStringConcat(result,number);
|
||||
@@ -1100,10 +1363,8 @@ static int ListHdbNode(SConnection *pCon, SicsInterp *pSics, void *pData,
|
||||
}
|
||||
}
|
||||
|
||||
node = GetHipadabaNode(root,argv[pathArg]);
|
||||
node = locateSICSNode(pSics,pCon,argv[pathArg]);
|
||||
if(node == NULL){
|
||||
SCWrite(pCon,"ERROR: node to list not found!",
|
||||
eError);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1136,10 +1397,8 @@ static int AutoNotifyHdbNode(SConnection *pCon, SicsInterp *pSics, void *pData,
|
||||
return 0;
|
||||
}
|
||||
|
||||
node = GetHipadabaNode(root,argv[1]);
|
||||
node = locateSICSNode(pSics,pCon,argv[1]);
|
||||
if(node == NULL){
|
||||
SCWrite(pCon,"ERROR: node to add notify found!",
|
||||
eError);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1168,15 +1427,18 @@ static int RemoveHdbCallback(SConnection *pCon, SicsInterp *pSics, void *pData,
|
||||
return 1;
|
||||
}
|
||||
/*---------------------------------------------------------------------------*/
|
||||
void killSICSHipadaba(void *pData){
|
||||
DeleteHipadabaNode(root);
|
||||
void killSICSHipadaba(){
|
||||
if(root != NULL){
|
||||
DeleteHipadabaNode(root);
|
||||
}
|
||||
root = NULL;
|
||||
/**
|
||||
* children have already been removed when killing the
|
||||
* main tree
|
||||
*/
|
||||
/**
|
||||
* children have already been removed when killing the
|
||||
* main tree
|
||||
*/
|
||||
if(scriptUpdate > 0 && LLDcheck(scriptUpdate) != LIST_EMPTY){
|
||||
LLDdelete(scriptUpdate);
|
||||
SCDeleteConnection(taskData.pCon);
|
||||
}
|
||||
}
|
||||
/*---------------------------------------------------------------------------*/
|
||||
int InstallSICSHipadaba(SConnection *pCon, SicsInterp *pSics, void *pData,
|
||||
@@ -1194,10 +1456,10 @@ int InstallSICSHipadaba(SConnection *pCon, SicsInterp *pSics, void *pData,
|
||||
TaskRegister(pServ->pTasker,
|
||||
SICSHipadabaTask,
|
||||
SICSHipadabaSignal,
|
||||
killSICSHipadaba,
|
||||
NULL,
|
||||
&taskData,1);
|
||||
|
||||
AddCommand(pSics,"hmake", MakeHdbNode, killSICSHipadaba, NULL);
|
||||
AddCommand(pSics,"hmake", MakeHdbNode, NULL, NULL);
|
||||
AddCommand(pSics,"hmakescript", MakeHdbScriptNode, NULL, NULL);
|
||||
AddCommand(pSics,"hdel", DeleteHdbNode, NULL, NULL);
|
||||
AddCommand(pSics,"hset", SetHdbNode, NULL, NULL);
|
||||
|
||||
Reference in New Issue
Block a user