Skip to content

Commit 291f5de

Browse files
committed
fixed timeout problem
1 parent f29f3f0 commit 291f5de

File tree

7 files changed

+140
-36
lines changed

7 files changed

+140
-36
lines changed

EventVisualisation/DataConverter/include/EventVisualisationDataConverter/Location.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ namespace o2::event_visualisation
2626
struct LocationParams {
2727
std::string fileName;
2828
int port = -1;
29+
int timeout = 100;
2930
std::string host = "localhost";
3031
bool toFile = true;
3132
bool toSocket = true;
@@ -38,6 +39,7 @@ class Location
3839
bool mToSocket;
3940
std::string mFileName;
4041
int mPort;
42+
int mTimeout;
4143
std::string mHostName;
4244

4345
public:
@@ -50,6 +52,7 @@ class Location
5052
this->mPort = params.port;
5153
this->mHostName = params.host;
5254
this->mClientSocket = -1;
55+
this->mTimeout = params.timeout;
5356
}
5457
~Location()
5558
{

EventVisualisation/DataConverter/src/Location.cxx

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,78 @@
1919
#include <sys/socket.h>
2020
#include <unistd.h>
2121
#include <netdb.h>
22+
#include <fcntl.h>
23+
#include <poll.h>
24+
#include <ctime>
2225

2326
using namespace std;
27+
2428
namespace o2::event_visualisation
2529
{
30+
31+
int connect_with_timeout(const int socket, const struct sockaddr* addr, socklen_t addrlen, const unsigned int timeout_ms)
32+
{
33+
int connection = 0;
34+
// Setting O_NONBLOCK
35+
int socket_flags_before;
36+
if ((socket_flags_before = fcntl(socket, F_GETFL, 0) < 0)) {
37+
return -1;
38+
}
39+
if (fcntl(socket, F_SETFL, socket_flags_before | O_NONBLOCK) < 0) {
40+
return -1;
41+
}
42+
do {
43+
if (connect(socket, addr, addrlen) < 0) {
44+
if ((errno != EWOULDBLOCK) && (errno != EINPROGRESS)) {
45+
connection = -1; // error
46+
} else { // wait for complete
47+
// deadline 'timeout' ms from now
48+
timespec now; // NOLINT(*-pro-type-member-init)
49+
if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) {
50+
connection = -1;
51+
break;
52+
}
53+
const timespec deadline = {.tv_sec = now.tv_sec,
54+
.tv_nsec = now.tv_nsec + timeout_ms * 1000000l};
55+
do {
56+
if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) {
57+
connection = -1;
58+
break;
59+
}
60+
// compute remaining deadline
61+
const int ms_until_deadline = static_cast<int>((deadline.tv_sec - now.tv_sec) * 1000l + (deadline.tv_nsec - now.tv_nsec) / 1000000l);
62+
if (ms_until_deadline < 0) {
63+
connection = 0;
64+
break;
65+
}
66+
pollfd connectionPool[] = {{.fd = socket, .events = POLLOUT}};
67+
connection = poll(connectionPool, 1, ms_until_deadline);
68+
69+
if (connection > 0) { // confirm the success
70+
int error = 0;
71+
socklen_t len = sizeof(error);
72+
if (getsockopt(socket, SOL_SOCKET, SO_ERROR, &error, &len) == 0) {
73+
errno = error;
74+
}
75+
if (error != 0) {
76+
connection = -1;
77+
}
78+
}
79+
} while (connection == -1 && errno == EINTR); // If interrupted, try again.
80+
if (connection == 0) {
81+
errno = ETIMEDOUT;
82+
connection = -1;
83+
}
84+
}
85+
}
86+
} while (false);
87+
// Restore socket state
88+
if (fcntl(socket, F_SETFL, socket_flags_before) < 0) {
89+
return -1;
90+
}
91+
return connection;
92+
}
93+
2694
void Location::open()
2795
{
2896
if (this->mToFile) {
@@ -37,7 +105,7 @@ void Location::open()
37105
// ask once
38106
static auto server = gethostbyname(this->mHostName.c_str());
39107
if (server == nullptr) {
40-
fprintf(stderr, "ERROR, no such host\n");
108+
LOGF(info, "Error no such host %s", this->mHostName.c_str());
41109
return;
42110
};
43111

@@ -52,8 +120,8 @@ void Location::open()
52120
return;
53121
}
54122

55-
if (connect(this->mClientSocket, (struct sockaddr*)&serverAddress,
56-
sizeof(serverAddress)) == -1) {
123+
if (connect_with_timeout(this->mClientSocket, (sockaddr*)&serverAddress,
124+
sizeof(serverAddress), this->mTimeout) == -1) {
57125
LOGF(info, "Error connecting to %s:%d", this->mHostName.c_str(), this->mPort);
58126
::close(this->mClientSocket);
59127
this->mClientSocket = -1;
@@ -97,6 +165,7 @@ void Location::write(char* buf, std::streamsize size)
97165
this->mOut->write(buf, size);
98166
}
99167
if (this->mToSocket && this->mClientSocket != -1) {
168+
LOGF(info, "Location::write() socket %s ++++++++++++++++++++++", fileName());
100169
try {
101170
auto real = send(this->mClientSocket, buf, size, 0);
102171
if (real != size) {
@@ -110,4 +179,4 @@ void Location::write(char* buf, std::streamsize size)
110179
}
111180
}
112181

113-
} // namespace o2::event_visualisation
182+
} // namespace o2::event_visualisation

EventVisualisation/Workflow/include/EveWorkflow/EveWorkflowHelper.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ class EveWorkflowHelper
183183
bool isInsideITSROF(float t);
184184
bool isInsideTimeBracket(float t);
185185

186-
void save(const std::string& jsonPath, const std::string& ext, int numberOfFiles, const std::string& receiverHostname, int receiverPort, bool useOnlyFiles, bool useOnlySockets);
186+
void save(const std::string& jsonPath, const std::string& ext, int numberOfFiles, const std::string& receiverHostname, int receiverPort, int receiverTimeout, bool useOnlyFiles, bool useOnlySockets);
187187

188188
bool mUseTimeBracket = false;
189189
bool mUseEtaBracketTPC = false;

EventVisualisation/Workflow/include/EveWorkflow/O2DPLDisplay.h

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ class TPCFastTransform;
5050
class O2DPLDisplaySpec : public o2::framework::Task
5151
{
5252
public:
53-
static constexpr auto allowedTracks = "ITS,TPC,MFT,MCH,MID,ITS-TPC,TPC-TRD,ITS-TPC-TOF,ITS-TPC-TRD,ITS-TPC-TRD-TOF,MCH-MID,MFT-MCH,MFT-MCH-MID,PHS,EMC,HMP";
53+
static constexpr auto allowedTracks =
54+
"ITS,TPC,MFT,MCH,MID,ITS-TPC,TPC-TRD,ITS-TPC-TOF,ITS-TPC-TRD,ITS-TPC-TRD-TOF,MCH-MID,MFT-MCH,MFT-MCH-MID,PHS,EMC,HMP";
5455
static constexpr auto allowedClusters = "ITS,TPC,TRD,TOF,MFT,MCH,MID,PHS,EMC,HMP";
5556

5657
O2DPLDisplaySpec(bool disableWrite, bool useMC, o2::dataformats::GlobalTrackID::mask_t trkMask,
@@ -63,9 +64,26 @@ class O2DPLDisplaySpec : public o2::framework::Task
6364
bool eveHostNameMatch,
6465
const std::string& receiverHostname,
6566
int receiverPort,
67+
int receiverTimeout,
6668
bool useOnlyFiles,
6769
bool useOnlySockets)
68-
: mDisableWrite(disableWrite), mUseMC(useMC), mTrkMask(trkMask), mClMask(clMask), mDataRequest(dataRequest), mGGCCDBRequest(gr), mEMCALCalibLoader(emcCalibLoader), mJsonPath(jsonPath), mExt(ext), mTimeInterval(timeInterval), mEveHostNameMatch(eveHostNameMatch), mRunType(o2::parameters::GRPECS::NONE), mReceiverHostname(receiverHostname), mReceiverPort(receiverPort), mUseOnlyFiles(useOnlyFiles), mUseOnlySockets(useOnlySockets)
70+
: mDisableWrite(disableWrite),
71+
mUseMC(useMC),
72+
mTrkMask(trkMask),
73+
mClMask(clMask),
74+
mDataRequest(dataRequest),
75+
mGGCCDBRequest(gr),
76+
mEMCALCalibLoader(emcCalibLoader),
77+
mJsonPath(jsonPath),
78+
mExt(ext),
79+
mTimeInterval(timeInterval),
80+
mEveHostNameMatch(eveHostNameMatch),
81+
mRunType(o2::parameters::GRPECS::NONE),
82+
mReceiverHostname(receiverHostname),
83+
mReceiverPort(receiverPort),
84+
mReceiverTimeout(receiverTimeout),
85+
mUseOnlyFiles(useOnlyFiles),
86+
mUseOnlySockets(useOnlySockets)
6987
{
7088
this->mTimeStamp = std::chrono::high_resolution_clock::now() - timeInterval; // first run meets condition
7189
}
@@ -85,7 +103,8 @@ class O2DPLDisplaySpec : public o2::framework::Task
85103
std::string mJsonPath; // folder where files are stored
86104
std::string mExt; // extension of created files (".json" or ".root")
87105
std::chrono::milliseconds mTimeInterval; // minimal interval between files in milliseconds
88-
bool mPrimaryVertexTriggers; // instead of drawing vertices with tracks (and maybe calorimeter triggers), draw vertices with calorimeter triggers (and maybe tracks)
106+
bool mPrimaryVertexTriggers;
107+
// instead of drawing vertices with tracks (and maybe calorimeter triggers), draw vertices with calorimeter triggers (and maybe tracks)
89108
int mEventCounter = 0;
90109
std::chrono::time_point<std::chrono::high_resolution_clock> mTimeStamp;
91110

@@ -101,10 +120,10 @@ class O2DPLDisplaySpec : public o2::framework::Task
101120

102121
std::string mReceiverHostname;
103122
int mReceiverPort;
123+
int mReceiverTimeout;
104124
bool mUseOnlyFiles;
105125
bool mUseOnlySockets;
106126
};
107-
108127
} // namespace o2::event_visualisation
109128

110-
#endif
129+
#endif

EventVisualisation/Workflow/src/AO2DConverter.cxx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ struct AO2DConverter {
7575
mHelper->mEvent.setFirstTForbit(mTfOrbit);
7676
mHelper->mEvent.setCreationTime(collision.collisionTime());
7777
const std::string hostname("localhost");
78-
mHelper->save(jsonPath, ".root", -1, hostname, -1, true, true);
78+
mHelper->save(jsonPath, ".root", -1, hostname, -1, 100, true, true);
7979
mHelper->clear();
8080
}
8181
};

EventVisualisation/Workflow/src/EveWorkflowHelper.cxx

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,10 @@ void EveWorkflowHelper::selectTracks(const CalibObjectsConst* calib,
164164
t0 *= this->mTPCBin2MUS;
165165
terr *= this->mTPCBin2MUS;
166166
} else if constexpr (isITSTrack<decltype(_tr)>()) {
167-
t0 += 0.5f * this->mITSROFrameLengthMUS; // ITS time is supplied in \mus as beginning of ROF
168-
terr *= this->mITSROFrameLengthMUS; // error is supplied as a half-ROF duration, convert to \mus
169-
} else if constexpr (isMFTTrack<decltype(_tr)>()) { // Same for MFT
167+
t0 += 0.5f * this->mITSROFrameLengthMUS; // ITS time is supplied in \mus as beginning of ROF
168+
terr *= this->mITSROFrameLengthMUS; // error is supplied as a half-ROF duration, convert to \mus
169+
} else if constexpr (isMFTTrack<decltype(_tr)>()) {
170+
// Same for MFT
170171
t0 += 0.5f * this->mMFTROFrameLengthMUS;
171172
terr *= this->mMFTROFrameLengthMUS;
172173
} else if constexpr (!(isMCHTrack<decltype(_tr)>() || isMIDTrack<decltype(_tr)>() ||
@@ -264,7 +265,8 @@ void EveWorkflowHelper::selectTracks(const CalibObjectsConst* calib,
264265
if (gid.getSource() == o2::dataformats::GlobalTrackID::TPC && checkTPCDCA) {
265266
const auto& tpcTr = mRecoCont->getTPCTrack(gid);
266267
o2::track::TrackPar trc{tpcTr};
267-
if (!tpcTr.hasBothSidesClusters()) { // need to correct track Z with this vertex time
268+
if (!tpcTr.hasBothSidesClusters()) {
269+
// need to correct track Z with this vertex time
268270
float dz = (tpcTr.getTime0() * mTPCTimeBins2MUS -
269271
(pv.getTimeStamp().getTimeStamp() + mTPCVDrift->getTimeOffset())) *
270272
mTPCVDrift->getVDrift();
@@ -427,10 +429,12 @@ void EveWorkflowHelper::draw(std::size_t primaryVertexIdx, bool sortTracks)
427429
break;
428430
case GID::TPC: {
429431
float dz = 0.f;
430-
if (conf.PVMode) { // for TPC the nominal time (center of the bracket) is stored but in the PVMode we correct it by the PV time
432+
if (conf.PVMode) {
433+
// for TPC the nominal time (center of the bracket) is stored but in the PVMode we correct it by the PV time
431434
tim = pvTime;
432435
const auto& tpcTr = mRecoCont->getTPCTrack(gid);
433-
if (!tpcTr.hasBothSidesClusters()) { // need to correct track Z with this vertex time
436+
if (!tpcTr.hasBothSidesClusters()) {
437+
// need to correct track Z with this vertex time
434438
float dz = (tpcTr.getTime0() * mTPCTimeBins2MUS - (pvTime + mTPCVDrift->getTimeOffset())) *
435439
mTPCVDrift->getVDrift();
436440
if (tpcTr.hasCSideClustersOnly()) {
@@ -513,7 +517,7 @@ void EveWorkflowHelper::draw(std::size_t primaryVertexIdx, bool sortTracks)
513517
}
514518

515519
void EveWorkflowHelper::save(const std::string& jsonPath, const std::string& ext, int numberOfFiles,
516-
const std::string& receiverHostname, int receiverPort, bool useOnlyFiles,
520+
const std::string& receiverHostname, int receiverPort, int receiverTimeout, bool useOnlyFiles,
517521
bool useOnlySockets)
518522
{
519523
mEvent.setEveVersion(o2_eve_version);
@@ -522,6 +526,7 @@ void EveWorkflowHelper::save(const std::string& jsonPath, const std::string& ext
522526

523527
VisualisationEventSerializer::getInstance(ext)->toFile(mEvent, Location({.fileName = producer.newFileName(),
524528
.port = receiverPort,
529+
.timeout = receiverTimeout,
525530
.host = receiverHostname,
526531
.toFile = !useOnlySockets,
527532
.toSocket = !useOnlyFiles}));
@@ -550,7 +555,8 @@ std::vector<PNT>
550555
auto tp = trc;
551556
float dxmin = std::abs(xMin - tp.getX()), dxmax = std::abs(xMax - tp.getX());
552557

553-
if (dxmin > dxmax) { // start from closest end
558+
if (dxmin > dxmax) {
559+
// start from closest end
554560
std::swap(xMin, xMax);
555561
dx = -dx;
556562
}
@@ -763,7 +769,7 @@ void EveWorkflowHelper::drawTPCTRD(GID gid, float trackTime, GID::Source source)
763769
const auto& tpcTrdTrack = mRecoCont->getTPCTRDTrack<o2::trd::TrackTRD>(gid);
764770
addTrackToEvent(tpcTrdTrack, gid, trackTime, 0., source);
765771
drawTPCClusters(tpcTrdTrack.getRefGlobalTrackId(), trackTime * mMUS2TPCTimeBins);
766-
drawTRDClusters(tpcTrdTrack); // tracktime
772+
drawTRDClusters(tpcTrdTrack); // tracktime
767773
}
768774

769775
void EveWorkflowHelper::drawITSTPCTRD(GID gid, float trackTime, GID::Source source)
@@ -799,7 +805,7 @@ void EveWorkflowHelper::drawTPCTOF(GID gid, float trackTime)
799805
const auto& match = mRecoCont->getTPCTOFMatch(gid.getIndex());
800806
addTrackToEvent(trTPCTOF, gid, trackTime, 0);
801807
drawTPCClusters(match.getTrackRef(), trackTime * mMUS2TPCTimeBins);
802-
drawTOFClusters(gid); // trackTime
808+
drawTOFClusters(gid); // trackTime
803809
}
804810

805811
void EveWorkflowHelper::drawMFTMCH(GID gid, float trackTime)
@@ -978,7 +984,8 @@ void EveWorkflowHelper::drawTOFClusters(GID gid)
978984

979985
void EveWorkflowHelper::drawITSClusters(GID gid) // float trackTime
980986
{
981-
if (gid.getSource() == GID::ITS) { // this is for for full standalone tracks
987+
if (gid.getSource() == GID::ITS) {
988+
// this is for for full standalone tracks
982989
const auto& trc = mRecoCont->getITSTrack(gid);
983990
auto refs = mRecoCont->getITSTracksClusterRefs();
984991
int ncl = trc.getNumberOfClusters();
@@ -989,7 +996,8 @@ void EveWorkflowHelper::drawITSClusters(GID gid) // float trackTime
989996
float xyz[] = {glo.X(), glo.Y(), glo.Z()};
990997
drawPoint(xyz); // trackTime;
991998
}
992-
} else if (gid.getSource() == GID::ITSAB) { // this is for ITS tracklets from ITS-TPC afterburner
999+
} else if (gid.getSource() == GID::ITSAB) {
1000+
// this is for ITS tracklets from ITS-TPC afterburner
9931001
const auto& trc = mRecoCont->getITSABRef(gid);
9941002
const auto& refs = mRecoCont->getITSABClusterRefs();
9951003
int ncl = trc.getNClusters();
@@ -1049,9 +1057,10 @@ void EveWorkflowHelper::drawTPC(GID gid, float trackTime, float dz)
10491057
}
10501058

10511059
addTrackToEvent(tr, gid, trackTime, dz, GID::TPC);
1052-
float clTime0 = EveConfParam::Instance().PVMode ? trackTime * mMUS2TPCTimeBins
1053-
: -2e9; // in PVMode use supplied real time converted to TB, otherwise pass dummy time to use tpcTrack.getTime0
1054-
drawTPCClusters(gid, clTime0); // trackTime
1060+
float clTime0 = EveConfParam::Instance().PVMode
1061+
? trackTime * mMUS2TPCTimeBins
1062+
: -2e9; // in PVMode use supplied real time converted to TB, otherwise pass dummy time to use tpcTrack.getTime0
1063+
drawTPCClusters(gid, clTime0); // trackTime
10551064
}
10561065

10571066
void EveWorkflowHelper::drawITS(GID gid, float trackTime)
@@ -1198,16 +1207,18 @@ EveWorkflowHelper::EveWorkflowHelper()
11981207
mTPCBin2MUS = elParams.ZbinWidth;
11991208
const auto grp = o2::base::GRPGeomHelper::instance().getGRPECS();
12001209
const auto& alpParamsITS = o2::itsmft::DPLAlpideParam<o2::detectors::DetID::ITS>::Instance();
1201-
mITSROFrameLengthMUS = grp->isDetContinuousReadOut(o2::detectors::DetID::ITS) ? alpParamsITS.roFrameLengthInBC *
1202-
o2::constants::lhc::LHCBunchSpacingMUS
1203-
: alpParamsITS.roFrameLengthTrig *
1204-
1.e-3;
1210+
mITSROFrameLengthMUS = grp->isDetContinuousReadOut(o2::detectors::DetID::ITS)
1211+
? alpParamsITS.roFrameLengthInBC *
1212+
o2::constants::lhc::LHCBunchSpacingMUS
1213+
: alpParamsITS.roFrameLengthTrig *
1214+
1.e-3;
12051215

12061216
const auto& alpParamsMFT = o2::itsmft::DPLAlpideParam<o2::detectors::DetID::MFT>::Instance();
1207-
mMFTROFrameLengthMUS = grp->isDetContinuousReadOut(o2::detectors::DetID::MFT) ? alpParamsMFT.roFrameLengthInBC *
1208-
o2::constants::lhc::LHCBunchSpacingMUS
1209-
: alpParamsMFT.roFrameLengthTrig *
1210-
1.e-3;
1217+
mMFTROFrameLengthMUS = grp->isDetContinuousReadOut(o2::detectors::DetID::MFT)
1218+
? alpParamsMFT.roFrameLengthInBC *
1219+
o2::constants::lhc::LHCBunchSpacingMUS
1220+
: alpParamsMFT.roFrameLengthTrig *
1221+
1.e-3;
12111222

12121223
mPVParams = &o2::vertexing::PVertexerParams::Instance();
12131224

EventVisualisation/Workflow/src/O2DPLDisplay.cxx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ void customize(std::vector<ConfigParamSpec>& workflowOptions)
6262
{"jsons-folder", VariantType::String, "jsons", {"name of the folder to store json files"}},
6363
{"receiver-hostname", VariantType::String, "arcbs04.cern.ch", {"name of the host where visualisation data is transmitted (only eve format)"}},
6464
{"receiver-port", VariantType::Int, 8001, {"port number of the host where visualisation data is transmitted (only eve format)"}},
65+
{"receiver-timeout", VariantType::Int, 300, {"socket connection timeout (ms)"}},
6566
{"use-only-files", VariantType::Bool, false, {"do not transmit visualisation data using sockets (only eve format)"}},
6667
{"use-only-sockets", VariantType::Bool, false, {"do not store visualisation data using filesystem"}},
6768
{"use-json-format", VariantType::Bool, false, {"instead of eve format (default) use json format"}},
@@ -190,7 +191,7 @@ void O2DPLDisplaySpec::run(ProcessingContext& pc)
190191
helper.mEvent.setRunType(this->mRunType);
191192
helper.mEvent.setPrimaryVertex(pv);
192193
helper.mEvent.setCreationTime(tinfo.creation);
193-
helper.save(this->mJsonPath, this->mExt, conf.maxFiles, this->mReceiverHostname, this->mReceiverPort, this->mUseOnlyFiles, this->mUseOnlySockets);
194+
helper.save(this->mJsonPath, this->mExt, conf.maxFiles, this->mReceiverHostname, this->mReceiverPort, this->mReceiverTimeout, this->mUseOnlyFiles, this->mUseOnlySockets);
194195
filesSaved++;
195196
currentTime = std::chrono::high_resolution_clock::now(); // time AFTER save
196197
this->mTimeStamp = currentTime; // next run AFTER period counted from last save
@@ -308,6 +309,7 @@ WorkflowSpec defineDataProcessing(ConfigContext const& cfgc)
308309

309310
auto receiverHostname = cfgc.options().get<std::string>("receiver-hostname");
310311
auto receiverPort = cfgc.options().get<int>("receiver-port");
312+
auto receiverTimeout = cfgc.options().get<int>("receiver-timeout");
311313
auto useOnlyFiles = cfgc.options().get<bool>("use-only-files");
312314
auto useOnlySockets = cfgc.options().get<bool>("use-only-sockets");
313315

@@ -409,7 +411,7 @@ WorkflowSpec defineDataProcessing(ConfigContext const& cfgc)
409411
{},
410412
AlgorithmSpec{adaptFromTask<O2DPLDisplaySpec>(disableWrite, useMC, srcTrk, srcCl, dataRequest, ggRequest,
411413
emcalCalibLoader, jsonFolder, ext, timeInterval, eveHostNameMatch,
412-
receiverHostname, receiverPort, useOnlyFiles, useOnlySockets)}});
414+
receiverHostname, receiverPort, receiverTimeout, useOnlyFiles, useOnlySockets)}});
413415

414416
// configure dpl timer to inject correct firstTForbit: start from the 1st orbit of TF containing 1st sampled orbit
415417
o2::raw::HBFUtilsInitializer hbfIni(cfgc, specs);

0 commit comments

Comments
 (0)