Skip to content
Snippets Groups Projects
Commit c9df4695 authored by Ruud Overeem's avatar Ruud Overeem
Browse files

BugID: 679

Moved startfunction of starter class to startDaemon itself because the function
became very simple after the introduction of APLCommon/ControllerDefines.
Changed name to CTStartDaemon.
parent 7f386a49
No related branches found
No related tags found
No related merge requests found
//# LDStartDaemon.cc: Program that can start others on command.
//# CTStartDaemon.cc: Program that can start others on command.
//#
//# Copyright (C) 2002-2004
//# ASTRON (Netherlands Foundation for Research in Astronomy)
......@@ -21,44 +21,39 @@
//# $Id$
#include <lofar_config.h>
#include <Common/LofarLogger.h>
#include <Common/LofarLocators.h>
#include <GCF/GCF_ServiceInfo.h>
#include <APL/APLCommon/APL_Defines.h>
#include <APL/APLCommon/APLUtilities.h>
#include <APL/APLCommon/StartDaemon_Protocol.ph>
#include <APL/APLCommon/Controller_Protocol.ph>
#include "LogicalDeviceStarter.h"
#include "LDStartDaemon.h"
#include "CTStartDaemon.h"
using namespace LOFAR::GCF::Common;
using namespace LOFAR::GCF::TM;
using namespace LOFAR::ACC::APS;
namespace LOFAR {
namespace CUDaemons {
//
// LDStartDaemon(taskname)
// CTStartDaemon(taskname)
//
LDStartDaemon::LDStartDaemon(const string& name) :
GCFTask ((State)&LDStartDaemon::initial_state, name),
CTStartDaemon::CTStartDaemon(const string& name) :
GCFTask ((State)&CTStartDaemon::initial_state, name),
itsActionList (),
itsActiveCntlrs (),
itsListener (0),
itsListenRetryTimer (0),
itsClients (),
itsStarter (0),
itsTimerPort (0)
{
LOG_TRACE_FLOW(formatString("LDStartDaemon(%s)", getName().c_str()));
LOG_TRACE_FLOW(formatString("CTStartDaemon(%s)", getName().c_str()));
itsListener = new GCFTCPPort(*this, MAC_SVCMASK_STARTDAEMON,
GCFPortInterface::MSPP, STARTDAEMON_PROTOCOL);
ASSERTSTR(itsListener, "Unable to allocate listener port");
itsStarter = new LogicalDeviceStarter(globalParameterSet());
ASSERTSTR(itsStarter, "Unable to allocate starter object");
itsTimerPort = new GCFTimerPort(*this, "TimerPort");
ASSERTSTR(itsTimerPort, "Unable to allocate timer port");
......@@ -67,21 +62,17 @@ LDStartDaemon::LDStartDaemon(const string& name) :
//
// ~LDStartDaemon
// ~CTStartDaemon
//
LDStartDaemon::~LDStartDaemon()
CTStartDaemon::~CTStartDaemon()
{
LOG_TRACE_FLOW(formatString("~LDStartDaemon(%s)", getName().c_str()));
LOG_TRACE_FLOW(formatString("~CTStartDaemon(%s)", getName().c_str()));
if (itsListener) {
itsListener->close();
delete itsListener;
}
if (itsStarter) {
delete itsStarter;
}
if (itsTimerPort) {
delete itsTimerPort;
}
......@@ -96,7 +87,7 @@ LDStartDaemon::~LDStartDaemon()
//
// findAction(port)
//
LDStartDaemon::actionIter LDStartDaemon::findAction(GCFPortInterface* aPort)
CTStartDaemon::actionIter CTStartDaemon::findAction(GCFPortInterface* aPort)
{
actionIter iter = itsActionList.begin();
actionIter end = itsActionList.end();
......@@ -110,7 +101,7 @@ LDStartDaemon::actionIter LDStartDaemon::findAction(GCFPortInterface* aPort)
//
// findAction(timerID)
//
LDStartDaemon::actionIter LDStartDaemon::findAction(uint32 aTimerID)
CTStartDaemon::actionIter CTStartDaemon::findAction(uint32 aTimerID)
{
actionIter iter = itsActionList.begin();
actionIter end = itsActionList.end();
......@@ -124,7 +115,7 @@ LDStartDaemon::actionIter LDStartDaemon::findAction(uint32 aTimerID)
//
// findAction(cntlrName)
//
LDStartDaemon::actionIter LDStartDaemon::findAction(const string& cntlrName)
CTStartDaemon::actionIter CTStartDaemon::findAction(const string& cntlrName)
{
actionIter iter = itsActionList.begin();
actionIter end = itsActionList.end();
......@@ -138,7 +129,7 @@ LDStartDaemon::actionIter LDStartDaemon::findAction(const string& cntlrName)
//
// findController(port)
//
LDStartDaemon::CTiter LDStartDaemon::findController(GCFPortInterface* aPort)
CTStartDaemon::CTiter CTStartDaemon::findController(GCFPortInterface* aPort)
{
CTiter iter = itsActiveCntlrs.begin();
CTiter end = itsActiveCntlrs.end();
......@@ -152,7 +143,7 @@ LDStartDaemon::CTiter LDStartDaemon::findController(GCFPortInterface* aPort)
//
// sendCreatedMsg(action, result)
//
void LDStartDaemon::sendCreatedMsg(actionIter action, int32 result)
void CTStartDaemon::sendCreatedMsg(actionIter action, int32 result)
{
// send customer message that controller is on the air
STARTDAEMONCreatedEvent createdEvent;
......@@ -166,7 +157,7 @@ void LDStartDaemon::sendCreatedMsg(actionIter action, int32 result)
//
// sendNewParentAndCreatedMsg()
//
void LDStartDaemon::sendNewParentAndCreatedMsg(actionIter action)
void CTStartDaemon::sendNewParentAndCreatedMsg(actionIter action)
{
// connection with new controller is made, send 'newparent' message
STARTDAEMONNewparentEvent msg;
......@@ -175,8 +166,9 @@ void LDStartDaemon::sendNewParentAndCreatedMsg(actionIter action)
msg.parentService = action->parentService;
// map controllername to controllerport
CTiter controller = itsActiveCntlrs.find(msg.cntlrName);
ASSERTSTR(isController(controller), msg.cntlrName <<
string adminName = sharedControllerName(msg.cntlrName);
CTiter controller = itsActiveCntlrs.find(adminName);
ASSERTSTR(isController(controller), adminName <<
" not found in controller list");
controller->second->send(msg);
LOG_DEBUG_STR("Sending NewParent(" << msg.cntlrName << "," <<
......@@ -194,7 +186,7 @@ void LDStartDaemon::sendNewParentAndCreatedMsg(actionIter action)
// A disconnect event was receiveed on a client port. Close the port
// and remove the port from our pool.
//
void LDStartDaemon::handleClientDisconnect(GCFPortInterface& port)
void CTStartDaemon::handleClientDisconnect(GCFPortInterface& port)
{
// end TCP connection
port.close();
......@@ -231,6 +223,51 @@ void LDStartDaemon::handleClientDisconnect(GCFPortInterface& port)
}
}
//
// startController(taskname, paramfile)
//
int32 CTStartDaemon::startController(uint16 cntlrType,
const string& cntlrName,
const string& parentHost,
const string& parentService)
{
// not found? report problem
if (cntlrType == CNTLRTYPE_NO_TYPE || cntlrType >= CNTLRTYPE_NR_TYPES) {
LOG_DEBUG_STR("No support for starting controller of the type " << cntlrType);
return (SD_RESULT_UNSUPPORTED_TYPE);
}
// locate program.
ProgramLocator PL;
string executable = PL.locate(getExecutable(cntlrType));
if (executable.empty()) {
LOG_DEBUG_STR("Executable '" << getExecutable(cntlrType) << "' not found.");
return (SD_RESULT_PROGRAM_NOT_FOUND);
}
// construct system command
string startCmd = formatString("./startController.sh %s %s %s %s",
executable.c_str(),
cntlrName.c_str(),
parentHost.c_str(),
parentService.c_str());
LOG_DEBUG_STR("About to start: " << startCmd);
int32 result = system (startCmd.c_str());
LOG_DEBUG_STR ("Result of start = " << result);
if (result == -1) {
return (SD_RESULT_START_FAILED);
}
return (SD_RESULT_NO_ERROR);
}
// -------------------- STATE MACHINES --------------------
//
......@@ -238,10 +275,10 @@ void LDStartDaemon::handleClientDisconnect(GCFPortInterface& port)
//
// The only target in this state is to get the listener port on the air.
//
GCFEvent::TResult LDStartDaemon::initial_state(GCFEvent& event,
GCFEvent::TResult CTStartDaemon::initial_state(GCFEvent& event,
GCFPortInterface& /*port*/)
{
LOG_DEBUG(formatString("LDStartDaemon(%s)::initial_state (%s)",getName().c_str(),evtstr(event)));
LOG_DEBUG(formatString("CTStartDaemon(%s)::initial_state (%s)",getName().c_str(),evtstr(event)));
GCFEvent::TResult status = GCFEvent::HANDLED;
switch (event.signal) {
......@@ -260,7 +297,7 @@ GCFEvent::TResult LDStartDaemon::initial_state(GCFEvent& event,
itsListenRetryTimer = 0;
}
LOG_DEBUG ("Listener port opened, going to operational mode");
TRAN(LDStartDaemon::operational_state);
TRAN(CTStartDaemon::operational_state);
}
break;
......@@ -276,7 +313,7 @@ GCFEvent::TResult LDStartDaemon::initial_state(GCFEvent& event,
break;
default:
LOG_DEBUG(formatString("LDStartDaemon(%s)::initial_state, default",getName().c_str()));
LOG_DEBUG("CTStartDaemon::initial_state, default");
status = GCFEvent::NOT_HANDLED;
break;
}
......@@ -290,11 +327,10 @@ GCFEvent::TResult LDStartDaemon::initial_state(GCFEvent& event,
// This is the normal operational mode. Wait for clients (e.g. MACScheduler) to
// connect, wait for commands from the clients and handle those.
//
GCFEvent::TResult LDStartDaemon::operational_state (GCFEvent& event,
GCFEvent::TResult CTStartDaemon::operational_state (GCFEvent& event,
GCFPortInterface& port)
{
LOG_DEBUG(formatString("LDStartDaemon(%s)::operational_state (%s)",
getName().c_str(),evtstr(event)));
LOG_DEBUG(formatString("operational_state:%s", evtstr(event)));
GCFEvent::TResult status = GCFEvent::HANDLED;
......@@ -349,18 +385,20 @@ GCFEvent::TResult LDStartDaemon::operational_state (GCFEvent& event,
createdEvent.cntlrType = createEvent.cntlrType;
createdEvent.cntlrName = createEvent.cntlrName;
// convert fullname used in SD protocol to sharedname for internal admin
string adminName = sharedControllerName(createEvent.cntlrName);
// is controller already known?
CTiter controller = itsActiveCntlrs.find(createEvent.cntlrName);
CTiter controller = itsActiveCntlrs.find(adminName);
if (!isController(controller)) { // no, controller is not active
// Ask starter Object to start the controller.
createdEvent.result =
itsStarter->startController (createEvent.cntlrType,
createEvent.cntlrName,
createEvent.parentHost,
createEvent.parentService);
createdEvent.result = startController(createEvent.cntlrType,
adminName,
createEvent.parentHost,
createEvent.parentService);
// when creation failed, report it back.
if (createdEvent.result != SD_RESULT_NO_ERROR) {
LOG_WARN_STR("Startup of " << createEvent.cntlrName << " failed");
LOG_WARN_STR("Startup of " << adminName << " failed");
port.send(createdEvent);
break;
}
......@@ -397,7 +435,7 @@ GCFEvent::TResult LDStartDaemon::operational_state (GCFEvent& event,
case STARTDAEMON_ANNOUNCEMENT: {
STARTDAEMONAnnouncementEvent inMsg(event);
// known controller?
// known controller? (announcement msg always contains sharedname).
CTiter controller = itsActiveCntlrs.find(inMsg.cntlrName);
// controller already registered?
if (isController(controller) && controller->second != 0) {
......@@ -432,7 +470,7 @@ GCFEvent::TResult LDStartDaemon::operational_state (GCFEvent& event,
}
default:
LOG_DEBUG(formatString("LDStartDaemon(%s)::operational_state, default",getName().c_str()));
LOG_DEBUG(formatString("CTStartDaemon(%s)::operational_state, default",getName().c_str()));
status = GCFEvent::NOT_HANDLED;
break;
}
......
# Configuration file for the LDStartDaemon.
#
//# LDStartDaemon.h: Server class that creates Logical Devices upon request.
//# CTStartDaemon.h: Server class that creates Logical Devices upon request.
//#
//# Copyright (C) 2002-2005
//# ASTRON (Netherlands Foundation for Research in Astronomy)
......@@ -20,8 +20,8 @@
//#
//# $Id$
#ifndef CUDAEMONS_LDSTARTDAEMON_H
#define CUDAEMONS_LDSTARTDAEMON_H
#ifndef CUDAEMONS_CTSTARTDAEMON_H
#define CUDAEMONS_CTSTARTDAEMON_H
//# Includes
#include <Common/lofar_map.h>
......@@ -31,7 +31,6 @@
#include <GCF/TM/GCF_Task.h>
#include <GCF/TM/GCF_Event.h>
#include <APL/APLCommon/APL_Defines.h>
#include "LogicalDeviceStarter.h"
namespace LOFAR {
......@@ -39,11 +38,11 @@ namespace LOFAR {
using namespace APLCommon;
namespace CUDaemons {
class LDStartDaemon : public GCFTask
class CTStartDaemon : public GCFTask
{
public:
explicit LDStartDaemon(const string& name);
virtual ~LDStartDaemon();
explicit CTStartDaemon(const string& name);
virtual ~CTStartDaemon();
private:
// The state machines of the StartDaemon
......@@ -51,13 +50,13 @@ private:
GCFEvent::TResult operational_state (GCFEvent& e, GCFPortInterface& p);
// protected copy constructor
LDStartDaemon(const LDStartDaemon&);
LDStartDaemon& operator=(const LDStartDaemon&);
CTStartDaemon(const CTStartDaemon&);
CTStartDaemon& operator=(const CTStartDaemon&);
// define a structure for delaying/retrying requests.
typedef struct action {
string cntlrName;
string cntlrType;
uint16 cntlrType;
string parentHost;
string parentService;
GCFPortInterface* parentPort;
......@@ -77,8 +76,13 @@ private:
void sendCreatedMsg (actionIter action, int32 result);
void sendNewParentAndCreatedMsg(actionIter action);
void handleClientDisconnect(GCFPortInterface& port);
int32 startController(uint16 cntlrType,
const string& cntlrName,
const string& parentHost,
const string& parentService);
// define structure to register controller announcements.
// sharedname, port
typedef map<string, GCFPortInterface*> controllerMap;
typedef controllerMap::iterator CTiter;
typedef controllerMap::const_iterator const_CTiter;
......@@ -95,8 +99,6 @@ private:
vector<GCFPortInterface*> itsClients; // the command ports
LogicalDeviceStarter* itsStarter; // the starter object
GCFTimerPort* itsTimerPort; // for internal timers
};
......
//# LDStartDaemon.cc: Main entry for the LogicalDevice startdaemon
//# CTStartDaemon.cc: Main entry for the LogicalDevice startdaemon
//#
//# Copyright (C) 2002-2005
//# ASTRON (Netherlands Foundation for Research in Astronomy)
......@@ -23,7 +23,7 @@
#include <Common/LofarLogger.h>
#include <signal.h>
#include "LDStartDaemon.h"
#include "CTStartDaemon.h"
using namespace LOFAR;
using namespace LOFAR::GCF::Common;
......@@ -34,7 +34,7 @@ int main(int argc, char* argv[])
GCFTask::init(argc, argv);
CUDaemons::LDStartDaemon sd(string("StartDaemon")); // give myself a name
CUDaemons::CTStartDaemon sd(string("StartDaemon")); // give myself a name
sd.start(); // make initial transition
......
# Configuration file for the LDStartDaemon.
#
# Define the characteristics from the Controllers the LDSD can start
# The sequencenumbers in the definition should be contiquous.
#
# Note: the types used in this list should correspond with the types
# defined in APL/APL/Common/APL_Defines.h
#
Controller.1.type = OBS_CTRL
Controller.1.program = ObservationControl
Controller.1.shared = false
Controller.2.type = BEAMDIR_CTRL
Controller.2.program = BeamDirectionControl
Controller.2.shared = true
Controller.3.type = GROUP_CTRL
Controller.3.program = RingControl
Controller.3.shared = true
Controller.4.type = STS_CTRL
Controller.4.program = StationControl
Controller.4.shared = false
Controller.5.type = DIGBOARD_CTRL
Controller.5.program = DigitalBoardControl
Controller.5.shared = false
Controller.6.type = BEAM_CTRL
Controller.6.program = BeamControl
Controller.6.shared = true
Controller.7.type = CAL_CTRL
Controller.7.program = CalibrationControl
Controller.7.shared = true
Controller.8.type = STSINFRA_CTRL
Controller.8.program = StationInfraControl
Controller.8.shared = true
//# LogicalDeviceStarter.cc: one line description
//#
//# Copyright (C) 2006
//# ASTRON (Netherlands Foundation for Research in Astronomy)
//# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands, seg@astron.nl
//#
//# 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 2 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, write to the Free Software
//# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//#
//# $Id$
//# Always #include <lofar_config.h> first!
#include <lofar_config.h>
//# Includes
#include <Common/LofarLogger.h>
#include <Common/LofarLocators.h>
#include <sys/types.h>
#include <unistd.h>
#include <APL/APLCommon/StartDaemon_Protocol.ph>
#include "LogicalDeviceStarter.h"
namespace LOFAR {
namespace CUDaemons {
//
// LogicalDeviceStarter(executablename)
//
LogicalDeviceStarter::LogicalDeviceStarter(ParameterSet* aParSet) :
itsProgramList()
{
string typeMask ("Controller.%d.type");
string programMask("Controller.%d.program");
string sharedMask ("Controller.%d.shared");
LDstart_t startInfo;
try {
for (uint32 counter = 1; ; counter++) {
string typeLabel (formatString(typeMask.c_str(), counter));
string programLabel(formatString(programMask.c_str(), counter));
string sharedLabel (formatString(sharedMask.c_str(), counter));
startInfo.name = aParSet->getString(typeLabel);
startInfo.executable = aParSet->getString(programLabel);
startInfo.shared = aParSet->getBool (sharedLabel);
itsProgramList.push_back(startInfo);
}
}
catch (Exception& e) {
// expected throw: when counter > seqnr used in parameterSet.
}
ASSERTSTR(itsProgramList.size() > 0, "No definitions of Controllers found "
"in the parameterSet.");
LOG_DEBUG_STR("Found " << itsProgramList.size() << " ControllerTypes");
}
//
// ~LogicalDeviceStarter
//
LogicalDeviceStarter::~LogicalDeviceStarter()
{
itsProgramList.clear();
}
//
// createLogicalDevice(taskname, paramfile)
//
int32 LogicalDeviceStarter::startController(const string& ldTypeName,
const string& taskname,
const string& parentHost,
const string& parentService)
{
// search controller type in table.
uint32 nrDevices = itsProgramList.size();
uint32 index = 0;
while (index < nrDevices && itsProgramList[index].name != ldTypeName){
index++;
}
// not found? report problem
if (index >= nrDevices) {
LOG_DEBUG_STR("No support for starting controller of the "
"type " << ldTypeName << ". See config file.");
return (SD_RESULT_UNSUPPORTED_TYPE);
}
// shared device? report success
if (itsProgramList[index].shared) {
LOG_DEBUG_STR("Controller of type " << ldTypeName << " is shared, no start.");
return (SD_RESULT_NO_ERROR);
}
// locate program.
ProgramLocator PL;
string executable = PL.locate(itsProgramList[index].executable);
if (executable.empty()) {
LOG_DEBUG_STR("Executable for controller " << ldTypeName << " not found.");
return (SD_RESULT_PROGRAM_NOT_FOUND);
}
// construct system command
string startCmd = formatString("./startController.sh %s %s %s %s",
executable.c_str(),
taskname.c_str(),
parentHost.c_str(),
parentService.c_str());
LOG_DEBUG_STR("About to start: " << startCmd);
int32 result = system (startCmd.c_str());
LOG_DEBUG_STR ("Result of start = " << result);
if (result == -1) {
return (SD_RESULT_START_FAILED);
}
return (SD_RESULT_NO_ERROR);
}
} // namespace CUDaemons
} // namespace LOFAR
//# LogicalDeviceStarter.h: one line description
//#
//# Copyright (C) 2006
//# ASTRON (Netherlands Foundation for Research in Astronomy)
//# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands, seg@astron.nl
//#
//# 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 2 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, write to the Free Software
//# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//#
//# $Id$
#ifndef LOFAR_CUDAEMONS_LOGICALDEVICESTARTER_H
#define LOFAR_CUDAEMONS_LOGICALDEVICESTARTER_H
// \file
// one line description.
//# Never #include <config.h> or #include <lofar_config.h> in a header file!
//# Includes
#include <APS/ParameterSet.h>
namespace LOFAR {
using ACC::APS::ParameterSet;
namespace CUDaemons {
// \addtogroup CUDaemons
// @{
//# Forward Declarations
//#class forward;
// Description of class.
class LogicalDeviceStarter
{
public:
explicit LogicalDeviceStarter (ParameterSet* aParSet);
~LogicalDeviceStarter();
int32 startController (const string& ldTypeName,
const string& taskname,
const string& parentHost,
const string& parentService);
private:
// Copying is not allowed
LogicalDeviceStarter();
LogicalDeviceStarter (const LogicalDeviceStarter& that);
LogicalDeviceStarter& operator= (const LogicalDeviceStarter& that);
private:
//# Datamembers
typedef struct {
string name;
string executable;
bool shared;
} LDstart_t;
vector<LDstart_t> itsProgramList;
};
// @}
} // namespace CUDaemons
} // namespace LOFAR
#endif
# Executables
bin_PROGRAMS = LDStartDaemon
bin_PROGRAMS = CTStartDaemon
LDStartDaemon_CPPFLAGS = -DBOOST_DISABLE_THREADS \
CTStartDaemon_CPPFLAGS = -DBOOST_DISABLE_THREADS \
-Wno-deprecated \
-fmessage-length=0 \
-fdiagnostics-show-location=once
LDStartDaemon_SOURCES = LogicalDeviceStarter.cc \
LDStartDaemon.cc \
LDStartDaemonMain.cc
LDStartDaemon_LDADD = $(LOFAR_DEPEND)
LDStartDaemon_DEPENDENCIES = $(LOFAR_DEPEND)
CTStartDaemon_SOURCES = CTStartDaemon.cc \
CTStartDaemonMain.cc
CTStartDaemon_LDADD = $(LOFAR_DEPEND)
CTStartDaemon_DEPENDENCIES = $(LOFAR_DEPEND)
bin_SCRIPTS = startController.sh
NOINSTHDRS = LDStartDaemon.h \
LogicalDeviceStarter.h
NOINSTHDRS = CTStartDaemon.h
INSTHDRS =
......@@ -31,8 +29,8 @@ EXTRA_DIST = $(configfiles_DATA) $(sysconf_DATA)
configfilesdir=$(bindir)
configfiles_DATA =
sysconf_DATA = LDStartDaemon.conf \
LDStartDaemon.log_prop
sysconf_DATA = CTStartDaemon.conf \
CTStartDaemon.log_prop
%.log_prop: %.log_prop.in
cp $< $@
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment