mirror of
https://github.com/uroni/urbackup_backend.git
synced 2025-10-26 11:36:50 +00:00
1387 lines
31 KiB
C++
1387 lines
31 KiB
C++
/*************************************************************************
|
|
* UrBackup - Client/Server backup system
|
|
* Copyright (C) 2011 Martin Raiber
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* 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.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
**************************************************************************/
|
|
|
|
#include "vld.h"
|
|
|
|
#include <iostream>
|
|
#include <fstream>
|
|
#include <memory.h>
|
|
#ifndef _WIN32
|
|
#include <errno.h>
|
|
#endif
|
|
#include "libfastcgi/fastcgi.hpp"
|
|
|
|
#include "Interface/PluginMgr.h"
|
|
#include "Interface/Thread.h"
|
|
|
|
#include "Server.h"
|
|
#include "Template.h"
|
|
#include "stringtools.h"
|
|
#include "defaults.h"
|
|
#include "SessionMgr.h"
|
|
#include "md5.h"
|
|
#include "ServiceAcceptor.h"
|
|
#include "LookupService.h"
|
|
#include "FileSettingsReader.h"
|
|
#include "DBSettingsReader.h"
|
|
#include "StreamPipe.h"
|
|
#include "ThreadPool.h"
|
|
#include "file.h"
|
|
#include "utf8/utf8.h"
|
|
#include "MemoryPipe.h"
|
|
#include "MemorySettingsReader.h"
|
|
//#include "file_memory.h"
|
|
|
|
|
|
#ifdef THREAD_BOOST
|
|
#include <boost/thread/thread.hpp>
|
|
#include <boost/thread/xtime.hpp>
|
|
#include <boost/bind.hpp>
|
|
#include "Mutex_boost.h"
|
|
#include "Condition_boost.h"
|
|
#else
|
|
#ifdef _WIN32
|
|
#else
|
|
#include <pthread.h>
|
|
#include "Mutex_lin.h"
|
|
#include "Condition_lin.h"
|
|
#endif
|
|
#endif
|
|
|
|
#ifdef _WIN32
|
|
# include <windows.h>
|
|
# include "Helper_win32.h"
|
|
#else
|
|
# include <ctime>
|
|
# include <sys/time.h>
|
|
# include <unistd.h>
|
|
# include <sys/types.h>
|
|
# include <pwd.h>
|
|
#endif
|
|
|
|
const size_t SEND_BLOCKSIZE=8192;
|
|
|
|
CServer::CServer()
|
|
{
|
|
curr_thread_id=0;
|
|
curr_pluginid=0;
|
|
curr_postfilekey=0;
|
|
loglevel=LL_INFO;
|
|
logfile_a=false;
|
|
|
|
log_mutex=createMutex();
|
|
action_mutex=createMutex();
|
|
requests_mutex=createMutex();
|
|
outputs_mutex=createMutex();
|
|
db_mutex=createMutex();
|
|
thread_mutex=createMutex();
|
|
plugin_mutex=createMutex();
|
|
rps_mutex=createMutex();
|
|
postfiles_mutex=createMutex();
|
|
param_mutex=createMutex();
|
|
}
|
|
|
|
void CServer::setup(void)
|
|
{
|
|
CFileSettingsReader::setup();
|
|
CDatabase::initMutex();
|
|
sessmgr=new CSessionMgr();
|
|
threadpool=new CThreadPool();
|
|
}
|
|
|
|
void CServer::destroyAllDatabases(void)
|
|
{
|
|
for(std::map<DATABASE_ID, std::pair<std::string, std::map<THREAD_ID, CDatabase*> > >::iterator i=databases.begin();
|
|
i!=databases.end();++i)
|
|
{
|
|
for( std::map<THREAD_ID, CDatabase*>::iterator j=i->second.second.begin();j!=i->second.second.end();++j)
|
|
{
|
|
delete j->second;
|
|
}
|
|
i->second.second.clear();
|
|
}
|
|
}
|
|
|
|
CServer::~CServer()
|
|
{
|
|
Log("deleting cached settings...");
|
|
CFileSettingsReader::cleanup();
|
|
|
|
Log("deleting stream services...");
|
|
//Delete extra Services
|
|
for(size_t i=0;i<stream_services.size();++i)
|
|
{
|
|
delete stream_services[i];
|
|
}
|
|
|
|
Log("deleting plugins...");
|
|
//delete Plugins
|
|
for(std::map<PLUGIN_ID, std::pair<IPluginMgr*,str_map> >::iterator iter1=perthread_pluginparams.begin();
|
|
iter1!=perthread_pluginparams.end();++iter1)
|
|
{
|
|
std::map<PLUGIN_ID, std::map<THREAD_ID, IPlugin*> >::iterator iter2=perthread_plugins.find( iter1->first );
|
|
|
|
if( iter2!=perthread_plugins.end() )
|
|
{
|
|
std::map<THREAD_ID, IPlugin*> *pmap=&iter2->second;
|
|
|
|
for( std::map<THREAD_ID, IPlugin*>::iterator iter3=pmap->begin();iter3!=pmap->end();++iter3 )
|
|
{
|
|
iter1->second.first->destroyPluginInstance( iter3->second );
|
|
}
|
|
}
|
|
}
|
|
|
|
Log("Deleting threadsafe plugins...");
|
|
for(std::map<PLUGIN_ID, IPlugin*>::iterator iter1=threadsafe_plugins.begin();iter1!=threadsafe_plugins.end();++iter1)
|
|
{
|
|
iter1->second->Remove();
|
|
}
|
|
|
|
Log("Deleting pluginmanagers...");
|
|
|
|
for(std::map<std::string, IPluginMgr*>::iterator iter=perthread_pluginmgrs.begin();iter!=perthread_pluginmgrs.end();++iter)
|
|
{
|
|
iter->second->Remove();
|
|
}
|
|
|
|
for(std::map<std::string, IPluginMgr*>::iterator iter=threadsafe_pluginmgrs.begin();iter!=threadsafe_pluginmgrs.end();++iter)
|
|
{
|
|
iter->second->Remove();
|
|
}
|
|
|
|
Log("Deleting actions...");
|
|
|
|
for(std::map< std::wstring, std::map<std::wstring, IAction*> >::iterator iter1=actions.begin();iter1!=actions.end();++iter1)
|
|
{
|
|
for(std::map<std::wstring, IAction*>::iterator iter2=iter1->second.begin();iter2!=iter1->second.end();++iter2)
|
|
{
|
|
iter2->second->Remove();
|
|
}
|
|
}
|
|
actions.clear();
|
|
|
|
Log("Deleting sessmgr...");
|
|
delete sessmgr;
|
|
|
|
Log("Shutting down ThreadPool...");
|
|
threadpool->Shutdown();
|
|
delete threadpool;
|
|
|
|
Log("destroying databases...");
|
|
//Destroy Databases
|
|
destroyAllDatabases();
|
|
|
|
Log("unloading dlls...");
|
|
UnloadDLLs();
|
|
|
|
Log("Destroying mutexes");
|
|
|
|
destroy(log_mutex);
|
|
destroy(action_mutex);
|
|
destroy(requests_mutex);
|
|
destroy(outputs_mutex);
|
|
destroy(db_mutex);
|
|
destroy(thread_mutex);
|
|
destroy(plugin_mutex);
|
|
destroy(rps_mutex);
|
|
destroy(postfiles_mutex);
|
|
destroy(param_mutex);
|
|
CDatabase::destroyMutex();
|
|
|
|
std::cout << "Server cleanup done..." << std::endl;
|
|
}
|
|
|
|
void CServer::setServerParameters(const str_nmap &pServerParams)
|
|
{
|
|
IScopedLock lock(param_mutex);
|
|
server_params=pServerParams;
|
|
}
|
|
|
|
std::string CServer::getServerParameter(const std::string &key)
|
|
{
|
|
return getServerParameter(key, "");
|
|
}
|
|
|
|
std::string CServer::getServerParameter(const std::string &key, const std::string &def)
|
|
{
|
|
IScopedLock lock(param_mutex);
|
|
str_nmap::iterator iter=server_params.find(key);
|
|
if( iter!=server_params.end() )
|
|
{
|
|
return iter->second;
|
|
}
|
|
else
|
|
return def;
|
|
}
|
|
|
|
void CServer::setServerParameter(const std::string &key, const std::string &value)
|
|
{
|
|
IScopedLock lock(param_mutex);
|
|
server_params[key]=value;
|
|
}
|
|
|
|
void CServer::Log( const std::string &pStr, int LogLevel)
|
|
{
|
|
if( loglevel <=LogLevel )
|
|
{
|
|
IScopedLock lock(log_mutex);
|
|
|
|
time_t rawtime;
|
|
char buffer [100];
|
|
time ( &rawtime );
|
|
#ifdef _WIN32
|
|
struct tm timeinfo;
|
|
localtime_s(&timeinfo, &rawtime);
|
|
strftime (buffer,100,"%x %X: ",&timeinfo);
|
|
#else
|
|
struct tm *timeinfo;
|
|
timeinfo = localtime ( &rawtime );
|
|
strftime (buffer,100,"%x %X: ",timeinfo);
|
|
#endif
|
|
|
|
|
|
if( LogLevel==LL_ERROR )
|
|
{
|
|
std::cout << buffer << "ERROR: " << pStr << std::endl;
|
|
if(logfile_a)
|
|
logfile << buffer << "ERROR: " << pStr << std::endl;
|
|
}
|
|
else if( LogLevel==LL_WARNING )
|
|
{
|
|
std::cout << buffer << "WARNING: " << pStr << std::endl;
|
|
if(logfile_a)
|
|
logfile<< buffer << "WARNING: " << pStr << std::endl;
|
|
}
|
|
else
|
|
{
|
|
std::cout << buffer << pStr << std::endl;
|
|
if(logfile_a)
|
|
logfile << buffer << pStr << std::endl;
|
|
}
|
|
|
|
if(logfile_a)
|
|
logfile.flush();
|
|
}
|
|
}
|
|
|
|
void CServer::Log( const std::wstring &pStr, int LogLevel)
|
|
{
|
|
if( loglevel <=LogLevel )
|
|
{
|
|
IScopedLock lock(log_mutex);
|
|
|
|
time_t rawtime;
|
|
char buffer [100];
|
|
time ( &rawtime );
|
|
#ifdef _WIN32
|
|
struct tm timeinfo;
|
|
localtime_s(&timeinfo, &rawtime);
|
|
strftime (buffer,100,"%x %X: ",&timeinfo);
|
|
#else
|
|
struct tm *timeinfo;
|
|
timeinfo = localtime ( &rawtime );
|
|
strftime (buffer,100,"%x %X: ",timeinfo);
|
|
#endif
|
|
|
|
if( LogLevel==LL_ERROR )
|
|
{
|
|
std::cout << buffer;
|
|
std::wcout << L"ERROR: " << pStr << std::endl;
|
|
if(logfile_a)
|
|
logfile << buffer << "ERROR: " << ConvertToUTF8(pStr) << std::endl;
|
|
}
|
|
else if( LogLevel==LL_WARNING )
|
|
{
|
|
std::cout << buffer;
|
|
std::wcout << L"WARNING: " << pStr << std::endl;
|
|
if(logfile_a)
|
|
logfile << buffer << "WARNING: " << ConvertToUTF8(pStr) << std::endl;
|
|
}
|
|
else
|
|
{
|
|
std::cout << buffer;
|
|
std::wcout << pStr << std::endl;
|
|
if(logfile_a)
|
|
logfile << buffer << ConvertToUTF8(pStr) << std::endl;
|
|
}
|
|
|
|
if(logfile_a)
|
|
logfile.flush();
|
|
}
|
|
}
|
|
|
|
void CServer::setLogFile(const std::string &plf, std::string chown_user)
|
|
{
|
|
if(logfile_a)
|
|
{
|
|
logfile.close();
|
|
logfile_a=false;
|
|
}
|
|
logfile.open( plf.c_str(), std::ios::app | std::ios::out | std::ios::binary );
|
|
if(logfile.is_open() )
|
|
{
|
|
#ifndef _WIN32
|
|
if(!chown_user.empty())
|
|
{
|
|
char buf[1000];
|
|
passwd pwbuf;
|
|
passwd *pw;
|
|
int rc=getpwnam_r(chown_user.c_str(), &pwbuf, buf, 1000, &pw);
|
|
if(pw!=NULL)
|
|
{
|
|
chown(plf.c_str(), pw->pw_uid, pw->pw_gid);
|
|
}
|
|
else
|
|
{
|
|
Server->Log("Unable to change logfile ownership", LL_ERROR);
|
|
}
|
|
}
|
|
#endif
|
|
logfile_a=true;
|
|
}
|
|
}
|
|
|
|
void CServer::setLogLevel(int LogLevel)
|
|
{
|
|
IScopedLock lock(log_mutex);
|
|
loglevel=LogLevel;
|
|
}
|
|
|
|
THREAD_ID CServer::Execute(const std::wstring &action, const std::wstring &context, str_map &GET, str_map &POST, str_nmap &PARAMS, IOutputStream *req)
|
|
{
|
|
IAction *action_ptr=NULL;
|
|
{
|
|
IScopedLock lock(action_mutex);
|
|
std::map<std::wstring, std::map<std::wstring, IAction*> >::iterator iter1=actions.find( context );
|
|
if( iter1!=actions.end() )
|
|
{
|
|
std::map<std::wstring, IAction*>::iterator iter2=iter1->second.find(action);
|
|
if( iter2!=iter1->second.end() )
|
|
action_ptr=iter2->second;
|
|
}
|
|
}
|
|
|
|
if( action_ptr!=NULL )
|
|
{
|
|
THREAD_ID tid=getThreadID();
|
|
IOutputStream *current_req=NULL;
|
|
{
|
|
IScopedLock lock(requests_mutex);
|
|
std::map<THREAD_ID, IOutputStream*>::iterator iter=current_requests.find(tid);
|
|
if( iter!=current_requests.end() )
|
|
{
|
|
current_req=iter->second;
|
|
iter->second=req;
|
|
}
|
|
else
|
|
{
|
|
current_requests.insert(std::pair<THREAD_ID, IOutputStream*>(tid, req) );
|
|
}
|
|
}
|
|
|
|
{
|
|
IScopedLock lock(outputs_mutex);
|
|
current_outputs[tid]=std::pair<bool, std::string>(false, "");
|
|
}
|
|
|
|
action_ptr->Execute( GET, POST, tid, PARAMS);
|
|
|
|
ClearDatabases(tid);
|
|
|
|
WriteRaw(tid, NULL, 0, false);
|
|
|
|
if( current_req!=NULL )
|
|
{
|
|
IScopedLock lock(requests_mutex);
|
|
current_requests[tid]=current_req;
|
|
}
|
|
|
|
return tid;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
std::string CServer::Execute(const std::wstring &action, const std::wstring &context, str_map &GET, str_map &POST, str_nmap &PARAMS)
|
|
{
|
|
CStringOutputStream cos;
|
|
Execute(action, context, GET, POST, PARAMS, &cos);
|
|
return cos.getData();
|
|
}
|
|
|
|
void CServer::AddAction(IAction *action)
|
|
{
|
|
IScopedLock lock(action_mutex);
|
|
|
|
std::map<std::wstring, IAction*> *ptr=&actions[action_context];
|
|
ptr->insert( std::pair<std::wstring, IAction*>(action->getName(), action ) );
|
|
}
|
|
|
|
bool CServer::RemoveAction(IAction *action)
|
|
{
|
|
IScopedLock lock(action_mutex);
|
|
|
|
std::map<std::wstring, std::map<std::wstring, IAction*> >::iterator iter1=actions.find(action_context);
|
|
|
|
if( iter1!=actions.end() )
|
|
{
|
|
std::map<std::wstring, IAction*>::iterator iter2=iter1->second.find( action->getName() );
|
|
if( iter2!=iter1->second.end() )
|
|
{
|
|
iter1->second.erase( iter2 );
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void CServer::setActionContext(std::wstring context)
|
|
{
|
|
action_context=context;
|
|
}
|
|
|
|
void CServer::resetActionContext(void)
|
|
{
|
|
action_context.clear();
|
|
}
|
|
|
|
unsigned int CServer::getTimeSeconds(void)
|
|
{
|
|
#ifdef _WIN32
|
|
SYSTEMTIME st;
|
|
GetSystemTime(&st);
|
|
return (unsigned int)(unix_timestamp(&st));
|
|
#else
|
|
timeval t;
|
|
gettimeofday(&t,NULL);
|
|
return (unsigned int)t.tv_sec;
|
|
#endif
|
|
}
|
|
|
|
unsigned int CServer::getTimeMS(void)
|
|
{
|
|
#ifdef _WIN32
|
|
return GetTickCount();
|
|
#else
|
|
//return (unsigned int)(((double)clock()/(double)CLOCKS_PER_SEC)*1000.0);
|
|
/*
|
|
boost::xtime xt;
|
|
boost::xtime_get(&xt, boost::TIME_UTC);
|
|
static boost::int_fast64_t start_t=xt.sec;
|
|
xt.sec-=start_t;
|
|
unsigned int t=xt.sec*1000+(unsigned int)((double)xt.nsec/1000000.0);
|
|
return t;*/
|
|
timeval tp;
|
|
gettimeofday(&tp, NULL);
|
|
static long start_t=tp.tv_sec;
|
|
tp.tv_sec-=start_t;
|
|
return tp.tv_sec*1000+tp.tv_usec/1000;
|
|
#endif
|
|
}
|
|
|
|
void CServer::WriteRaw(THREAD_ID tid, const char *buf, size_t bsize, bool cached)
|
|
{
|
|
if( cached==false )
|
|
{
|
|
IOutputStream* req=NULL;
|
|
{
|
|
IScopedLock lock(requests_mutex);
|
|
std::map<THREAD_ID, IOutputStream*>::iterator iter=current_requests.find( tid );
|
|
if( iter!=current_requests.end() )
|
|
req=iter->second;
|
|
}
|
|
if( req!=NULL )
|
|
{
|
|
{
|
|
IScopedLock lock(outputs_mutex);
|
|
std::pair<bool, std::string> *co=¤t_outputs[tid];
|
|
std::string *curr_output=&co->second;
|
|
bool sent=co->first;
|
|
|
|
if( sent==false && next(*curr_output,0,"Content-type: ")==false )
|
|
{
|
|
curr_output->insert(0, "Content-type: "+DEFAULT_CONTENTTYPE+"\r\n\r\n");
|
|
co->first=true;
|
|
}
|
|
|
|
if(curr_output->size()<SEND_BLOCKSIZE)
|
|
{
|
|
req->write(*curr_output);
|
|
}
|
|
else
|
|
{
|
|
for(size_t i=0,size=curr_output->size();i<size;i+=SEND_BLOCKSIZE)
|
|
{
|
|
req->write(&(*curr_output)[i], (std::min)(SEND_BLOCKSIZE, size-i) );
|
|
}
|
|
}
|
|
curr_output->clear();
|
|
}
|
|
|
|
if( bsize>0 && bsize<SEND_BLOCKSIZE)
|
|
{
|
|
req->write(buf, bsize);
|
|
}
|
|
else if(bsize>0 )
|
|
{
|
|
for(size_t i=0,size=bsize;i<size;i+=SEND_BLOCKSIZE)
|
|
{
|
|
req->write(&buf[i], (std::min)(SEND_BLOCKSIZE, size-i) );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
Log("Couldn't find THREAD_ID - cached=true", LL_ERROR);
|
|
}
|
|
else
|
|
{
|
|
if( bsize>0 )
|
|
{
|
|
IScopedLock lock(outputs_mutex);
|
|
std::map<THREAD_ID, std::pair<bool, std::string> >::iterator iter=current_outputs.find( tid );
|
|
if( iter!=current_outputs.end() )
|
|
{
|
|
iter->second.second.append(buf, bsize);
|
|
}
|
|
else
|
|
{
|
|
Log("Couldn't find THREAD_ID - cached=false", LL_ERROR);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void CServer::Write(THREAD_ID tid, const std::string &str, bool cached)
|
|
{
|
|
WriteRaw(tid, str.c_str(), str.size(), cached);
|
|
}
|
|
|
|
bool CServer::UnloadDLLs(void)
|
|
{
|
|
UnloadDLLs2();
|
|
return true;
|
|
}
|
|
|
|
void CServer::ShutdownPlugins(void)
|
|
{
|
|
for(std::map<std::string, UNLOADACTIONS>::iterator iter=unload_functs.begin();
|
|
iter!=unload_functs.end();++iter)
|
|
{
|
|
if(iter->second!=NULL)
|
|
iter->second();
|
|
}
|
|
unload_functs.clear();
|
|
}
|
|
|
|
bool CServer::UnloadDLL(const std::string &name)
|
|
{
|
|
std::map<std::string, UNLOADACTIONS>::iterator iter=unload_functs.find(name);
|
|
if(iter!=unload_functs.end() )
|
|
{
|
|
if( iter->second!=NULL )
|
|
{
|
|
iter->second();
|
|
unload_functs.erase(iter);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void CServer::destroy(IObject *obj)
|
|
{
|
|
obj->Remove();
|
|
}
|
|
|
|
ITemplate* CServer::createTemplate(std::string pFile)
|
|
{
|
|
return new CTemplate(pFile);
|
|
}
|
|
|
|
|
|
THREAD_ID CServer::getThreadID(void)
|
|
{
|
|
#ifdef THREAD_BOOST
|
|
IScopedLock lock(thread_mutex);
|
|
|
|
boost::thread::id ct=boost::this_thread::get_id();
|
|
std::map<boost::thread::id, THREAD_ID>::iterator iter=threads.find(ct);
|
|
|
|
if(iter!=threads.end() )
|
|
{
|
|
return iter->second;
|
|
}
|
|
|
|
++curr_thread_id;
|
|
if( curr_thread_id>10000 )
|
|
curr_thread_id=0;
|
|
|
|
threads.insert( std::pair<boost::thread::id, THREAD_ID>( ct, curr_thread_id) );
|
|
|
|
return curr_thread_id;
|
|
#else
|
|
#ifdef _WIN32
|
|
#else
|
|
IScopedLock lock(thread_mutex);
|
|
|
|
pthread_t ct=pthread_self();
|
|
std::map<pthread_t, THREAD_ID>::iterator iter=threads.find(ct);
|
|
|
|
if(iter!=threads.end() )
|
|
{
|
|
return iter->second;
|
|
}
|
|
|
|
++curr_thread_id;
|
|
if( curr_thread_id>100000 )
|
|
curr_thread_id=0;
|
|
|
|
threads.insert( std::pair<pthread_t, THREAD_ID>( ct, curr_thread_id) );
|
|
|
|
return curr_thread_id;
|
|
#endif
|
|
#endif //THREAD_BOOST
|
|
}
|
|
|
|
bool CServer::openDatabase(std::string pFile, DATABASE_ID pIdentifier)
|
|
{
|
|
IScopedLock lock(db_mutex);
|
|
|
|
std::fstream c(pFile.c_str(), std::ios::in);
|
|
if( c.is_open()==false )
|
|
return false;
|
|
|
|
c.close();
|
|
|
|
std::map<DATABASE_ID, std::pair<std::string, std::map<THREAD_ID, CDatabase*> > >::iterator iter=databases.find(pIdentifier);
|
|
if( iter!=databases.end() )
|
|
return false;
|
|
|
|
std::map<THREAD_ID, CDatabase*> thread_map;
|
|
std::pair<std::string, std::map<THREAD_ID, CDatabase*> > tpair(pFile, thread_map);
|
|
databases.insert( std::pair<DATABASE_ID, std::pair<std::string, std::map<THREAD_ID, CDatabase*> > >(pIdentifier, tpair) );
|
|
|
|
return true;
|
|
}
|
|
|
|
IDatabase* CServer::getDatabase(THREAD_ID tid, DATABASE_ID pIdentifier)
|
|
{
|
|
IScopedLock lock(db_mutex);
|
|
|
|
std::map<DATABASE_ID, std::pair<std::string, std::map<THREAD_ID, CDatabase*> > >::iterator database_iter=databases.find(pIdentifier);
|
|
if( database_iter==databases.end() )
|
|
{
|
|
Log("Database with identifier \""+nconvert((int)pIdentifier)+"\" couldn't be opened", LL_ERROR);
|
|
return NULL;
|
|
}
|
|
|
|
std::map<THREAD_ID, CDatabase*>::iterator thread_iter=database_iter->second.second.find( tid );
|
|
if( thread_iter==database_iter->second.second.end() )
|
|
{
|
|
CDatabase *db=new CDatabase;
|
|
if(db->Open(database_iter->second.first)==false )
|
|
{
|
|
Log("Database \""+database_iter->second.first+"\" couldn't be opened", LL_ERROR);
|
|
return NULL;
|
|
}
|
|
|
|
database_iter->second.second.insert( std::pair< THREAD_ID, CDatabase* >( tid, db ) );
|
|
|
|
return db;
|
|
}
|
|
else
|
|
{
|
|
return thread_iter->second;
|
|
}
|
|
}
|
|
|
|
void CServer::ClearDatabases(THREAD_ID tid)
|
|
{
|
|
IScopedLock lock(db_mutex);
|
|
|
|
for(std::map<DATABASE_ID, std::pair<std::string, std::map<THREAD_ID, CDatabase*> > >::iterator i=databases.begin();
|
|
i!=databases.end();++i)
|
|
{
|
|
std::map<THREAD_ID, CDatabase*>::iterator iter=i->second.second.find(tid);
|
|
if( iter!=i->second.second.end() )
|
|
{
|
|
iter->second->destroyAllQueries();
|
|
}
|
|
}
|
|
}
|
|
|
|
void CServer::setContentType(THREAD_ID tid, const std::string &str)
|
|
{
|
|
{
|
|
IScopedLock lock(outputs_mutex);
|
|
|
|
std::pair<bool, std::string> *co=¤t_outputs[tid];
|
|
std::string *curr_output=&co->second;
|
|
|
|
if( curr_output->find("Content-type: ")==0 )
|
|
{
|
|
*curr_output=strdelete("Content-type: "+getbetween("Content-type: ","\r\n\r\n", *curr_output)+"\r\n\r\n", *curr_output);
|
|
}
|
|
|
|
if(curr_output->find("\r\n\r\n")!=std::string::npos )
|
|
{
|
|
curr_output->insert(0, "Content-type: "+str+"\r\n");
|
|
}
|
|
else
|
|
{
|
|
curr_output->insert(0, "Content-type: "+str+"\r\n\r\n");
|
|
}
|
|
|
|
co->first=true;
|
|
}
|
|
}
|
|
|
|
void CServer::addHeader(THREAD_ID tid, const std::string &str)
|
|
{
|
|
{
|
|
IScopedLock lock(outputs_mutex);
|
|
|
|
std::pair<bool, std::string> *co=¤t_outputs[tid];
|
|
std::string *curr_output=&co->second;
|
|
|
|
std::string tadd=str;
|
|
|
|
if( curr_output->find("\r\n\r\n")!=std::string::npos )
|
|
{
|
|
tadd+="\r\n";
|
|
}
|
|
else
|
|
{
|
|
tadd+="\r\n\r\n";
|
|
}
|
|
|
|
curr_output->insert(0, tadd);
|
|
co->first=true;
|
|
}
|
|
}
|
|
|
|
ISessionMgr *CServer::getSessionMgr(void)
|
|
{
|
|
return sessmgr;
|
|
}
|
|
|
|
std::string CServer::GenerateHexMD5(const std::string &input)
|
|
{
|
|
MD5 md((unsigned char*)input.c_str() );
|
|
char *p=md.hex_digest();
|
|
std::string ret=p;
|
|
delete []p;
|
|
return ret;
|
|
}
|
|
|
|
std::string CServer::GenerateBinaryMD5(const std::string &input)
|
|
{
|
|
MD5 md((unsigned char*)input.c_str() );
|
|
unsigned char *p=md.raw_digest();
|
|
std::string ret;
|
|
ret.resize(16);
|
|
for(size_t i=0;i<16;++i)
|
|
ret[i]=p[i];
|
|
delete []p;
|
|
return ret;
|
|
}
|
|
|
|
std::string CServer::GenerateHexMD5(const std::wstring &input)
|
|
{
|
|
unsigned int *tmp=new unsigned int[input.size()];
|
|
for(size_t i=0,l=input.size();i<l;++i)
|
|
{
|
|
tmp[i]=input[i];
|
|
}
|
|
MD5 md((unsigned char*)tmp, (unsigned int)input.size()*sizeof(unsigned int) );
|
|
char *p=md.hex_digest();
|
|
std::string ret=p;
|
|
delete []p;
|
|
delete []tmp;
|
|
return ret;
|
|
}
|
|
|
|
std::string CServer::GenerateBinaryMD5(const std::wstring &input)
|
|
{
|
|
unsigned int *tmp=new unsigned int[input.size()];
|
|
for(size_t i=0,l=input.size();i<l;++i)
|
|
{
|
|
tmp[i]=input[i];
|
|
}
|
|
MD5 md((unsigned char*)tmp, (unsigned int)input.size()*sizeof(unsigned int) );
|
|
unsigned char *p=md.raw_digest();
|
|
std::string ret;
|
|
ret.resize(16);
|
|
for(size_t i=0;i<16;++i)
|
|
ret[i]=p[i];
|
|
delete []p;
|
|
delete []tmp;
|
|
return ret;
|
|
}
|
|
|
|
|
|
void CServer::StartCustomStreamService(IService *pService, std::string pServiceName, unsigned short pPort)
|
|
{
|
|
CServiceAcceptor *acc=new CServiceAcceptor(pService, pServiceName, pPort);
|
|
Server->createThread(acc);
|
|
|
|
stream_services.push_back( acc );
|
|
}
|
|
|
|
IPipe* CServer::ConnectStream(std::string pServer, unsigned short pPort, unsigned int pTimeoutms)
|
|
{
|
|
sockaddr_in server;
|
|
LookupBlocking(pServer, &server.sin_addr);
|
|
server.sin_port=htons(pPort);
|
|
server.sin_family=AF_INET;
|
|
|
|
SOCKET s=socket(AF_INET, SOCK_STREAM, 0);
|
|
if(s==SOCKET_ERROR)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
u_long nonBlocking=1;
|
|
ioctlsocket(s,FIONBIO,&nonBlocking);
|
|
#else
|
|
fcntl(s,F_SETFL,fcntl(s, F_GETFL, 0) | O_NONBLOCK);
|
|
#endif
|
|
|
|
int rc=connect(s, (sockaddr*)&server, sizeof(sockaddr_in) );
|
|
#ifndef _WIN32
|
|
if(rc==SOCKET_ERROR)
|
|
{
|
|
if(errno!=EINPROGRESS)
|
|
{
|
|
closesocket(s);
|
|
Server->Log("errno !=EINPROGRESS. Connect failed...", LL_DEBUG);
|
|
return NULL;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return new CStreamPipe(s);
|
|
}
|
|
#else
|
|
if(rc!=SOCKET_ERROR)
|
|
{
|
|
return new CStreamPipe(s);
|
|
}
|
|
#endif
|
|
|
|
fd_set conn;
|
|
FD_ZERO(&conn);
|
|
FD_SET(s, &conn);
|
|
|
|
timeval lon;
|
|
lon.tv_sec=(long)(pTimeoutms/1000);
|
|
lon.tv_usec=(long)(pTimeoutms%1000)*1000;
|
|
|
|
rc=select((int)s+1,NULL,&conn,NULL,&lon);
|
|
|
|
if( rc>0 && FD_ISSET(s, &conn) )
|
|
{
|
|
int err;
|
|
socklen_t len=sizeof(int);
|
|
rc=getsockopt(s, SOL_SOCKET, SO_ERROR, (char*)&err, &len);
|
|
if(rc<0)
|
|
{
|
|
closesocket(s);
|
|
Server->Log("Error getting socket status.", LL_ERROR);
|
|
return NULL;
|
|
}
|
|
if(err)
|
|
{
|
|
closesocket(s);
|
|
Server->Log("Socket has error: "+nconvert(err), LL_INFO);
|
|
return NULL;
|
|
}
|
|
else
|
|
{
|
|
#ifdef _WIN32
|
|
int window_size=512*1024;
|
|
setsockopt(s, SOL_SOCKET, SO_SNDBUF, (char *) &window_size, sizeof(window_size));
|
|
setsockopt(s, SOL_SOCKET, SO_RCVBUF, (char *) &window_size, sizeof(window_size));
|
|
#endif
|
|
return new CStreamPipe(s);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
void CServer::DisconnectStream(IPipe *pipe)
|
|
{
|
|
CStreamPipe *sp=(CStreamPipe*)pipe;
|
|
SOCKET s=sp->getSocket();
|
|
closesocket(s);
|
|
}
|
|
|
|
bool CServer::RegisterPluginPerThreadModel(IPluginMgr *pPluginMgr, std::string pName)
|
|
{
|
|
IScopedLock lock(plugin_mutex);
|
|
|
|
std::map<std::string, IPluginMgr*>::iterator iter=perthread_pluginmgrs.find( pName );
|
|
|
|
if( iter!= perthread_pluginmgrs.end() )
|
|
return false;
|
|
|
|
perthread_pluginmgrs.insert( std::pair<std::string, IPluginMgr*>( pName, pPluginMgr ) );
|
|
return true;
|
|
}
|
|
|
|
bool CServer::RegisterPluginThreadsafeModel(IPluginMgr *pPluginMgr, std::string pName)
|
|
{
|
|
IScopedLock lock(plugin_mutex);
|
|
|
|
std::map<std::string, IPluginMgr*>::iterator iter=threadsafe_pluginmgrs.find( pName );
|
|
|
|
if( iter!= threadsafe_pluginmgrs.end() )
|
|
return false;
|
|
|
|
threadsafe_pluginmgrs.insert( std::pair<std::string, IPluginMgr*>( pName, pPluginMgr ) );
|
|
return true;
|
|
}
|
|
|
|
PLUGIN_ID CServer::StartPlugin(std::string pName, str_map ¶ms)
|
|
{
|
|
IScopedLock lock(plugin_mutex);
|
|
{
|
|
std::map<std::string, IPluginMgr*>::iterator iter=perthread_pluginmgrs.find( pName );
|
|
|
|
if( iter!=perthread_pluginmgrs.end() )
|
|
{
|
|
++curr_pluginid;
|
|
std::pair<IPluginMgr*, str_map> tmp(iter->second, params);
|
|
perthread_pluginparams.insert( std::pair<PLUGIN_ID, std::pair<IPluginMgr*, str_map> >(curr_pluginid, tmp) );
|
|
return curr_pluginid;
|
|
}
|
|
}
|
|
{
|
|
std::map<std::string, IPluginMgr*>::iterator iter1=threadsafe_pluginmgrs.find( pName );
|
|
|
|
if( iter1!=threadsafe_pluginmgrs.end() )
|
|
{
|
|
++curr_pluginid;
|
|
IPlugin *plugin=iter1->second->createPluginInstance( params );
|
|
threadsafe_plugins.insert( std::pair<PLUGIN_ID, IPlugin*>( curr_pluginid, plugin) );
|
|
return curr_pluginid;
|
|
}
|
|
}
|
|
|
|
return ILLEGAL_PLUGIN_ID;
|
|
}
|
|
|
|
bool CServer::RestartPlugin(PLUGIN_ID pIdentifier)
|
|
{
|
|
IScopedLock lock(plugin_mutex);
|
|
{
|
|
std::map<PLUGIN_ID, std::map<THREAD_ID, IPlugin*> >::iterator iter1=perthread_plugins.find( pIdentifier );
|
|
|
|
if( iter1!=perthread_plugins.end() )
|
|
{
|
|
bool ret=true;
|
|
for( std::map<THREAD_ID, IPlugin*>::iterator iter2=iter1->second.begin();iter2!=iter1->second.end();++iter2)
|
|
{
|
|
bool b=iter2->second->Reload();
|
|
if( b==false )
|
|
ret=false;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
{
|
|
std::map<PLUGIN_ID, IPlugin*>::iterator iter1=threadsafe_plugins.find( pIdentifier );
|
|
|
|
if( iter1!=threadsafe_plugins.end() )
|
|
{
|
|
return iter1->second->Reload();
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
IPlugin* CServer::getPlugin(THREAD_ID tid, PLUGIN_ID pIdentifier)
|
|
{
|
|
IScopedLock lock(plugin_mutex);
|
|
{
|
|
std::map<PLUGIN_ID, IPlugin*>::iterator iter1=threadsafe_plugins.find( pIdentifier );
|
|
|
|
if( iter1!=threadsafe_plugins.end() )
|
|
{
|
|
return iter1->second;
|
|
}
|
|
}
|
|
|
|
{
|
|
std::map<PLUGIN_ID, std::pair<IPluginMgr*,str_map> >::iterator iter1=perthread_pluginparams.find( pIdentifier );
|
|
|
|
if( iter1!= perthread_pluginparams.end() )
|
|
{
|
|
std::map<THREAD_ID, IPlugin*> *pmap=&perthread_plugins[pIdentifier];
|
|
|
|
std::map<THREAD_ID, IPlugin*>::iterator iter2=pmap->find(tid);
|
|
|
|
if( iter2==pmap->end() )
|
|
{
|
|
IPlugin* newplugin=iter1->second.first->createPluginInstance( iter1->second.second);
|
|
pmap->insert( std::pair<THREAD_ID, IPlugin*>( tid, newplugin) );
|
|
newplugin->Reset();
|
|
return newplugin;
|
|
}
|
|
else
|
|
{
|
|
iter2->second->Reset();
|
|
return iter2->second;
|
|
}
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
IMutex* CServer::createMutex(void)
|
|
{
|
|
return new CMutex;
|
|
}
|
|
|
|
IPipe *CServer::createMemoryPipe(void)
|
|
{
|
|
return new CMemoryPipe;
|
|
}
|
|
|
|
#ifdef THREAD_BOOST
|
|
#else
|
|
#ifndef _WIN32
|
|
void *thread_helper_f(void * t)
|
|
{
|
|
IThread *tmp=(IThread*)t;
|
|
(*tmp)();
|
|
}
|
|
#endif
|
|
#endif //THREAD_BOOST
|
|
|
|
void CServer::createThread(IThread *thread)
|
|
{
|
|
#ifdef THREAD_BOOST
|
|
boost::thread tr(boost::ref(*thread));
|
|
tr.yield();
|
|
#else
|
|
#ifdef _WIN32
|
|
|
|
#else
|
|
pthread_t t;
|
|
pthread_create(&t, NULL, &thread_helper_f, (void*)thread);
|
|
#endif
|
|
#endif //THREAD_BOOST
|
|
}
|
|
|
|
IThreadPool *CServer::getThreadPool(void)
|
|
{
|
|
return threadpool;
|
|
}
|
|
|
|
ISettingsReader* CServer::createFileSettingsReader(std::string pFile)
|
|
{
|
|
return new CFileSettingsReader(pFile);
|
|
}
|
|
|
|
ISettingsReader* CServer::createDBSettingsReader(THREAD_ID tid, DATABASE_ID pIdentifier, const std::string &pTable, const std::string &pSQL)
|
|
{
|
|
return new CDBSettingsReader(tid, pIdentifier, pTable, pSQL);
|
|
}
|
|
|
|
ISettingsReader* CServer::createDBSettingsReader(IDatabase *db, const std::string &pTable, const std::string &pSQL)
|
|
{
|
|
return new CDBSettingsReader(db, pTable, pSQL);
|
|
}
|
|
|
|
ISettingsReader* CServer::createMemorySettingsReader(const std::string &pData)
|
|
{
|
|
return new CMemorySettingsReader(pData);
|
|
}
|
|
|
|
void CServer::wait(unsigned int ms)
|
|
{
|
|
#ifdef _WIN32
|
|
Sleep(ms);
|
|
#else
|
|
usleep(ms*1000);
|
|
#endif
|
|
}
|
|
|
|
unsigned int CServer::getNumRequests(void)
|
|
{
|
|
IScopedLock lock(rps_mutex);
|
|
unsigned int ret=num_requests;
|
|
num_requests=0;
|
|
return ret;
|
|
}
|
|
|
|
void CServer::addRequest(void)
|
|
{
|
|
IScopedLock lock(rps_mutex);
|
|
++num_requests;
|
|
}
|
|
|
|
IFile* CServer::openFile(std::string pFilename, int pMode)
|
|
{
|
|
return openFile(widen(pFilename), pMode);
|
|
}
|
|
|
|
IFile* CServer::openFile(std::wstring pFilename, int pMode)
|
|
{
|
|
File *file=new File;
|
|
if(!file->Open(pFilename, pMode) )
|
|
{
|
|
delete file;
|
|
return NULL;
|
|
}
|
|
return file;
|
|
}
|
|
|
|
IFile* CServer::openTemporaryFile(void)
|
|
{
|
|
File *file=new File;
|
|
if(!file->OpenTemporaryFile(tmpdir) )
|
|
{
|
|
Server->Log("Error creating temporary file", LL_ERROR);
|
|
delete file;
|
|
return NULL;
|
|
}
|
|
return file;
|
|
}
|
|
|
|
IFile* CServer::openMemoryFile(void)
|
|
{
|
|
//return new CMemoryFile();
|
|
return openTemporaryFile();
|
|
}
|
|
|
|
bool CServer::deleteFile(std::string pFilename)
|
|
{
|
|
return DeleteFileInt(pFilename);
|
|
}
|
|
|
|
bool CServer::deleteFile(std::wstring pFilename)
|
|
{
|
|
return DeleteFileInt(pFilename);
|
|
}
|
|
|
|
std::string CServer::ConvertToUTF8(const std::wstring &input)
|
|
{
|
|
std::string ret;
|
|
try
|
|
{
|
|
if(sizeof(wchar_t)==2 )
|
|
utf8::utf16to8(input.begin(), input.end(), back_inserter(ret));
|
|
else
|
|
utf8::utf32to8(input.begin(), input.end(), back_inserter(ret));
|
|
}
|
|
catch(...){}
|
|
return ret;
|
|
}
|
|
|
|
std::wstring CServer::ConvertToUnicode(const std::string &input)
|
|
{
|
|
std::wstring ret;
|
|
try
|
|
{
|
|
if(sizeof(wchar_t)==2)
|
|
utf8::utf8to16(input.begin(), input.end(), back_inserter(ret));
|
|
else
|
|
utf8::utf8to32(input.begin(), input.end(), back_inserter(ret));
|
|
}
|
|
catch(...){}
|
|
|
|
return ret;
|
|
}
|
|
|
|
std::string CServer::ConvertToUTF16(const std::wstring &input)
|
|
{
|
|
std::string ret;
|
|
try
|
|
{
|
|
if(sizeof(wchar_t)==2)
|
|
{
|
|
ret.resize(input.size()*2);
|
|
memcpy(&ret[0], &input[0], input.size()*2);
|
|
}
|
|
else
|
|
{
|
|
std::string utf8=ConvertToUTF8(input);
|
|
std::vector<utf8::uint16_t> tmp;
|
|
utf8::utf8to16(utf8.begin(), utf8.end(), back_inserter(tmp) );
|
|
ret.resize(tmp.size()*2);
|
|
memcpy(&ret[0], &tmp[0], tmp.size()*2);
|
|
}
|
|
}
|
|
catch(...){}
|
|
|
|
return ret;
|
|
}
|
|
|
|
std::string CServer::ConvertToUTF32(const std::wstring &input)
|
|
{
|
|
std::string ret;
|
|
try
|
|
{
|
|
if(sizeof(wchar_t)==4)
|
|
{
|
|
ret.resize(input.size()*4);
|
|
memcpy(&ret[0], &input[0], input.size()*4);
|
|
}
|
|
else
|
|
{
|
|
std::string utf8=ConvertToUTF8(input);
|
|
std::vector<utf8::uint32_t> tmp;
|
|
utf8::utf8to32(utf8.begin(), utf8.end(), back_inserter(tmp) );
|
|
ret.resize(tmp.size()*4);
|
|
memcpy(&ret[0], &tmp[0], tmp.size()*4);
|
|
}
|
|
}
|
|
catch(...){}
|
|
|
|
return ret;
|
|
}
|
|
|
|
std::wstring CServer::ConvertFromUTF16(const std::string &input)
|
|
{
|
|
std::wstring ret;
|
|
try
|
|
{
|
|
if(sizeof(wchar_t)==2)
|
|
{
|
|
ret.resize(input.size()/2);
|
|
memcpy(&ret[0], &input[0], input.size());
|
|
}
|
|
else
|
|
{
|
|
if(input.empty())
|
|
{
|
|
return L"";
|
|
}
|
|
else
|
|
{
|
|
std::string tmp;
|
|
utf8::utf16to8((utf8::uint16_t*)&input[0], (utf8::uint16_t*)(&input[input.size()-1]+1), back_inserter(tmp));
|
|
ret=ConvertToUnicode(tmp);
|
|
}
|
|
}
|
|
}
|
|
catch(...){}
|
|
|
|
return ret;
|
|
}
|
|
|
|
std::wstring CServer::ConvertFromUTF32(const std::string &input)
|
|
{
|
|
std::wstring ret;
|
|
try
|
|
{
|
|
if(sizeof(wchar_t)==4)
|
|
{
|
|
ret.resize(input.size()/4);
|
|
memcpy(&ret[0], &input[0], input.size());
|
|
}
|
|
else
|
|
{
|
|
if(input.empty())
|
|
{
|
|
return L"";
|
|
}
|
|
else
|
|
{
|
|
std::string tmp;
|
|
utf8::utf32to8((utf8::uint32_t*)&input[0], (utf8::uint32_t*)(&input[input.size()-1]+1), back_inserter(tmp));
|
|
ret=ConvertToUnicode(tmp);
|
|
}
|
|
}
|
|
}
|
|
catch(...){}
|
|
|
|
return ret;
|
|
}
|
|
|
|
ICondition* CServer::createCondition(void)
|
|
{
|
|
return new CCondition();
|
|
}
|
|
|
|
void CServer::addPostFile(POSTFILE_KEY pfkey, const std::string &name, const SPostfile &pf)
|
|
{
|
|
IScopedLock lock(postfiles_mutex);
|
|
postfiles[pfkey][name]=pf;
|
|
}
|
|
|
|
SPostfile CServer::getPostFile(POSTFILE_KEY pfkey, const std::string &name)
|
|
{
|
|
IScopedLock lock(postfiles_mutex);
|
|
std::map<THREAD_ID, std::map<std::string, SPostfile > >::iterator iter1=postfiles.find(pfkey);
|
|
if(iter1!=postfiles.end())
|
|
{
|
|
std::map<std::string, SPostfile >::iterator iter2=iter1->second.find(name);
|
|
if(iter2!=iter1->second.end() )
|
|
{
|
|
return iter2->second;
|
|
}
|
|
}
|
|
return SPostfile();
|
|
}
|
|
|
|
void CServer::clearPostFiles(POSTFILE_KEY pfkey)
|
|
{
|
|
IScopedLock lock(postfiles_mutex);
|
|
|
|
std::map<THREAD_ID, std::map<std::string, SPostfile > >::iterator iter1=postfiles.find(pfkey);
|
|
if(iter1!=postfiles.end())
|
|
{
|
|
for(std::map<std::string, SPostfile >::iterator iter2=iter1->second.begin();iter2!=iter1->second.end();++iter2)
|
|
{
|
|
destroy(iter2->second.file);
|
|
}
|
|
postfiles.erase(iter1);
|
|
}
|
|
}
|
|
|
|
POSTFILE_KEY CServer::getPostFileKey()
|
|
{
|
|
IScopedLock lock(postfiles_mutex);
|
|
return curr_postfilekey++;
|
|
}
|
|
|
|
std::wstring CServer::getServerWorkingDir(void)
|
|
{
|
|
return workingdir;
|
|
}
|
|
|
|
void CServer::setServerWorkingDir(const std::wstring &wdir)
|
|
{
|
|
workingdir=wdir;
|
|
}
|
|
|
|
void CServer::setTemporaryDirectory(const std::wstring &dir)
|
|
{
|
|
tmpdir=dir;
|
|
} |