otsdaq  3.11.00
ARTDAQSupervisor.cc
1 
2 
3 #define TRACEMF_USE_VERBATIM 1 // for trace longer path filenames
4 #include "otsdaq/ARTDAQSupervisor/ARTDAQSupervisor.hh"
5 
6 #include "artdaq-core/Utilities/configureMessageFacility.hh"
7 #include "artdaq/BuildInfo/GetPackageBuildInfo.hh"
8 #include "artdaq/DAQdata/Globals.hh"
9 #include "artdaq/ExternalComms/MakeCommanderPlugin.hh"
10 #include "cetlib_except/exception.h"
11 #include "fhiclcpp/make_ParameterSet.h"
12 #include "otsdaq/ARTDAQSupervisor/ARTDAQSupervisorTRACEController.h"
13 
14 #include "artdaq-core/Utilities/ExceptionHandler.hh" /*for artdaq::ExceptionHandler*/
15 
16 #include <boost/exception/all.hpp>
17 #include <boost/filesystem.hpp>
18 
19 #include <signal.h>
20 #include <cerrno>
21 #include <cstring>
22 #include <fstream>
23 #include <regex>
24 
25 #include "otsdaq/ConfigurationInterface/ConfigurationInterface.h"
26 #include "otsdaq/Macros/StringMacros.h"
27 #include "otsdaq/TableCore/TableBase.h"
28 
29 #define OUT_ON_ERR_SIZE 2000 //tail size of output to include on error
30 //bounded wait for the DAQInterface runner thread to exit during Halt
31 #define HALT_RUNNER_STOP_TIMEOUT_SECONDS 5
32 //bounded wait for the Python interpreter mutex during Halt
33 #define HALT_PYTHON_MUTEX_TIMEOUT_SECONDS 5
34 //bounded wait for the runner thread before tearing down Python
35 #define DESTROY_RUNNER_STOP_TIMEOUT_SECONDS 5
36 //XML-RPC status polls (1 s apart) that may fail before the run is ended
37 #define RUNNER_MAX_CONSECUTIVE_STATUS_FAILURES 5
38 
39 using namespace ots;
40 
41 XDAQ_INSTANTIATOR_IMPL(ARTDAQSupervisor)
42 
43 const std::string ARTDAQSupervisor::ARTDAQ_SYSVAR_NAMESPACE = "artdaq";
44 
45 #define FAKE_CONFIG_NAME "ots_config"
46 #define DAQINTERFACE_PORT \
47  std::atoi(__ENV__("ARTDAQ_BASE_PORT")) + \
48  (partition_ * std::atoi(__ENV__("ARTDAQ_PORTS_PER_PARTITION")))
49 
50 static ARTDAQSupervisor* instance = nullptr;
51 static std::unordered_map<int, struct sigaction> old_actions =
52  std::unordered_map<int, struct sigaction>();
53 static bool sighandler_init = false;
54 static void signal_handler(int signum)
55 {
56 // Messagefacility may already be gone at this point, TRACE ONLY!
57 #if TRACE_REVNUM < 1459
58  TRACE_STREAMER(TLVL_ERROR, &("ARTDAQsupervisor")[0], 0, 0, 0)
59 #else
60  TRACE_STREAMER(TLVL_ERROR, TLOG2("ARTDAQsupervisor", 0), 0)
61 #endif
62  << "A signal of type " << signum
63  << " was caught by ARTDAQSupervisor. Shutting down DAQInterface, "
64  "then proceeding with default handlers!";
65 
66  if(instance)
67  instance->destroy();
68 
69  sigset_t set;
70  pthread_sigmask(SIG_UNBLOCK, NULL, &set);
71  pthread_sigmask(SIG_UNBLOCK, &set, NULL);
72 
73 #if TRACE_REVNUM < 1459
74  TRACE_STREAMER(TLVL_ERROR, &("ARTDAQsupervisor")[0], 0, 0, 0)
75 #else
76  TRACE_STREAMER(TLVL_ERROR, TLOG2("ARTDAQsupervisor", 0), 0)
77 #endif
78  << "Calling default signal handler";
79  if(signum != SIGUSR2)
80  {
81  sigaction(signum, &old_actions[signum], NULL);
82  kill(getpid(), signum); // Only send signal to self
83  }
84  else
85  {
86  // Send Interrupt signal if parsing SIGUSR2 (i.e. user-defined exception that
87  // should tear down ARTDAQ)
88  sigaction(SIGINT, &old_actions[SIGINT], NULL);
89  kill(getpid(), SIGINT); // Only send signal to self
90  }
91 }
92 
93 static void init_sighandler(ARTDAQSupervisor* inst)
94 {
95  static std::mutex sighandler_mutex;
96  std::unique_lock<std::mutex> lk(sighandler_mutex);
97 
98  if(!sighandler_init)
99  {
100  sighandler_init = true;
101  instance = inst;
102  std::vector<int> signals = {
103  SIGINT,
104  SIGILL,
105  SIGABRT,
106  SIGFPE,
107  SIGSEGV,
108  SIGPIPE,
109  SIGALRM,
110  SIGTERM,
111  SIGUSR2,
112  SIGHUP}; // SIGQUIT is used by art in normal operation
113  for(auto signal : signals)
114  {
115  struct sigaction old_action;
116  sigaction(signal, NULL, &old_action);
117 
118  // If the old handler wasn't SIG_IGN (it's a handler that just
119  // "ignore" the signal)
120  if(old_action.sa_handler != SIG_IGN)
121  {
122  struct sigaction action;
123  action.sa_handler = signal_handler;
124  sigemptyset(&action.sa_mask);
125  for(auto sigblk : signals)
126  {
127  sigaddset(&action.sa_mask, sigblk);
128  }
129  action.sa_flags = 0;
130 
131  // Replace the signal handler of SIGINT with the one described by
132  // new_action
133  sigaction(signal, &action, NULL);
134  old_actions[signal] = old_action;
135  }
136  }
137  }
138 }
139 
140 //==============================================================================
141 ARTDAQSupervisor::ARTDAQSupervisor(xdaq::ApplicationStub* stub)
142  : CoreSupervisorBase(stub)
143  , daqinterface_ptr_(NULL)
144  , partition_(getSupervisorProperty("partition", 0))
145  , daqinterface_state_("notrunning")
146  , runner_thread_(nullptr)
147 {
148  __SUP_COUT__ << "Constructor." << __E__;
149 
150  INIT_MF("." /*directory used is USER_DATA/LOG/.*/);
151  init_sighandler(this);
152 
153  // Only use system Python
154  // unsetenv("PYTHONPATH");
155  // unsetenv("PYTHONHOME");
156 
157  // Write out settings file
158  auto settings_file = __ENV__("DAQINTERFACE_SETTINGS");
159  std::ofstream of(settings_file, std::ios::trunc);
160  const int openErrno = errno; // capture errno immediately after open attempt
161  if(!of.is_open() || of.fail())
162  {
163  __SS__ << "Failed to open DAQINTERFACE_SETTINGS file '" << settings_file
164  << "' for writing: " << strerror(openErrno) << __E__;
165  __SS_THROW__;
166  }
167  std::stringstream o;
168 
169  setenv("DAQINTERFACE_PARTITION_NUMBER", std::to_string(partition_).c_str(), 1);
170  auto logfileName = std::string(__ENV__("OTSDAQ_LOG_DIR")) +
171  "/DAQInteface/DAQInterface_partition" +
172  std::to_string(partition_) + ".log";
173  setenv("DAQINTERFACE_LOGFILE", logfileName.c_str(), 1);
174 
175  o << "log_directory: "
176  << getSupervisorProperty("log_directory", std::string(__ENV__("OTSDAQ_LOG_DIR")))
177  << std::endl;
178 
179  {
180  const std::string record_directory = getSupervisorProperty(
181  "record_directory", ARTDAQTableBase::ARTDAQ_FCL_PATH + "/run_records/");
182  mkdir(record_directory.c_str(), 0755);
183  o << "record_directory: " << record_directory << std::endl;
184  }
185 
186  o << "package_hashes_to_save: "
187  << getSupervisorProperty("package_hashes_to_save", "[artdaq]") << std::endl;
188 
189  o << "spack_root_for_bash_scripts: "
190  << getSupervisorProperty("spack_root_for_bash_scripts",
191  std::string(__ENV__("SPACK_ROOT")))
192  << std::endl;
193  o << "boardreader timeout: " << getSupervisorProperty("boardreader_timeout", 30)
194  << std::endl;
195  o << "eventbuilder timeout: " << getSupervisorProperty("eventbuilder_timeout", 30)
196  << std::endl;
197  o << "datalogger timeout: " << getSupervisorProperty("datalogger_timeout", 30)
198  << std::endl;
199  o << "dispatcher timeout: " << getSupervisorProperty("dispatcher_timeout", 30)
200  << std::endl;
201  // Only put max_fragment_size_bytes into DAQInterface settings file if advanced_memory_usage is disabled
202  if(!getSupervisorProperty("advanced_memory_usage", false))
203  {
204  o << "max_fragment_size_bytes: "
205  << getSupervisorProperty("max_fragment_size_bytes", 1048576) << std::endl;
206  }
207  o << "transfer_plugin_to_use: "
208  << getSupervisorProperty("transfer_plugin_to_use", "TCPSocket") << std::endl;
209  if(getSupervisorProperty("transfer_plugin_from_brs", "") != "")
210  {
211  o << "transfer_plugin_from_brs: "
212  << getSupervisorProperty("transfer_plugin_from_brs", "") << std::endl;
213  }
214  if(getSupervisorProperty("transfer_plugin_from_ebs", "") != "")
215  {
216  o << "transfer_plugin_from_ebs: "
217  << getSupervisorProperty("transfer_plugin_from_ebs", "") << std::endl;
218  }
219  if(getSupervisorProperty("transfer_plugin_from_dls", "") != "")
220  {
221  o << "transfer_plugin_from_dls: "
222  << getSupervisorProperty("transfer_plugin_from_dls", "") << std::endl;
223  }
224  o << "all_events_to_all_dispatchers: " << std::boolalpha
225  << getSupervisorProperty("all_events_to_all_dispatchers", true) << std::endl;
226  if(getSupervisorProperty("data_directory_override", "") != "")
227  {
228  o << "data_directory_override: "
229  << getSupervisorProperty("data_directory_override", "") << std::endl;
230  }
231  o << "max_configurations_to_list: "
232  << getSupervisorProperty("max_configurations_to_list", 10) << std::endl;
233  o << "disable_unique_rootfile_labels: "
234  << getSupervisorProperty("disable_unique_rootfile_labels", false) << std::endl;
235  o << "use_messageviewer: " << std::boolalpha
236  << getSupervisorProperty("use_messageviewer", false) << std::endl;
237  o << "use_messagefacility: " << std::boolalpha
238  << getSupervisorProperty("use_messagefacility", true) << std::endl;
239  o << "fake_messagefacility: " << std::boolalpha
240  << getSupervisorProperty("fake_messagefacility", false) << std::endl;
241  o << "kill_existing_processes: " << std::boolalpha
242  << getSupervisorProperty("kill_existing_processes", true) << std::endl;
243  o << "advanced_memory_usage: " << std::boolalpha
244  << getSupervisorProperty("advanced_memory_usage", false) << std::endl;
245  o << "strict_fragment_id_mode: " << std::boolalpha
246  << getSupervisorProperty("strict_fragment_id_mode", false) << std::endl;
247  o << "disable_private_network_bookkeeping: " << std::boolalpha
248  << getSupervisorProperty("disable_private_network_bookkeeping", false) << std::endl;
249  o << "allowed_processors: "
250  << getSupervisorProperty(
251  "allowed_processors",
252  "0-255") // Note this sets a taskset for ALL processes, on all nodes (ex. "1,2,5-7")
253  << std::endl;
254  if(getSupervisorProperty("partition_label_format", "") !=
255  "") //Add to ARTDAQSupervisor properties partition_label_format: "-P%s"
256  o << "partition_label_format: "
257  << getSupervisorProperty("partition_label_format", "") << std::endl;
258 
259  __COUT_MULTI__(0, o.str());
260 
261  of << o.str();
262  of.close();
263 
264  // destroy current TRACEController and instantiate ARTDAQSupervisorTRACEController
265  if(CorePropertySupervisorBase::theTRACEController_)
266  {
267  __SUP_COUT__ << "Destroying TRACE Controller..." << __E__;
268  delete CorePropertySupervisorBase::
269  theTRACEController_; // destruct current TRACEController
270  CorePropertySupervisorBase::theTRACEController_ = nullptr;
271  }
272  CorePropertySupervisorBase::theTRACEController_ =
274  ((ARTDAQSupervisorTRACEController*)CorePropertySupervisorBase::theTRACEController_)
275  ->setSupervisorPtr(this);
276 
277  __SUP_COUT__ << "Constructed." << __E__;
278 } // end constructor()
279 
280 //==============================================================================
281 ARTDAQSupervisor::~ARTDAQSupervisor(void)
282 {
283  __SUP_COUT__ << "Destructor." << __E__;
284  destroy();
285 
286  if(runner_abandoned_)
287  __SUP_COUT_WARN__ << "Skipping Py_Finalize(): a DAQInterface runner thread may "
288  "still be inside a Python call. The interpreter is leaked "
289  "rather than finalized under a live C-API call."
290  << __E__;
291  else
292  {
293  __SUP_COUT__ << "Calling Py_Finalize()" << __E__;
294  Py_Finalize();
295  }
296 
297  // CorePropertySupervisorBase would destroy, but since it was created here, attempt to destroy
299  {
300  __SUP_COUT__ << "Destroying TRACE Controller..." << __E__;
303  }
304 
305  __SUP_COUT__ << "Destructed." << __E__;
306 } // end destructor()
307 
308 //==============================================================================
309 void ARTDAQSupervisor::destroy(void)
310 {
311  __SUP_COUT__ << "Destroying..." << __E__;
312 
313  // Stop the runner before touching the interpreter. If it does not exit in
314  // time it is blocked inside a Python call: everything below that enters
315  // Python (recover, DECREF, thread cleanup, and Py_Finalize in the
316  // destructor) is then skipped. The process is on its way out, so leaking the
317  // interpreter is harmless; tearing it down under a live C-API call is not.
318  // The same applies if an earlier transition (e.g. Halt) already abandoned a
319  // runner: stop_runner_() recorded that in runner_abandoned_, and that thread
320  // may still be inside Python even though it is no longer tracked here.
321  if(!stop_runner_(DESTROY_RUNNER_STOP_TIMEOUT_SECONDS) || runner_abandoned_)
322  {
323  __SUP_COUT_WARN__ << "A DAQInterface runner thread is (or was earlier) abandoned "
324  "while possibly inside a Python call; skipping DAQInterface "
325  "recover and Python cleanup. artdaq processes may need "
326  "manual cleanup."
327  << __E__;
328  return;
329  }
330 
331  if(daqinterface_ptr_ != NULL)
332  {
333  __SUP_COUT__ << "Calling recover transition" << __E__;
334  std::lock_guard<std::recursive_timed_mutex> lk(daqinterface_pythonMutex_);
335 
336  PyObjectGuard pName(PyUnicode_FromString("do_recover"));
337  PyObjectGuard res(
338  PyObject_CallMethodObjArgs(daqinterface_ptr_, pName.get(), NULL));
339 
340  __SUP_COUT__ << "Making sure that correct state has been reached" << __E__;
341  getDAQState_();
342  while(daqinterface_state_ != "stopped")
343  {
344  getDAQState_();
345  __SUP_COUT__ << "State is " << daqinterface_state_
346  << ", waiting 1s and retrying..." << __E__;
347  usleep(1000000);
348  }
349 
350  // Cleanup
351  Py_XDECREF(daqinterface_ptr_);
352  daqinterface_ptr_ = NULL;
353  }
354 
355  __SUP_COUT__ << "Flusing printouts" << __E__;
356 
357  //make sure to flush printouts
358  PyRun_SimpleString(R"(
359 import sys
360 sys.stdout = sys.__stdout__
361 sys.stderr = sys.__stderr__
362 )");
363  // stringIO_out_ and stringIO_err_ may refer to borrowed Python objects
364  // (e.g., via PyDict_GetItemString in the tee-buffer path). Do not DECREF
365  // them here to avoid corrupting their reference counts; treat them as
366  // non-owning pointers and clear them instead.
367  stringIO_out_ = nullptr;
368  stringIO_err_ = nullptr;
369 
370  __SUP_COUT__ << "Thread and garbage cleanup" << __E__;
371  // force python thread cleanup:
372  PyRun_SimpleString(
373  "import threading; [t.join() for t in threading.enumerate() if t is not "
374  "threading.main_thread() and not isinstance(t, threading._DummyThread)]");
375  PyRun_SimpleString("import gc; gc.collect()");
376  // __SUP_COUT__ << "Calling Py_Finalize()" << __E__;
377  // Py_Finalize();
378 
379  __SUP_COUT__ << "Destroyed." << __E__;
380 } // end destroy()
381 
382 //==============================================================================
383 void ARTDAQSupervisor::init(void)
384 {
385  stop_runner_();
386 
387  __SUP_COUT__ << "Initializing..." << __E__;
388  {
389  std::lock_guard<std::recursive_timed_mutex> lk(daqinterface_pythonMutex_);
390 
391  // allSupervisorInfo_.init(getApplicationContext());
392  artdaq::configureMessageFacility("ARTDAQSupervisor");
393  __SUP_COUT__ << "artdaq MF configured." << __E__;
394 
395  // initialization
396  char* daqinterface_dir = getenv("ARTDAQ_DAQINTERFACE_DIR");
397  if(daqinterface_dir == NULL)
398  {
399  __SS__ << "ARTDAQ_DAQINTERFACE_DIR environment variable not set! This "
400  "means that DAQInterface has not been setup!"
401  << __E__;
402  __SUP_SS_THROW__;
403  }
404  else
405  {
406  __SUP_COUT__ << "Initializing Python" << __E__;
407  Py_Initialize();
408 
409  //setup Python output to tee output to stdout/err and to StringIO buffer "tee_buffer"
410  PyRun_SimpleString(
411  "import sys\n"
412  "from io import StringIO\n"
413  "\n"
414  "class TeeOut:\n"
415  " def __init__(self, real, buf):\n"
416  " self.real = real\n"
417  " self.buf = buf\n"
418  " def write(self, data):\n"
419  " self.real.write(data)\n"
420  " self.buf.write(data)\n"
421  " def flush(self):\n"
422  " self.real.flush()\n"
423  " self.buf.flush()\n"
424  "\n"
425  "tee_buffer = StringIO()\n"
426  "sys.stdout = TeeOut(sys.stdout, tee_buffer)\n"
427  "sys.stderr = TeeOut(sys.stderr, tee_buffer)\n");
428 
429  __SUP_COUT__ << "Adding DAQInterface directory to PYTHON_PATH" << __E__;
430  PyObject* sysPath = PySys_GetObject(
431  (char*)"path"); //do NOT DECREF borrowed objects through GetObject!
432  PyObjectGuard programName(PyUnicode_FromString(daqinterface_dir));
433  PyList_Append(sysPath, programName.get());
434 
435  __SUP_COUT__ << "Creating Module name" << __E__;
436  PyObjectGuard pName(PyUnicode_FromString("rc.control.daqinterface"));
437  /* Error checking of pName left out */
438 
439  __SUP_COUT__ << "Importing module" << __E__;
440  PyObjectGuard pModule(PyImport_Import(pName.get()));
441 
442  if(pModule.get() == NULL)
443  {
444  std::string err = capturePyErr("import rc.control.daqinterface");
445  __SS__ << "Failed to load rc.control.daqinterface. Python Exception: "
446  << err << __E__;
447  __SUP_SS_THROW__;
448  }
449  else
450  {
451  __SUP_COUT__ << "Loading python module dictionary" << __E__;
452  PyObject* pDict = PyModule_GetDict(
453  pModule.get()); //do NOT DECREF borrowed objects through GetDict!
454  if(pDict == NULL)
455  {
456  std::string err = capturePyErr("module dict");
457  __SS__ << "Unable to load module dictionary. Python Exception: "
458  << err << __E__;
459  __SUP_SS_THROW__;
460  }
461  else
462  {
463  __SUP_COUT__ << "Getting DAQInterface object pointer" << __E__;
464  PyObject* di_obj_raw = PyDict_GetItemString(
465  pDict, "DAQInterface"); // borrowed reference
466  if(di_obj_raw == NULL)
467  {
468  std::string err = capturePyErr("DAQInterface lookup");
469  __SS__ << "Unable to find 'DAQInterface' in module dictionary. "
470  "Python Exception: "
471  << err << __E__;
472  __SUP_SS_THROW__;
473  }
474  Py_INCREF(di_obj_raw); // convert borrowed reference to owned
475  PyObjectGuard di_obj_ptr(di_obj_raw);
476 
477  __SUP_COUT__ << "Filling out DAQInterface args struct" << __E__;
478  PyObjectGuard pArgs(PyTuple_New(0));
479 
480  PyObjectGuard kwargs(Py_BuildValue("{s:s, s:s, s:i, s:i, s:s, s:s}",
481  "logpath",
482  ".daqint.log",
483  "name",
484  "DAQInterface",
485  "partition_number",
486  partition_,
487  "rpc_port",
488  DAQINTERFACE_PORT,
489  "rpc_host",
490  "localhost",
491  "control_host",
492  "localhost"));
493 
494  __SUP_COUT__ << "Calling DAQInterface Object Constructor" << __E__;
495 
496  // Get sys and io
497  PyObjectGuard sys(PyImport_ImportModule("sys"));
498  PyObjectGuard io(PyImport_ImportModule("io"));
499 
500  if(0)
501  {
502  //------------- redirect stdout to string
503 
504  // Create StringIO objects for stdout and stderr
505  stringIO_out_ = PyObject_CallMethod(io.get(), "StringIO", NULL);
506  stringIO_err_ = PyObject_CallMethod(io.get(), "StringIO", NULL);
507 
508  // Save originals (not needed, since just keep the redirection until daqinterface_ptr_ is destructed)
509  // PyObject* sys_stdout = PyObject_GetAttrString(sys, "stdout");
510  // PyObject* sys_stderr = PyObject_GetAttrString(sys, "stderr");
511 
512  // Redirect
513  PyObject_SetAttrString(sys.get(), "stdout", stringIO_out_);
514  PyObject_SetAttrString(sys.get(), "stderr", stringIO_err_);
515  //------------- end redirect stdout to string
516  }
517  else //capture tee buffer instead so output to console continues
518  {
519  PyObject* mainmod =
520  PyImport_AddModule("__main__"); // borrowed ref
521  PyObject* globals = PyModule_GetDict(mainmod); // borrowed ref
522 
523  stringIO_out_ =
524  PyDict_GetItemString(globals, "tee_buffer"); // borrowed ref
525 
526  // Do not Py_DECREF borrowed references.
527  }
528 
529  daqinterface_ptr_ =
530  PyObject_Call(di_obj_ptr.get(), pArgs.get(), kwargs.get());
531  if(checkPythonError(daqinterface_ptr_))
532  {
533  std::string err = capturePyErr("DAQInterface constructor");
534  __SS__ << "DAQInterface constructor failed. Python Exception: "
535  << err << __E__;
536  __SUP_SS_THROW__;
537  }
538 
539  if(0) //example printout handling
540  {
541  // Force an error
542  PyObjectGuard bad(
543  PyObject_CallMethod(sys.get(), "does_not_exist", NULL));
544  if(!bad.get())
545  PyErr_Print(); // <-- this writes into stringIO_err_, not the terminal
546 
547  // Grab stderr contents
548  PyObjectGuard err_text(
549  PyObject_CallMethod(stringIO_err_, "getvalue", NULL));
550  if(err_text.get())
551  __COUT__ << "Captured stderr:\n"
552  << PyUnicode_AsUTF8(err_text.get()) << "\n";
553  else
554  __COUT__ << "Capture of stderr failed.";
555  } //end example printout handling
556  }
557  }
558  }
559 
560  getDAQState_();
561 
562  // { //attempt to cleanup old artdaq processes DOES NOT WORK because artdaq interface knows it hasn't started
563  // __SUP_COUT__ << "Attempting artdaq stale cleanup..." << __E__;
564  // std::lock_guard<std::recursive_timed_mutex> lk(daqinterface_pythonMutex_);
565  // getDAQState_();
566  // __SUP_COUT__ << "Status before cleanup: " << daqinterface_state_ << __E__;
567 
568  // PyObjectGuard pName(PyUnicode_FromString("do_recover"));
569  // PyObjectGuard res(PyObject_CallMethodObjArgs(daqinterface_ptr_, pName, NULL));
570  // __COUT_MULTI_LBL__(0,captureStderrAndStdout_("do_recover"),"do_recover");
571 
572  // if(res == NULL)
573  // {
574  // std::string err = capturePyErr("do_recover");
575  // __SS__ << "Error with clean up calling do_recover: " << err << __E__;
576  // __SUP_SS_THROW__;
577  // }
578  // getDAQState_();
579  // __SUP_COUT__ << "Status after cleanup: " << daqinterface_state_ << __E__;
580  // __SUP_COUT__ << "cleanup DONE." << __E__;
581  // }
582  }
583  start_runner_();
584 
585  initArtdaqSystemVariables();
586 
587  __SUP_COUT__ << "Initialized." << __E__;
588 } // end init()
589 
590 //==============================================================================
591 void ARTDAQSupervisor::transitionConfiguring(toolbox::Event::Reference /*event*/)
592 {
593  __SUP_COUTT__ << "transitionConfiguring" << __E__;
594 
595  loadArtdaqSystemVariables();
596 
598  {
599  thread_error_message_ = "";
600  thread_progress_bar_.resetProgressBar(0);
601  last_thread_progress_update_ = time(0); // initialize timeout timer
602 
603  // Skip the table group reload when the same group+key is already active
604  // (safe: keys are immutable and configureInit() verifies no
605  // scratch/temporary versions or merge/override lists are in play);
606  // set supervisor property SkipRedundantConfigureActivation=0 to disable
607  CoreSupervisorBase::configureInit(
608  getSupervisorProperty("SkipRedundantConfigureActivation", 1) ==
609  1 /*attemptSkipIfGroupUnchanged*/);
610 
611  // start configuring thread
612  std::thread(&ARTDAQSupervisor::configuringThread, this).detach();
613 
614  __SUP_COUT__ << "Configuring thread started." << __E__;
615 
616  RunControlStateMachine::indicateIterationWork(); // poll on the next iteration
617  }
618  else // not first time
619  {
620  std::string errorMessage;
621  {
622  std::lock_guard<std::mutex> lock(
623  thread_mutex_); // lock out for remainder of scope
624  errorMessage = thread_error_message_; // theStateMachine_.getErrorMessage();
625  }
626  int progress = thread_progress_bar_.read();
627  __SUP_COUTVS__(2, errorMessage);
628  __SUP_COUTVS__(2, progress);
629  __SUP_COUTVS__(2, thread_progress_bar_.isComplete());
630 
631  // check for done and error messages
632  if(errorMessage == "" && // if no update in 600 seconds, give up
633  time(0) - last_thread_progress_update_ > 600)
634  {
635  __SUP_SS__ << "There has been no update from the configuration thread for "
636  << (time(0) - last_thread_progress_update_)
637  << " seconds, assuming something is wrong and giving up! "
638  << "Last progress received was " << progress << __E__;
639  errorMessage = ss.str();
640  }
641 
642  // Check if any subapp (artdaq component) has entered a Failed state.
643  // This can happen when a component fails during do_config/do_boot while the
644  // configuringThread is blocked waiting for DAQInterface, and would otherwise
645  // cause a 600-second timeout before the error is detected.
646  if(errorMessage == "")
647  {
648  auto subapps = getSubappInfo();
649  for(auto& subapp : subapps)
650  {
651  if(subapp.status == RunControlStateMachine::FAILED_STATE_NAME)
652  {
653  __SUP_SS__ << "Component '" << subapp.name
654  << "' entered Failed state during configuration! "
655  << "(url: " << subapp.url << ")" << __E__;
656  errorMessage = ss.str();
657  __SUP_COUT_ERR__ << "\n" << ss.str();
658  break;
659  }
660  }
661  }
662 
663  if(errorMessage != "")
664  {
665  __SUP_SS__ << "Error was caught in configuring thread: " << errorMessage
666  << __E__;
667  __SUP_COUT_ERR__ << "\n" << ss.str();
668 
669  theStateMachine_.setErrorMessage(ss.str());
670  throw toolbox::fsm::exception::Exception(
671  "Transition Error" /*name*/,
672  ss.str() /* message*/,
673  "CoreSupervisorBase::transitionConfiguring" /*module*/,
674  __LINE__ /*line*/,
675  __FUNCTION__ /*function*/
676  );
677  }
678 
679  if(!thread_progress_bar_.isComplete())
680  {
681  __SUP_COUTT__ << "Not done yet..." << __E__;
682  //attempt to get live view of python output
683  // __COUT_MULTI_LBL__(0, captureStderrAndStdout_("statuscheck"), "statuscheck");
684 
685  RunControlStateMachine::
686  indicateIterationWork(); // use Iteration to allow other steps to complete in the system
687 
688  if(last_thread_progress_read_ != progress)
689  {
690  last_thread_progress_read_ = progress;
691  last_thread_progress_update_ = time(0);
692  }
693 
694  sleep(1 /*seconds*/);
695  }
696  else
697  {
698  __SUP_COUT_INFO__ << "Complete configuring transition!" << __E__;
699  __SUP_COUTV__(getProcessInfo_());
700  }
701  }
702 
703  return;
704 } // end transitionConfiguring()
705 
706 //==============================================================================
707 void ARTDAQSupervisor::configuringThread()
708 try
709 {
710  std::string uid = theConfigurationManager_
711  ->getNode(ConfigurationManager::XDAQ_APPLICATION_TABLE_NAME +
712  "/" + CorePropertySupervisorBase::getSupervisorUID() +
713  "/" + "LinkToSupervisorTable")
714  .getValueAsString();
715 
716  __COUT__ << "Supervisor uid is " << uid << ", getting supervisor table node" << __E__;
717 
718  const std::string mfSubject_ = supervisorClassNoNamespace_ + "-" + uid;
719 
720  ConfigurationTree theSupervisorNode = getSupervisorTableNode();
721 
722  thread_progress_bar_.step();
723 
724  set_thread_message_("ConfigGen");
725 
726  auto info = ARTDAQTableBase::extractARTDAQInfo(
727  theSupervisorNode,
728  false /*getStatusFalseNodes*/,
729  true /*doWriteFHiCL*/,
730  getSupervisorProperty("max_fragment_size_bytes", 8888),
731  getSupervisorProperty("routing_timeout_ms", 1999),
732  getSupervisorProperty("routing_retry_count", 12),
733  &thread_progress_bar_);
734 
735  // Check lists
736  if(info.processes.count(ARTDAQTableBase::ARTDAQAppType::BoardReader) == 0)
737  {
738  __GEN_SS__ << "There must be at least one enabled BoardReader!" << __E__;
739  __GEN_SS_THROW__;
740  }
741  if(info.processes.count(ARTDAQTableBase::ARTDAQAppType::EventBuilder) == 0)
742  {
743  __GEN_SS__ << "There must be at least one enabled EventBuilder!" << __E__;
744  __GEN_SS_THROW__;
745  }
746 
747  thread_progress_bar_.step();
748  set_thread_message_("Writing boot.txt");
749 
750  __GEN_COUT__ << "Writing boot.txt" << __E__;
751 
752  int debugLevel = theSupervisorNode.getNode("DAQInterfaceDebugLevel").getValue<int>();
753  std::string setupScript = theSupervisorNode.getNode("DAQSetupScript").getValue();
754 
755  // Generate boot file content using helper function
756  std::string bootContent =
757  ARTDAQTableBase::getBootFileContentFromInfo(info, setupScript, debugLevel);
758 
759  // Populate label_to_proc_type_map_ (still needed for later)
760  for(auto& builder : info.processes[ARTDAQTableBase::ARTDAQAppType::EventBuilder])
761  label_to_proc_type_map_[builder.label] = "EventBuilder";
762  for(auto& logger : info.processes[ARTDAQTableBase::ARTDAQAppType::DataLogger])
763  label_to_proc_type_map_[logger.label] = "DataLogger";
764  for(auto& dispatcher : info.processes[ARTDAQTableBase::ARTDAQAppType::Dispatcher])
765  label_to_proc_type_map_[dispatcher.label] = "Dispatcher";
766  for(auto& rmanager : info.processes[ARTDAQTableBase::ARTDAQAppType::RoutingManager])
767  label_to_proc_type_map_[rmanager.label] = "RoutingManager";
768 
769  // Write boot.txt file
770  std::ofstream o(ARTDAQTableBase::ARTDAQ_FCL_PATH + "/boot.txt", std::ios::trunc);
771  o << bootContent;
772  o.close();
773 
774  // TODO: To save to runlog, store bootContent in metadata/configuration archive
775  // Example (add when implementing runlog integration):
776  // saveToRunlog("boot.txt", bootContent, run_number);
777 
778  thread_progress_bar_.step();
779  set_thread_message_("Writing Fhicl Files");
780 
781  __GEN_COUT__ << "Building configuration directory" << __E__;
782 
783  boost::system::error_code ignored;
784  boost::filesystem::remove_all(ARTDAQTableBase::ARTDAQ_FCL_PATH + FAKE_CONFIG_NAME,
785  ignored);
786  mkdir((ARTDAQTableBase::ARTDAQ_FCL_PATH + FAKE_CONFIG_NAME).c_str(), 0755);
787 
788  for(auto& reader : info.processes[ARTDAQTableBase::ARTDAQAppType::BoardReader])
789  {
790  symlink(ARTDAQTableBase::getFlatFHICLFilename(
791  ARTDAQTableBase::ARTDAQAppType::BoardReader, reader.label)
792  .c_str(),
793  (ARTDAQTableBase::ARTDAQ_FCL_PATH + FAKE_CONFIG_NAME + "/" +
794  reader.label + ".fcl")
795  .c_str());
796  }
797  for(auto& builder : info.processes[ARTDAQTableBase::ARTDAQAppType::EventBuilder])
798  {
799  symlink(ARTDAQTableBase::getFlatFHICLFilename(
800  ARTDAQTableBase::ARTDAQAppType::EventBuilder, builder.label)
801  .c_str(),
802  (ARTDAQTableBase::ARTDAQ_FCL_PATH + FAKE_CONFIG_NAME + "/" +
803  builder.label + ".fcl")
804  .c_str());
805  }
806  for(auto& logger : info.processes[ARTDAQTableBase::ARTDAQAppType::DataLogger])
807  {
808  symlink(ARTDAQTableBase::getFlatFHICLFilename(
809  ARTDAQTableBase::ARTDAQAppType::DataLogger, logger.label)
810  .c_str(),
811  (ARTDAQTableBase::ARTDAQ_FCL_PATH + FAKE_CONFIG_NAME + "/" +
812  logger.label + ".fcl")
813  .c_str());
814  }
815  for(auto& dispatcher : info.processes[ARTDAQTableBase::ARTDAQAppType::Dispatcher])
816  {
817  symlink(ARTDAQTableBase::getFlatFHICLFilename(
818  ARTDAQTableBase::ARTDAQAppType::Dispatcher, dispatcher.label)
819  .c_str(),
820  (ARTDAQTableBase::ARTDAQ_FCL_PATH + FAKE_CONFIG_NAME + "/" +
821  dispatcher.label + ".fcl")
822  .c_str());
823  }
824  for(auto& rmanager : info.processes[ARTDAQTableBase::ARTDAQAppType::RoutingManager])
825  {
826  symlink(ARTDAQTableBase::getFlatFHICLFilename(
827  ARTDAQTableBase::ARTDAQAppType::RoutingManager, rmanager.label)
828  .c_str(),
829  (ARTDAQTableBase::ARTDAQ_FCL_PATH + FAKE_CONFIG_NAME + "/" +
830  rmanager.label + ".fcl")
831  .c_str());
832  }
833 
834  thread_progress_bar_.step();
835 
836  // Block 1: State check — acquire and release daqinterface_pythonMutex_
837  // so the runner thread and halt transition can interleave between steps
838  {
839  std::lock_guard<std::recursive_timed_mutex> lk(daqinterface_pythonMutex_);
840  getDAQState_();
841  if(daqinterface_state_ != "stopped" && daqinterface_state_ != "")
842  {
843  __GEN_SS__ << "Cannot configure DAQInterface because it is in the wrong state"
844  << " (" << daqinterface_state_ << " != stopped)!" << __E__;
845  __GEN_SS_THROW__
846  }
847 
848  if(daqinterface_ptr_ == nullptr)
849  {
850  __GEN_SS__ << "DAQInterface is not initialized. "
851  "Check earlier Python import/constructor errors (e.g. syntax) "
852  "in DAQInterface."
853  << __E__;
854  __GEN_SS_THROW__;
855  }
856  } // end Block 1 — release daqinterface_pythonMutex_
857 
858  // Block 2: setdaqcomps
859  set_thread_message_("Calling setdaqcomps");
860  __GEN_COUT__ << "Calling setdaqcomps" << __E__;
861  {
862  std::lock_guard<std::recursive_timed_mutex> lk(daqinterface_pythonMutex_);
863 
864  __GEN_COUT__ << "Status before setdaqcomps: " << daqinterface_state_ << __E__;
865 
866  PyObjectGuard pName1(PyUnicode_FromString("setdaqcomps"));
867 
868  PyObjectGuard readerDict(PyDict_New());
869  for(auto& reader : info.processes[ARTDAQTableBase::ARTDAQAppType::BoardReader])
870  {
871  // PyDict_SetItem INCREFs key/value, so use PyObjectGuard to manage references
872  label_to_proc_type_map_[reader.label] = "BoardReader";
873  PyObjectGuard readerName(PyUnicode_FromString(reader.label.c_str()));
874 
875  int list_size = reader.allowed_processors != "" ? 4 : 3;
876 
877  PyObjectGuard readerData(PyList_New(list_size));
878  PyObject* readerHost = PyUnicode_FromString(reader.hostname.c_str());
879  PyObject* readerPort = PyUnicode_FromString("-1");
880  PyObject* readerSubsystem =
881  PyUnicode_FromString(std::to_string(reader.subsystem).c_str());
882  PyList_SetItem(readerData.get(), 0, readerHost);
883  PyList_SetItem(readerData.get(), 1, readerPort);
884  PyList_SetItem(readerData.get(), 2, readerSubsystem);
885  if(reader.allowed_processors != "")
886  {
887  PyObject* readerAllowedProcessors =
888  PyUnicode_FromString(reader.allowed_processors.c_str());
889  PyList_SetItem(readerData.get(), 3, readerAllowedProcessors);
890  }
891  PyDict_SetItem(readerDict.get(), readerName.get(), readerData.get());
892  }
893  PyObjectGuard res1(PyObject_CallMethodObjArgs(
894  daqinterface_ptr_, pName1.get(), readerDict.get(), NULL));
895  __COUT_MULTI_LBL__(0, captureStderrAndStdout_("setdaqcomps"), "setdaqcomps");
896 
897  if(checkPythonError(res1.get()))
898  {
899  std::string err_msg = capturePyErr("setdaqcomps");
900  __GEN_SS__ << "Error calling setdaqcomps: " << err_msg << __E__;
901  __GEN_SS_THROW__;
902  }
903 
904  getDAQState_();
905  __GEN_COUT__ << "Status after setdaqcomps: " << daqinterface_state_ << __E__;
906  } // end Block 2 — release daqinterface_pythonMutex_
907 
908  thread_progress_bar_.step();
909 
910  // Block 3: do_boot (with recover + retry)
911  set_thread_message_("Calling do_boot");
912  __GEN_COUT_INFO__ << "Calling do_boot" << __E__;
913  std::string doBootOutput = "";
914  {
915  std::lock_guard<std::recursive_timed_mutex> lk(daqinterface_pythonMutex_);
916 
917  __GEN_COUT__ << "Status before boot: " << daqinterface_state_ << __E__;
918 
919  // 1. Create Python Strings
920  PyObjectGuard pNameBoot(PyUnicode_FromString("do_boot"));
921  PyObjectGuard pBootArgs(PyUnicode_FromString(
922  (ARTDAQTableBase::ARTDAQ_FCL_PATH + "/boot.txt").c_str()));
923 
924  // 2. First Attempt: Call do_boot
925  PyObjectGuard resBoot1(PyObject_CallMethodObjArgs(
926  daqinterface_ptr_, pNameBoot.get(), pBootArgs.get(), NULL));
927 
928  doBootOutput = captureStderrAndStdout_("do_boot");
929  __COUT_MULTI_LBL__(0, doBootOutput, "do_boot");
930  __GEN_COUT_INFO__ << "do_boot output captured (" << doBootOutput.size()
931  << " chars)" << __E__;
932 
933  if(checkPythonError(resBoot1.get()))
934  {
935  // --- FAILURE PATH ---
936 
937  std::string err1 = capturePyErr("do_boot");
938 
939  __GEN_COUT_INFO__ << "Error on first boot attempt: " << err1
940  << ". Recovering and retrying..." << __E__;
941 
942  // B. Attempt 'do_recover'
943  PyObjectGuard pNameRecover(PyUnicode_FromString("do_recover"));
944  PyObjectGuard resRecover(
945  PyObject_CallMethodObjArgs(daqinterface_ptr_, pNameRecover.get(), NULL));
946  __COUT_MULTI_LBL__(0, captureStderrAndStdout_("do_recover"), "do_recover");
947 
948  if(checkPythonError(resRecover.get()))
949  {
950  // Recover failed - Critical Error
951  std::string errRec = capturePyErr("do_recover");
952 
953  std::stringstream oss;
954  oss << "Error calling recover transition!!!! " << errRec;
955  if(doBootOutput.size() > OUT_ON_ERR_SIZE)
956  oss << "... last " << OUT_ON_ERR_SIZE
957  << " chars: " << doBootOutput.substr(doBootOutput.size() - 1000);
958  else
959  oss << doBootOutput;
960 
961  // Clean up original args before throwing
962  __GEN_SS__ << oss.str() << __E__;
963  __GEN_SS_THROW__;
964  }
965 
966  // C. Retry 'do_boot'
967  thread_progress_bar_.step();
968  set_thread_message_("Calling do_boot (retry)");
969  __GEN_COUT_INFO__ << "Calling do_boot again" << __E__;
970 
971  // Reuse pNameBoot and pBootArgs (valid until end of scope)
972  PyObjectGuard resBoot2(PyObject_CallMethodObjArgs(
973  daqinterface_ptr_, pNameBoot.get(), pBootArgs.get(), NULL));
974 
975  doBootOutput = captureStderrAndStdout_("do_boot (retry)");
976  __COUT_MULTI_LBL__(0, doBootOutput, "do_boot (retry)");
977 
978  if(checkPythonError(resBoot2.get()))
979  {
980  // Second boot failed
981  std::string err2 = capturePyErr("do_boot retry");
982 
983  std::stringstream oss;
984  oss << "Error calling boot transition (2nd try): " << err2;
985  if(doBootOutput.size() > OUT_ON_ERR_SIZE)
986  oss << "... last " << OUT_ON_ERR_SIZE
987  << " chars: " << doBootOutput.substr(doBootOutput.size() - 1000);
988  else
989  oss << doBootOutput;
990 
991  __GEN_SS__ << oss.str() << __E__;
992  __GEN_SS_THROW__;
993  }
994  }
995 
996  getDAQState_();
997  __GEN_COUT_INFO__ << "do_boot block complete, state=" << daqinterface_state_
998  << __E__;
999  if(daqinterface_state_ != "booted")
1000  {
1001  std::cout << "Do boot output on error: \n" << doBootOutput << __E__;
1002  __GEN_SS__ << "DAQInterface boot transition failed! "
1003  << "Status after boot attempt: " << daqinterface_state_ << __E__;
1004 
1005  // Read the critical error saved before recovery output flooded the buffer
1006  {
1007  //Note: older DAQInterface implementations do not define this attribute.
1008  // The failed lookup returns null AND leaves a Python exception pending,
1009  // which would corrupt the recovery calls made after this throw, so it
1010  // must be cleared here.
1011  PyObject* pyCritErr =
1012  PyObject_GetAttrString(daqinterface_ptr_, "last_critical_error");
1013  if(pyCritErr == nullptr)
1014  PyErr_Clear();
1015  else
1016  {
1017  if(PyUnicode_Check(pyCritErr))
1018  {
1019  //PyUnicode_AsUTF8 returns null (and sets an exception) on
1020  // conversion failure; never construct a std::string from that
1021  const char* critErrCStr = PyUnicode_AsUTF8(pyCritErr);
1022  if(critErrCStr == nullptr)
1023  PyErr_Clear();
1024  else if(critErrCStr[0] != '\0')
1025  ss << "\n\nCritical error: " << critErrCStr << __E__;
1026  }
1027  Py_DECREF(pyCritErr);
1028  }
1029  }
1030 
1031  if(doBootOutput.size() > OUT_ON_ERR_SIZE) //last OUT_ON_ERR_SIZE chars only
1032  ss << "... last " << OUT_ON_ERR_SIZE
1033  << " characters: " << doBootOutput.substr(doBootOutput.size() - 1000);
1034  else
1035  ss << doBootOutput;
1036  __GEN_SS_THROW__;
1037  }
1038  __GEN_COUT__ << "Status after boot: " << daqinterface_state_ << __E__;
1039  } // end Block 3 — release daqinterface_pythonMutex_
1040 
1041  thread_progress_bar_.step();
1042 
1043  // Block 4: do_config
1044  set_thread_message_("Calling do_config");
1045  __GEN_COUT_INFO__ << "Calling do_config" << __E__;
1046  std::string doConfigOutput = "";
1047  {
1048  std::lock_guard<std::recursive_timed_mutex> lk(daqinterface_pythonMutex_);
1049 
1050  __GEN_COUT_INFO__ << "do_config python mutex acquired, state="
1051  << daqinterface_state_ << __E__;
1052 
1053  { //do_config call
1054  // RAII wrapper for Python objects to ensure cleanup even on exception
1055 
1056  PyObjectGuard pName3(PyUnicode_FromString("do_config"));
1057  // 2. Create the argument - list containing config name: ["my_config"]
1058  PyObjectGuard pArg(Py_BuildValue("[s]", FAKE_CONFIG_NAME));
1059 
1060  // 3. Call the method
1061  PyObjectGuard res3(PyObject_CallMethodObjArgs(
1062  daqinterface_ptr_, pName3.get(), pArg.get(), NULL));
1063 
1064  // 4. Check for errors FIRST before capturing output (which might clear error state)
1065  if(checkPythonError(res3.get()))
1066  {
1067  // Get the error message before doing anything else
1068  std::string err = capturePyErr("do_config");
1069 
1070  // Now capture output for diagnostics
1071  doConfigOutput = captureStderrAndStdout_("do_config");
1072 
1073  __GEN_SS__ << "Error calling config transition: " << err << __E__;
1074  __GEN_SS_THROW__;
1075  }
1076 
1077  // 5. Success path - capture output
1078  doConfigOutput = captureStderrAndStdout_("do_config");
1079  __COUT_MULTI_LBL__(0, doConfigOutput, "do_config");
1080 
1081  // 6. Success Handling (Safe conversion to string)
1082  // We use PyObject_Str to safely convert any return type (None, Int, String) to text
1083  PyObjectGuard strRes(PyObject_Str(res3.get()));
1084  const char* res_cstr = "";
1085  if(strRes.get())
1086  {
1087  res_cstr = PyUnicode_AsUTF8(strRes.get());
1088  }
1089 
1090  __SUP_COUTT__ << "do_config result=" << (res_cstr ? res_cstr : "N/A")
1091  << __E__;
1092  } //end do_config call
1093 
1094  getDAQState_();
1095  if(daqinterface_state_ != "ready")
1096  {
1097  __GEN_SS__ << "DAQInterface config transition failed!" << __E__
1098  << "Supervisor state: \"" << daqinterface_state_
1099  << "\" != \"ready\" " << __E__;
1100  auto doConfigOutput_recover_i =
1101  doConfigOutput.find("RECOVER transition underway");
1102  if(doConfigOutput_recover_i == std::string::npos)
1103  ss << doConfigOutput;
1104  else if(doConfigOutput_recover_i >
1105  OUT_ON_ERR_SIZE) //last OUT_ON_ERR_SIZE chars only
1106  ss << "... tail of " << OUT_ON_ERR_SIZE << " characters before recovery: "
1107  << doConfigOutput.substr(
1108  doConfigOutput_recover_i - OUT_ON_ERR_SIZE +
1109  std::string("RECOVER transition underway").size(),
1110  OUT_ON_ERR_SIZE);
1111  else
1112  ss << doConfigOutput.substr(
1113  0,
1114  doConfigOutput_recover_i +
1115  std::string("RECOVER transition underway").size());
1116  __GEN_SS_THROW__;
1117  }
1118  __GEN_COUT__ << "Status after config: " << daqinterface_state_ << __E__;
1119  } // end Block 4 — release daqinterface_pythonMutex_
1120 
1121  thread_progress_bar_.complete();
1122  set_thread_message_("Configured");
1123  __GEN_COUT_INFO__ << "Configured." << __E__;
1124 
1125 } // end configuringThread()
1126 catch(const std::runtime_error& e)
1127 {
1128  set_thread_message_("ERROR");
1129  __SS__ << "Error was caught while configuring: " << e.what() << __E__;
1130  __COUT_ERR__ << "\n" << ss.str();
1131  std::lock_guard<std::mutex> lock(thread_mutex_); // lock out for remainder of scope
1132  thread_error_message_ = ss.str();
1133 }
1134 catch(...)
1135 {
1136  set_thread_message_("ERROR");
1137  __SS__ << "Unknown error was caught while configuring. Please checked the logs."
1138  << __E__;
1139  __COUT_ERR__ << "\n" << ss.str();
1140 
1141  artdaq::ExceptionHandler(artdaq::ExceptionHandlerRethrow::no, ss.str());
1142 
1143  std::lock_guard<std::mutex> lock(thread_mutex_); // lock out for remainder of scope
1144  thread_error_message_ = ss.str();
1145 } // end configuringThread() error handling
1146 
1147 //==============================================================================
1148 void ARTDAQSupervisor::transitionHalting(toolbox::Event::Reference /*event*/)
1149 try
1150 {
1151  set_thread_message_("Halting");
1152  __SUP_COUT__ << "Halting..." << __E__;
1153 
1154  // The runner thread continuously acquires the Python mutex for its status
1155  // polls; stop it first so we can take the mutex without contention.
1156  // Bounded: the XML-RPC polls are themselves bounded, but the runner can still
1157  // be inside a Python call while holding the interpreter mutex, and a Halt
1158  // must not hang forever on it.
1159  if(!stop_runner_(HALT_RUNNER_STOP_TIMEOUT_SECONDS))
1160  __SUP_COUT_WARN__ << "DAQInterface runner thread did not stop within "
1161  << HALT_RUNNER_STOP_TIMEOUT_SECONDS
1162  << " seconds; proceeding with the Halt transition." << __E__;
1163 
1164  {
1165  //bounded acquisition, so a configure or heartbeat thread stuck inside Python
1166  // fails the Halt with a clear message instead of hanging it indefinitely
1167  std::unique_lock<std::recursive_timed_mutex> lk(
1168  daqinterface_pythonMutex_,
1169  std::chrono::seconds(HALT_PYTHON_MUTEX_TIMEOUT_SECONDS));
1170  if(!lk.owns_lock())
1171  {
1172  __SUP_SS__ << "Could not acquire the DAQInterface Python mutex within "
1173  << HALT_PYTHON_MUTEX_TIMEOUT_SECONDS
1174  << " seconds during the Halt transition. A configure or "
1175  "heartbeat thread appears to be stuck inside a Python call."
1176  << __E__;
1177  __SUP_SS_THROW__;
1178  }
1179  getDAQState_();
1180  __SUP_COUT__ << "Status before halt: " << daqinterface_state_ << __E__;
1181 
1182  if(daqinterface_state_ == "running")
1183  {
1184  // First stop before halting
1185  PyObjectGuard pName(PyUnicode_FromString("do_stop_running"));
1186  PyObjectGuard res(
1187  PyObject_CallMethodObjArgs(daqinterface_ptr_, pName.get(), NULL));
1188  __COUT_MULTI_LBL__(
1189  0, captureStderrAndStdout_("do_stop_running"), "do_stop_running");
1190 
1191  if(res.get() == NULL)
1192  {
1193  std::string err = capturePyErr();
1194  __SS__ << "Error calling DAQ Interface stop transition: " << err
1195  << __E__;
1196  __SUP_SS_THROW__;
1197  }
1198  }
1199 
1200  // If DAQInterface is already stopped (e.g. after enteringError ran do_recover),
1201  // there are no artdaq processes to send Shutdown to — skip do_command.
1202  if(daqinterface_state_ == "stopped" || daqinterface_state_ == "")
1203  {
1204  __SUP_COUT__ << "DAQInterface already stopped, skipping Shutdown command."
1205  << __E__;
1206  }
1207  else
1208  {
1209  PyObjectGuard pName(PyUnicode_FromString("do_command"));
1210  PyObjectGuard pArg(PyUnicode_FromString("Shutdown"));
1211  PyObjectGuard res(PyObject_CallMethodObjArgs(
1212  daqinterface_ptr_, pName.get(), pArg.get(), NULL));
1213  __COUT_MULTI_LBL__(
1214  0, captureStderrAndStdout_("do_command Shutdown"), "do_command Shutdown");
1215 
1216  if(checkPythonError(res.get()))
1217  {
1218  std::string err = capturePyErr("do_command Shutdown");
1219  __SS__ << "Error calling DAQ Interface halt transition: " << err << __E__;
1220  __SUP_SS_THROW__;
1221  }
1222  }
1223 
1224  getDAQState_();
1225  __SUP_COUT__ << "Status after halt: " << daqinterface_state_ << __E__;
1226  } // release daqinterface_pythonMutex_
1227 
1228  __SUP_COUT__ << "Halted." << __E__;
1229  set_thread_message_("Halted");
1230 } // end transitionHalting()
1231 catch(const std::runtime_error& e)
1232 {
1233  const std::string transitionName = "Halting";
1234  // if halting from Failed state, then ignore errors
1235  if(theStateMachine_.getProvenanceStateName() ==
1236  RunControlStateMachine::FAILED_STATE_NAME ||
1237  theStateMachine_.getProvenanceStateName() ==
1238  RunControlStateMachine::HALTED_STATE_NAME)
1239  {
1240  __SUP_COUT_INFO__ << "Error was caught while halting (but ignoring because "
1241  "previous state was '"
1242  << RunControlStateMachine::FAILED_STATE_NAME
1243  << "'): " << e.what() << __E__;
1244  }
1245  else // if not previously in Failed state, then fail
1246  {
1247  __SUP_SS__ << "Error was caught while " << transitionName << ": " << e.what()
1248  << __E__;
1249  __SUP_COUT_ERR__ << "\n" << ss.str();
1250  theStateMachine_.setErrorMessage(ss.str());
1251  throw toolbox::fsm::exception::Exception(
1252  "Transition Error" /*name*/,
1253  ss.str() /* message*/,
1254  "ARTDAQSupervisorBase::transition" + transitionName /*module*/,
1255  __LINE__ /*line*/,
1256  __FUNCTION__ /*function*/
1257  );
1258  }
1259 } // end transitionHalting() std::runtime_error exception handling
1260 catch(...)
1261 {
1262  const std::string transitionName = "Halting";
1263  // if halting from Failed state, then ignore errors
1264  if(theStateMachine_.getProvenanceStateName() ==
1265  RunControlStateMachine::FAILED_STATE_NAME ||
1266  theStateMachine_.getProvenanceStateName() ==
1267  RunControlStateMachine::HALTED_STATE_NAME)
1268  {
1269  __SUP_COUT_INFO__ << "Unknown error was caught while halting (but ignoring "
1270  "because previous state was '"
1271  << RunControlStateMachine::FAILED_STATE_NAME << "')." << __E__;
1272  }
1273  else // if not previously in Failed state, then fail
1274  {
1275  __SUP_SS__ << "Unknown error was caught while " << transitionName
1276  << ". Please checked the logs." << __E__;
1277  __SUP_COUT_ERR__ << "\n" << ss.str();
1278  theStateMachine_.setErrorMessage(ss.str());
1279 
1280  artdaq::ExceptionHandler(artdaq::ExceptionHandlerRethrow::no, ss.str());
1281 
1282  throw toolbox::fsm::exception::Exception(
1283  "Transition Error" /*name*/,
1284  ss.str() /* message*/,
1285  "ARTDAQSupervisorBase::transition" + transitionName /*module*/,
1286  __LINE__ /*line*/,
1287  __FUNCTION__ /*function*/
1288  );
1289  }
1290 } // end transitionHalting() exception handling
1291 
1292 //==============================================================================
1293 void ARTDAQSupervisor::transitionInitializing(toolbox::Event::Reference /*event*/)
1294 try
1295 {
1296  set_thread_message_("Initializing");
1297  __SUP_COUT__ << "Initializing..." << __E__;
1298  init();
1299  __SUP_COUT__ << "Initialized." << __E__;
1300  set_thread_message_("Initialized");
1301 } // end transitionInitializing()
1302 catch(const std::runtime_error& e)
1303 {
1304  __SS__ << "Error was caught while Initializing: " << e.what() << __E__;
1305  __SS_THROW__;
1306 }
1307 catch(...)
1308 {
1309  __SS__ << "Unknown error was caught while Initializing. Please checked the logs."
1310  << __E__;
1311  artdaq::ExceptionHandler(artdaq::ExceptionHandlerRethrow::no, ss.str());
1312  __SS_THROW__;
1313 } // end transitionInitializing() error handling
1314 
1315 //==============================================================================
1316 void ARTDAQSupervisor::transitionPausing(toolbox::Event::Reference /*event*/)
1317 try
1318 {
1319  set_thread_message_("Pausing");
1320  __SUP_COUT__ << "Pausing..." << __E__;
1321  std::lock_guard<std::recursive_timed_mutex> lk(daqinterface_pythonMutex_);
1322 
1323  getDAQState_();
1324  __SUP_COUT__ << "Status before pause: " << daqinterface_state_ << __E__;
1325 
1326  PyObjectGuard pName(PyUnicode_FromString("do_command"));
1327  PyObjectGuard pArg(PyUnicode_FromString("Pause"));
1328  PyObjectGuard res(
1329  PyObject_CallMethodObjArgs(daqinterface_ptr_, pName.get(), pArg.get(), NULL));
1330  __COUT_MULTI_LBL__(
1331  0, captureStderrAndStdout_("do_command Pause"), "do_command Pause");
1332 
1333  if(checkPythonError(res.get()))
1334  {
1335  std::string err = capturePyErr("do_command Pause");
1336  __SS__ << "Error calling DAQ Interface Pause transition: " << err << __E__;
1337  __SUP_SS_THROW__;
1338  }
1339 
1340  getDAQState_();
1341  __SUP_COUT__ << "Status after pause: " << daqinterface_state_ << __E__;
1342 
1343  __SUP_COUT__ << "Paused." << __E__;
1344  set_thread_message_("Paused");
1345 } // end transitionPausing()
1346 catch(const std::runtime_error& e)
1347 {
1348  __SS__ << "Error was caught while Pausing: " << e.what() << __E__;
1349  __SS_THROW__;
1350 }
1351 catch(...)
1352 {
1353  __SS__ << "Unknown error was caught while Pausing. Please checked the logs." << __E__;
1354  artdaq::ExceptionHandler(artdaq::ExceptionHandlerRethrow::no, ss.str());
1355  __SS_THROW__;
1356 } // end transitionPausing() error handling
1357 
1358 //==============================================================================
1359 void ARTDAQSupervisor::transitionResuming(toolbox::Event::Reference /*event*/)
1360 try
1361 {
1362  set_thread_message_("Resuming");
1363  __SUP_COUT__ << "Resuming..." << __E__;
1364  std::lock_guard<std::recursive_timed_mutex> lk(daqinterface_pythonMutex_);
1365 
1366  getDAQState_();
1367  __SUP_COUT__ << "Status before resume: " << daqinterface_state_ << __E__;
1368  PyObjectGuard pName(PyUnicode_FromString("do_command"));
1369  PyObjectGuard pArg(PyUnicode_FromString("Resume"));
1370  PyObjectGuard res(
1371  PyObject_CallMethodObjArgs(daqinterface_ptr_, pName.get(), pArg.get(), NULL));
1372  __COUT_MULTI_LBL__(
1373  0, captureStderrAndStdout_("do_command Resume"), "do_command Resume");
1374 
1375  if(checkPythonError(res.get()))
1376  {
1377  std::string err = capturePyErr("do_command Resume");
1378  __SS__ << "Error calling DAQ Interface Resume transition: " << err << __E__;
1379  __SUP_SS_THROW__;
1380  }
1381 
1382  getDAQState_();
1383  __SUP_COUT__ << "Status after resume: " << daqinterface_state_ << __E__;
1384  __SUP_COUT__ << "Resumed." << __E__;
1385  set_thread_message_("Resumed");
1386 } // end transitionResuming()
1387 catch(const std::runtime_error& e)
1388 {
1389  __SS__ << "Error was caught while Resuming: " << e.what() << __E__;
1390  __SS_THROW__;
1391 }
1392 catch(...)
1393 {
1394  __SS__ << "Unknown error was caught while Resuming. Please checked the logs."
1395  << __E__;
1396  artdaq::ExceptionHandler(artdaq::ExceptionHandlerRethrow::no, ss.str());
1397  __SS_THROW__;
1398 } // end transitionResuming() error handling
1399 
1400 //==============================================================================
1401 void ARTDAQSupervisor::transitionStarting(toolbox::Event::Reference /*event*/)
1402 try
1403 {
1404  __SUP_COUT__ << "transitionStarting" << __E__;
1405 
1406  // Start sequence, ordered across subsystems (subsystem-iterations under a top-level,
1407  // plain iterations when standalone — see getSubsystemSyncStepIndex()):
1408  // Step 0: idle — let DTCs SoftReset before launching artdaq.
1409  // Step 1: launch artdaq (do_start_running); sub-iterate until complete
1410  // so DTCs don't SoftReset while artdaq is starting.
1411  // Step 2+: idle — DTCs do post-artdaq SoftReset, then CFO launches run plan.
1412 
1413  const unsigned int startIteration =
1415 
1416  if(startIteration == 0)
1417  {
1418  // Step 0: idle — let DTCs SoftReset before we launch artdaq
1419  __SUP_COUT_INFO__ << "Step 0: idle, waiting for DTCs to SoftReset." << __E__;
1420  RunControlStateMachine::indicateSubsystemSyncStepWork();
1421  return;
1422  }
1423 
1424  if(startIteration == 1 && RunControlStateMachine::getSubIterationIndex() == 0)
1425  {
1426  // Step 1: launch artdaq do_start_running
1427  thread_error_message_ = "";
1428  thread_progress_bar_.resetProgressBar(0);
1429  last_thread_progress_update_ = time(0);
1430 
1431  std::thread(&ARTDAQSupervisor::startingThread, this).detach();
1432 
1433  __SUP_COUT_INFO__ << "Step 1: artdaq starting thread launched." << __E__;
1434 
1435  // Sub-iterate to poll thread; the Gateway's broadcast thread stays busy
1436  // with this supervisor's sub-iteration loop, blocking global iteration
1437  // advance until artdaq is done.
1438  RunControlStateMachine::indicateSubIterationWork();
1439  return;
1440  }
1441 
1442  if(startIteration == 1) // sub-iteration > 0: poll the starting thread
1443  {
1444  std::string errorMessage;
1445  {
1446  std::lock_guard<std::mutex> lock(
1447  thread_mutex_); // lock out for remainder of scope
1448  errorMessage = thread_error_message_; // theStateMachine_.getErrorMessage();
1449  }
1450  int progress = thread_progress_bar_.read();
1451  __SUP_COUTV__(errorMessage);
1452  __SUP_COUTV__(progress);
1453  __SUP_COUTV__(thread_progress_bar_.isComplete());
1454 
1455  if(errorMessage == "" && time(0) - last_thread_progress_update_ > 600)
1456  {
1457  __SUP_SS__ << "There has been no update from the start thread for "
1458  << (time(0) - last_thread_progress_update_)
1459  << " seconds, assuming something is wrong and giving up! "
1460  << "Last progress received was " << progress << __E__;
1461  errorMessage = ss.str();
1462  }
1463 
1464  if(errorMessage != "")
1465  {
1466  __SUP_SS__ << "Error was caught in starting thread: " << errorMessage
1467  << __E__;
1468  __SUP_COUT_ERR__ << "\n" << ss.str();
1469 
1470  theStateMachine_.setErrorMessage(ss.str());
1471  throw toolbox::fsm::exception::Exception(
1472  "Transition Error" /*name*/,
1473  ss.str() /* message*/,
1474  "CoreSupervisorBase::transitionStarting" /*module*/,
1475  __LINE__ /*line*/,
1476  __FUNCTION__ /*function*/
1477  );
1478  }
1479 
1480  if(!thread_progress_bar_.isComplete())
1481  {
1482  __SUP_COUT__ << "Step 1: artdaq not done yet..." << __E__;
1483 
1484  RunControlStateMachine::indicateSubIterationWork();
1485 
1486  if(last_thread_progress_read_ != progress)
1487  {
1488  last_thread_progress_read_ = progress;
1489  last_thread_progress_update_ = time(0);
1490  }
1491 
1492  sleep(1 /*seconds*/);
1493  }
1494  else
1495  {
1496  // Thread done — stop sub-iterating. The broadcast thread returns,
1497  // and the Gateway sees this supervisor needs another step to advance to 2+.
1498  __SUP_COUT_INFO__ << "Step 1: artdaq starting transition completed!" << __E__;
1499  __SUP_COUTV__(getProcessInfo_());
1500  RunControlStateMachine::indicateSubsystemSyncStepWork();
1501  }
1502 
1503  return;
1504  }
1505 
1506  // Steps 2+: idle while DTCs do post-artdaq SoftReset and CFO launches run plan.
1507  return;
1508 
1509 } // end transitionStarting()
1510 catch(const std::runtime_error& e)
1511 {
1512  __SS__ << "Error was caught while Starting: " << e.what() << __E__;
1513  __SS_THROW__;
1514 }
1515 catch(...)
1516 {
1517  __SS__ << "Unknown error was caught while Starting. Please checked the logs."
1518  << __E__;
1519  artdaq::ExceptionHandler(artdaq::ExceptionHandlerRethrow::no, ss.str());
1520  __SS_THROW__;
1521 } // end transitionStarting() error handling
1522 
1523 //==============================================================================
1524 void ARTDAQSupervisor::startingThread()
1525 try
1526 {
1527  std::string uid = theConfigurationManager_
1528  ->getNode(ConfigurationManager::XDAQ_APPLICATION_TABLE_NAME +
1529  "/" + CorePropertySupervisorBase::getSupervisorUID() +
1530  "/" + "LinkToSupervisorTable")
1531  .getValueAsString();
1532 
1533  __COUT__ << "Supervisor uid is " << uid << ", getting supervisor table node" << __E__;
1534  const std::string mfSubject_ = supervisorClassNoNamespace_ + "-" + uid;
1535  __GEN_COUT__ << "Starting..." << __E__;
1536  set_thread_message_("Starting");
1537 
1538  thread_progress_bar_.step();
1539  stop_runner_();
1540  {
1541  std::lock_guard<std::recursive_timed_mutex> lk(daqinterface_pythonMutex_);
1542  getDAQState_();
1543  __GEN_COUT__ << "Status before start: " << daqinterface_state_ << __E__;
1544  auto runNumber = SOAPUtilities::translate(theStateMachine_.getCurrentMessage())
1545  .getParameters()
1546  .getValue("RunNumber");
1547 
1548  thread_progress_bar_.step();
1549 
1550  __GEN_COUT_INFO__ << "Calling do_start_running" << __E__;
1551  PyObjectGuard pName(PyUnicode_FromString("do_start_running"));
1552  int run_number = std::stoi(runNumber);
1553  PyObjectGuard pStateArgs(PyLong_FromLong(run_number));
1554  PyObjectGuard res(PyObject_CallMethodObjArgs(
1555  daqinterface_ptr_, pName.get(), pStateArgs.get(), NULL));
1556  std::string doStartOutput;
1557 
1558  thread_progress_bar_.step();
1559 
1560  if(checkPythonError(res.get()))
1561  {
1562  std::string err = capturePyErr("do_start_running");
1563  doStartOutput = captureStderrAndStdout_("do_start_running");
1564  __SS__ << "Error calling start transition: " << err << __E__;
1565  if(doStartOutput.size() > OUT_ON_ERR_SIZE) //last OUT_ON_ERR_SIZE chars only
1566  ss << "... last " << OUT_ON_ERR_SIZE << " characters: "
1567  << doStartOutput.substr(doStartOutput.size() - OUT_ON_ERR_SIZE);
1568  else
1569  ss << doStartOutput;
1570  __GEN_SS_THROW__;
1571  }
1572 
1573  doStartOutput = captureStderrAndStdout_("do_start_running");
1574  __COUT_MULTI_LBL__(0, doStartOutput, "do_start_running");
1575  getDAQState_();
1576 
1577  thread_progress_bar_.step();
1578 
1579  __GEN_COUT__ << "Status after start: " << daqinterface_state_ << __E__;
1580  if(daqinterface_state_ != "running")
1581  {
1582  __SS__ << "DAQInterface start transition failed!" << __E__
1583  << "DAQInterface state: \"" << daqinterface_state_
1584  << "\" != \"running\" " << __E__;
1585  if(doStartOutput.size() > OUT_ON_ERR_SIZE) //last OUT_ON_ERR_SIZE chars only
1586  ss << "... last " << OUT_ON_ERR_SIZE << " characters: "
1587  << doStartOutput.substr(doStartOutput.size() - OUT_ON_ERR_SIZE);
1588  else
1589  ss << doStartOutput;
1590  __GEN_SS_THROW__;
1591  }
1592 
1593  thread_progress_bar_.step();
1594  }
1595  start_runner_();
1596  set_thread_message_("Started");
1597  thread_progress_bar_.step();
1598 
1599  __GEN_COUT_INFO__ << "Started." << __E__;
1600  thread_progress_bar_.complete();
1601 
1602 } // end startingThread()
1603 catch(const std::runtime_error& e)
1604 {
1605  __SS__ << "Error was caught while Starting: " << e.what() << __E__;
1606  __COUT_ERR__ << "\n" << ss.str();
1607  std::lock_guard<std::mutex> lock(thread_mutex_); // lock out for remainder of scope
1608  thread_error_message_ = ss.str();
1609 }
1610 catch(...)
1611 {
1612  __SS__ << "Unknown error was caught while Starting. Please checked the logs."
1613  << __E__;
1614  __COUT_ERR__ << "\n" << ss.str();
1615 
1616  artdaq::ExceptionHandler(artdaq::ExceptionHandlerRethrow::no, ss.str());
1617 
1618  std::lock_guard<std::mutex> lock(thread_mutex_); // lock out for remainder of scope
1619  thread_error_message_ = ss.str();
1620 } // end startingThread() error handling
1621 
1622 //==============================================================================
1623 void ARTDAQSupervisor::transitionStopping(toolbox::Event::Reference /*event*/)
1624 try
1625 {
1626  __SUP_COUT__ << "Stopping..." << __E__;
1627  set_thread_message_("Stopping");
1628  std::lock_guard<std::recursive_timed_mutex> lk(daqinterface_pythonMutex_);
1629  getDAQState_();
1630  __SUP_COUT__ << "Status before stop: " << daqinterface_state_ << __E__;
1631  PyObjectGuard pName(PyUnicode_FromString("do_stop_running"));
1632  PyObjectGuard res(PyObject_CallMethodObjArgs(daqinterface_ptr_, pName.get(), NULL));
1633  __COUT_MULTI_LBL__(0, captureStderrAndStdout_("do_stop_running"), "do_stop_running");
1634 
1635  if(checkPythonError(res.get()))
1636  {
1637  std::string err = capturePyErr("do_stop_running");
1638  __SS__ << "Error calling DAQ Interface stop transition: " << err << __E__;
1639  __SUP_SS_THROW__;
1640  }
1641  getDAQState_();
1642  __SUP_COUT__ << "Status after stop: " << daqinterface_state_ << __E__;
1643  __SUP_COUT__ << "Stopped." << __E__;
1644  set_thread_message_("Stopped");
1645 } // end transitionStopping()
1646 catch(const std::runtime_error& e)
1647 {
1648  __SS__ << "Error was caught while Stopping: " << e.what() << __E__;
1649  __SS_THROW__;
1650 }
1651 catch(...)
1652 {
1653  __SS__ << "Unknown error was caught while Stopping. Please checked the logs."
1654  << __E__;
1655  artdaq::ExceptionHandler(artdaq::ExceptionHandlerRethrow::no, ss.str());
1656  __SS_THROW__;
1657 } // end transitionStopping() error handling
1658 
1659 //==============================================================================
1660 void ots::ARTDAQSupervisor::enteringError(toolbox::Event::Reference /*event*/)
1661 {
1662  __SUP_COUT__ << "Entering error recovery state" << __E__;
1663  std::lock_guard<std::recursive_timed_mutex> lk(daqinterface_pythonMutex_);
1664  getDAQState_();
1665  __SUP_COUT__ << "Status before error: " << daqinterface_state_ << __E__;
1666 
1667  PyObjectGuard pName(PyUnicode_FromString("do_recover"));
1668  PyObjectGuard res(PyObject_CallMethodObjArgs(daqinterface_ptr_, pName.get(), NULL));
1669  __COUT_MULTI_LBL__(0, captureStderrAndStdout_("do_recover"), "do_recover");
1670 
1671  if(checkPythonError(res.get()))
1672  {
1673  std::string err = capturePyErr("do_recover");
1674  //do not throw exception when entering error, because failing DAQ interface could be the reason for error in first place
1675  __SUP_COUT_WARN__ << "Error calling DAQ Interface recover transition: " << err
1676  << __E__;
1677  return;
1678  }
1679 
1680  getDAQState_();
1681  __SUP_COUT__ << "Status after error: " << daqinterface_state_ << __E__;
1682  __SUP_COUT__ << "EnteringError DONE." << __E__;
1683 
1684 } // end enteringError()
1685 
1686 std::vector<SupervisorInfo::SubappInfo> ots::ARTDAQSupervisor::getSubappInfo(void)
1687 {
1688  auto apps = getAndParseProcessInfo_();
1689 
1690  std::map<int, SupervisorInfo::SubappInfo> subapp_infos;
1691  for(auto& app : apps)
1692  {
1694 
1695  info.name = app.label;
1696  info.detail = "Rank " + std::to_string(app.rank) + ", subsystem " +
1697  std::to_string(app.subsystem);
1698  info.lastStatusTime = time(0);
1699  info.progress = 100;
1700  info.status = artdaqStateToOtsState(app.state);
1701  info.url = "http://" + app.host + ":" + std::to_string(app.port) + "/RPC2";
1702  info.class_name = "ARTDAQ " + labelToProcType_(app.label);
1703 
1704  subapp_infos[app.rank] = info;
1705  }
1706 
1707  std::vector<SupervisorInfo::SubappInfo> output;
1708  for(auto& [rank, info] : subapp_infos)
1709  {
1710  output.push_back(info);
1711  }
1712  return output;
1713 } //end getSubappInfo()
1714 
1715 //==============================================================================
1716 // Helper function to check if a Python call failed
1717 // Returns true if there was an error (result is NULL or PyErr_Occurred)
1718 // NOTE: Does NOT clear the Python error state - caller should call capturePyErr() to fetch it
1719 bool ots::ARTDAQSupervisor::checkPythonError(PyObject* result)
1720 {
1721  if(result == NULL || PyErr_Occurred())
1722  {
1723  // Assume result is cleaned up by its PyObjectGuard if needed
1724 
1725  // Note: We keep the Python error state so caller can extract the message with capturePyErr()
1726  return true; // Error occurred
1727  }
1728  return false; // No error
1729 } //end checkPythonError()
1730 
1731 //==============================================================================
1737 std::string ots::ARTDAQSupervisor::capturePyErr(std::string label /* = "" */,
1738  std::string* summaryOut /* = nullptr */)
1739 {
1740  std::string err_msg = "Unknown Python Error";
1741  PyObject * pType, *pValue, *pTraceback;
1742  PyErr_Fetch(&pType, &pValue, &pTraceback);
1743  PyErr_NormalizeException(&pType, &pValue, &pTraceback);
1744 
1745  if(summaryOut)
1746  *summaryOut = err_msg;
1747 
1748  if(pType != NULL)
1749  {
1750  if(summaryOut)
1751  {
1752  std::string summary = ((PyTypeObject*)pType)->tp_name;
1753  if(pValue != NULL)
1754  {
1755  PyObjectGuard pStr(PyObject_Str(pValue));
1756  const char* valueCStr =
1757  pStr.get() ? PyUnicode_AsUTF8(pStr.get()) : nullptr;
1758  if(valueCStr)
1759  {
1760  std::string value(valueCStr);
1761  size_t b = value.find_first_not_of(" \t\r\n");
1762  size_t e = value.find_last_not_of(" \t\r\n");
1763  if(b != std::string::npos)
1764  summary += ": " + value.substr(b, e - b + 1);
1765  }
1766  else
1767  PyErr_Clear();
1768  }
1769  *summaryOut = summary;
1770  }
1771 
1772  // Format the full traceback like Python does
1773  PyObjectGuard traceback_module(PyImport_ImportModule("traceback"));
1774  if(traceback_module.get() != NULL)
1775  {
1776  PyObjectGuard format_exception(
1777  PyObject_GetAttrString(traceback_module.get(), "format_exception"));
1778  if(format_exception.get() != NULL)
1779  {
1780  PyObjectGuard formatted(
1781  PyObject_CallFunctionObjArgs(format_exception.get(),
1782  pType,
1783  pValue ? pValue : Py_None,
1784  pTraceback ? pTraceback : Py_None,
1785  NULL));
1786  if(formatted.get() != NULL)
1787  {
1788  // formatted is a list of strings, join them
1789  PyObjectGuard empty_string(PyUnicode_FromString(""));
1790  PyObjectGuard joined(
1791  PyUnicode_Join(empty_string.get(), formatted.get()));
1792  if(joined.get() != NULL)
1793  {
1794  const char* traceback_cstr = PyUnicode_AsUTF8(joined.get());
1795  if(traceback_cstr)
1796  err_msg = traceback_cstr;
1797  }
1798  }
1799  }
1800  }
1801 
1802  // Fallback to simple message if traceback formatting failed
1803  if(err_msg == "Unknown Python Error" && pValue != NULL)
1804  {
1805  PyObjectGuard pStr(PyObject_Str(pValue));
1806  if(pStr.get() != NULL)
1807  {
1808  const char* error_cstr = PyUnicode_AsUTF8(pStr.get());
1809  if(error_cstr)
1810  err_msg = error_cstr;
1811  }
1812  }
1813  }
1814 
1815  Py_XDECREF(pType);
1816  Py_XDECREF(pValue);
1817  Py_XDECREF(pTraceback);
1818 
1819  // Add label prefix if provided
1820  if(!label.empty())
1821  err_msg = label + ":\n" + err_msg;
1822 
1823  return err_msg;
1824 } //end capturePyErr()
1825 
1826 //==============================================================================
1827 std::string ots::ARTDAQSupervisor::captureStderrAndStdout_(std::string label /* = "" */)
1828 {
1829  if(!stringIO_out_)
1830  return ""; // Not defined
1831  // If a Python error is already pending, do not consume it here
1832  if(PyErr_Occurred())
1833  return "";
1834  if(label.size())
1835  label += ' '; //for nice printing
1836 
1837  std::string outString = "";
1838  PyObjectGuard out(PyObject_CallMethod(stringIO_out_, "getvalue", NULL));
1839 
1840  if(checkPythonError(out.get()))
1841  {
1842  // Error getting output - clear the error and return empty string
1843  capturePyErr("captureStderrAndStdout getvalue");
1844  return "";
1845  }
1846 
1847  const char* text = PyUnicode_AsUTF8(out.get());
1848 
1849  return text ? text : "";
1850 } //end captureStderrAndStdout_()
1851 
1852 void ots::ARTDAQSupervisor::getDAQState_()
1853 {
1854  __SUP_COUTS__(50) << "Getting DAQInterface python lock" << __E__;
1855  std::lock_guard<std::recursive_timed_mutex> lk(daqinterface_pythonMutex_);
1856  __SUP_COUTS__(50) << "Have DAQInterface python lock" << __E__;
1857 
1858  if(daqinterface_ptr_ == NULL)
1859  {
1860  daqinterface_state_ = "";
1861  __SUP_COUT_WARN__ << "daqinterface_ptr_ is not initialized!" << __E__;
1862  return;
1863  }
1864 
1865  // Prepare Python Strings ONCE (Move outside loop to prevent 5x memory leak)
1866  PyObjectGuard pName(PyUnicode_FromString("state"));
1867  PyObjectGuard pArg(PyUnicode_FromString("DAQInterface"));
1868 
1869  // WARNING: Verify your Python 'state' method accepts an argument.
1870  // If 'def state(self):' is the signature, passing pArg will fail.
1871  // If so, call: PyObject_CallMethodObjArgs(daqinterface_ptr_, pName, NULL);
1872 
1873  int tries = 0;
1874  while(tries < 5)
1875  {
1876  // Call the method
1877  PyObjectGuard res(
1878  PyObject_CallMethodObjArgs(daqinterface_ptr_, pName.get(), pArg.get(), NULL));
1879 
1880  if(checkPythonError(res.get()))
1881  {
1882  tries++;
1883 
1884  // Get the error message
1885  std::string err_msg = capturePyErr("state");
1886 
1887  std::ostringstream ss;
1888  ss << "Attempt n " << tries
1889  << ". Error calling 'state'. Python Exception: " << err_msg;
1890 
1891  if(tries >= 5)
1892  {
1893  __COUT_ERR__ << ss.str() << __E__; // Log error on final fail
1894  daqinterface_state_ = "ERROR"; // distinct from empty
1895  }
1896  else
1897  {
1898  __COUT__ << ss.str() << __E__; // Log warning
1899  usleep(100000); // 100ms
1900  }
1901  continue;
1902  }
1903 
1904  // --- SUCCESS CASE ---
1905 
1906  // Safely convert result to string (res might not be a string!)
1907  PyObjectGuard strRes(PyObject_Str(res.get())); // Force conversion to string
1908  if(strRes.get())
1909  {
1910  daqinterface_state_ = std::string(PyUnicode_AsUTF8(strRes.get()));
1911  }
1912  else
1913  {
1914  // Rare case: object couldn't be converted to string
1915  daqinterface_state_ = "UNKNOWN";
1916  }
1917 
1918  __SUP_COUTS__(20) << "getDAQState_ state=" << daqinterface_state_ << __E__;
1919  break;
1920  }
1921 
1922  // Cleanup the string objects we created
1923 } //end getDAQState_()
1924 
1925 //==============================================================================
1926 std::string ots::ARTDAQSupervisor::getProcessInfo_(void)
1927 {
1928  __SUP_COUTS__(50) << "Getting DAQInterface state lock" << __E__;
1929  std::lock_guard<std::recursive_timed_mutex> lk(daqinterface_pythonMutex_);
1930  __SUP_COUTS__(50) << "Have DAQInterface state lock" << __E__;
1931 
1932  if(daqinterface_ptr_ == nullptr)
1933  {
1934  return "";
1935  }
1936 
1937  PyObjectGuard pName(PyUnicode_FromString("artdaq_process_info"));
1938  PyObjectGuard pArg(PyUnicode_FromString("DAQInterface"));
1939  PyObjectGuard pArg2(PyBool_FromLong(true));
1940  PyObjectGuard res(PyObject_CallMethodObjArgs(
1941  daqinterface_ptr_, pName.get(), pArg.get(), pArg2.get(), NULL));
1942 
1943  if(checkPythonError(res.get()))
1944  {
1945  std::string err = capturePyErr("artdaq_process_info");
1946  __SS__ << "Error calling artdaq_process_info function: " << err << __E__;
1947  __SUP_SS_THROW__;
1948  return "";
1949  }
1950  //cache status as latest
1951  std::lock_guard<std::mutex> lock(daqinterface_statusMutex_);
1952  daqinterface_status_ = std::string(PyUnicode_AsUTF8(res.get()));
1953  return daqinterface_status_;
1954 } // end getProcessInfo_()
1955 
1956 std::string ots::ARTDAQSupervisor::artdaqStateToOtsState(std::string state)
1957 {
1958  if(state == "nonexistent" || state == "nonexistant")
1959  return RunControlStateMachine::INITIAL_STATE_NAME;
1960  if(state == "Ready")
1961  return "Configured";
1962  if(state == "Running")
1963  return RunControlStateMachine::RUNNING_STATE_NAME;
1964  if(state == "Paused")
1965  return RunControlStateMachine::PAUSED_STATE_NAME;
1966  if(state == "Stopped")
1967  return RunControlStateMachine::HALTED_STATE_NAME;
1968 
1969  TLOG(TLVL_WARNING) << "Unrecognized state name " << state;
1970  return RunControlStateMachine::FAILED_STATE_NAME;
1971 }
1972 
1973 std::string ots::ARTDAQSupervisor::labelToProcType_(std::string label)
1974 {
1975  if(label_to_proc_type_map_.count(label))
1976  {
1977  return label_to_proc_type_map_[label];
1978  }
1979  return "UNKNOWN";
1980 }
1981 
1982 //==============================================================================
1984 std::list<ots::ARTDAQSupervisor::DAQInterfaceProcessInfo>
1985 ots::ARTDAQSupervisor::getAndParseProcessInfo_()
1986 {
1987  std::list<ots::ARTDAQSupervisor::DAQInterfaceProcessInfo> output;
1988  // full acquire from getProcessInfo_ creates mutex locking up!
1989  // auto info = getProcessInfo_();
1990  std::string info;
1991 
1992  std::unique_lock<std::recursive_timed_mutex> lk(daqinterface_pythonMutex_,
1993  std::try_to_lock);
1994  if(!lk.owns_lock()) //if lock not availabe, just report last status
1995  {
1996  __COUTS__(50) << "Do not have python lock." << __E__;
1997  std::lock_guard<std::mutex> lock(daqinterface_statusMutex_);
1998  info = daqinterface_status_;
1999  }
2000  else //have lock! so retrieve Python Interface status
2001  {
2002  __COUTS__(50) << "Have python lock!" << __E__;
2003  info = getProcessInfo_();
2004  }
2005  __COUTVS__(20, info);
2006 
2007  auto procs = tokenize_(info);
2008 
2009  // 0: Whole string
2010  // 1: Process Label
2011  // 2: Process host
2012  // 3: Process port
2013  // 4: Process subsystem
2014  // 5: Process Rank
2015  // 6: Process state
2016  std::regex re("(.*?) at ([^:]*):(\\d+) \\(subsystem (\\d+), rank (\\d+)\\): (.*)");
2017 
2018  for(auto& proc : procs)
2019  {
2020  std::smatch match;
2021  if(std::regex_match(proc, match, re))
2022  {
2023  DAQInterfaceProcessInfo info;
2024 
2025  info.label = match[1];
2026  info.host = match[2];
2027  info.port = std::stoi(match[3]);
2028  info.subsystem = std::stoi(match[4]);
2029  info.rank = std::stoi(match[5]);
2030  info.state = match[6];
2031 
2032  output.push_back(info);
2033  }
2034  }
2035  return output;
2036 } // end getAndParseProcessInfo_()
2037 
2038 //==============================================================================
2040  std::unique_ptr<artdaq::CommanderInterface>>>
2041 ots::ARTDAQSupervisor::makeCommandersFromProcessInfo()
2042 {
2043  std::list<
2044  std::pair<DAQInterfaceProcessInfo, std::unique_ptr<artdaq::CommanderInterface>>>
2045  output;
2046  auto infos = getAndParseProcessInfo_();
2047 
2048  for(auto& info : infos)
2049  {
2050  artdaq::Commandable cm;
2051  fhicl::ParameterSet ps;
2052 
2053  ps.put<std::string>("commanderPluginType", "xmlrpc");
2054  ps.put<int>("id", info.port);
2055  ps.put<std::string>("server_url", info.host);
2056 
2057  output.emplace_back(std::make_pair<DAQInterfaceProcessInfo,
2058  std::unique_ptr<artdaq::CommanderInterface>>(
2059  std::move(info), artdaq::MakeCommanderPlugin(ps, cm)));
2060  }
2061 
2062  return output;
2063 } // end makeCommandersFromProcessInfo()
2064 
2065 //==============================================================================
2066 // getConfiguredArtdaqHosts
2067 // Returns the de-duplicated set of hostnames of all enabled artdaq processes,
2068 // taken from the active configuration via ARTDAQTableBase::extractARTDAQInfo
2069 // (the same call used by configuringThread()). Unlike makeCommandersFromProcessInfo()
2070 // -- which reads the live DAQInterface status and is empty when DAQInterface is
2071 // not running -- this reflects the configuration's intended deployment. The
2072 // configuration does NOT carry the runtime xmlrpc commander ports, so this is
2073 // used for host discovery only (e.g. to drive 'ots -tt <hosts>').
2074 std::set<std::string> ots::ARTDAQSupervisor::getConfiguredArtdaqHosts(void)
2075 {
2076  std::set<std::string> hosts;
2077  try
2078  {
2079  ConfigurationTree supervisorNode = getSupervisorTableNode();
2080  ARTDAQTableBase::ARTDAQInfo info = ARTDAQTableBase::extractARTDAQInfo(
2081  supervisorNode, false /*getStatusFalseNodes*/, false /*doWriteFHiCL*/);
2082  for(const auto& typeProcs : info.processes)
2083  for(const auto& proc : typeProcs.second)
2084  if(proc.status && !proc.hostname.empty())
2085  hosts.insert(proc.hostname);
2086  }
2087  catch(const std::exception& e)
2088  {
2089  __SUP_COUT_ERR__ << "Failed to extract configured artdaq hosts: " << e.what()
2090  << __E__;
2091  }
2092  catch(...)
2093  {
2094  __SUP_COUT_ERR__
2095  << "Failed to extract configured artdaq hosts (unknown exception)." << __E__;
2096  }
2097  __SUP_COUT__ << "Configured artdaq hosts: " << StringMacros::setToString(hosts)
2098  << __E__;
2099  return hosts;
2100 } // end getConfiguredArtdaqHosts()
2101 
2102 //==============================================================================
2103 std::list<std::string> ots::ARTDAQSupervisor::tokenize_(std::string const& input)
2104 {
2105  size_t pos = 0;
2106  std::list<std::string> output;
2107 
2108  while(pos != std::string::npos && pos < input.size())
2109  {
2110  auto newpos = input.find('\n', pos);
2111  if(newpos != std::string::npos)
2112  {
2113  output.emplace_back(input, pos, newpos - pos);
2114  // TLOG(TLVL_TRACE) << "tokenize_: " << output.back();
2115  pos = newpos + 1;
2116  }
2117  else
2118  {
2119  output.emplace_back(input, pos);
2120  // TLOG(TLVL_TRACE) << "tokenize_: " << output.back();
2121  pos = newpos;
2122  }
2123  }
2124  return output;
2125 } // end tokenize_()
2126 
2127 //==============================================================================
2140 void ots::ARTDAQSupervisor::daqinterfaceRunner_(std::shared_ptr<RunnerControl> control)
2141 try
2142 {
2143  //Note: control->exited is set by the thread wrapper in start_runner_() only
2144  // after this function AND its function-try-block catch handler have returned.
2145  // A guard local here would fire before the handler (locals are destroyed
2146  // before a function-try-block handler runs), letting stop_runner_() join
2147  // unbounded while the handler is still sending the error to the Gateway.
2148 
2149  TLOG(TLVL_TRACE) << "Runner thread starting";
2150  unsigned int consecutiveStatusFailures = 0;
2151  while(control->running)
2152  {
2153  if(daqinterface_ptr_ != NULL)
2154  {
2155  std::unique_lock<std::recursive_timed_mutex> lk(daqinterface_pythonMutex_);
2156  getDAQState_();
2157  std::string state_before = daqinterface_state_;
2158 
2159  __SUP_COUTS__(2) << "Runner state_before=" << state_before
2160  << " state now=" << daqinterface_state_
2161  << " ?= running, ready, or booted" << __E__;
2162 
2163  if(daqinterface_state_ == "running" || daqinterface_state_ == "ready" ||
2164  daqinterface_state_ == "booted")
2165  {
2166  // Only the C-API call itself is inside this try: the result checks
2167  // below throw our own runtime_errors, which must not be caught here
2168  // (a second capturePyErr() would then just append "Unknown Python
2169  // Error" to a message that already explained the failure).
2170  PyObject* resRaw = nullptr;
2171  try
2172  {
2173  TLOG(TLVL_TRACE) << "Calling DAQInterface::check_proc_exceptions";
2174  PyObjectGuard pName(PyUnicode_FromString("check_proc_exceptions"));
2175  resRaw =
2176  PyObject_CallMethodObjArgs(daqinterface_ptr_, pName.get(), NULL);
2177  TLOG(TLVL_TRACE)
2178  << "Done with DAQInterface::check_proc_exceptions call";
2179  }
2180  catch(cet::exception& ex)
2181  {
2182  control->running = false;
2183  std::string err = capturePyErr("check_proc_exceptions");
2184  __SS__ << "An cet::exception occurred while calling "
2185  "check_proc_exceptions function "
2186  << ex.explain_self() << ": " << err << __E__;
2187  __SUP_SS_THROW__;
2188  }
2189  catch(std::exception& ex)
2190  {
2191  control->running = false;
2192  std::string err = capturePyErr("check_proc_exceptions");
2193  __SS__ << "An std::exception occurred while calling "
2194  "check_proc_exceptions function: "
2195  << ex.what() << "\n\n"
2196  << err << __E__;
2197  __SUP_SS_THROW__;
2198  }
2199  catch(...)
2200  {
2201  control->running = false;
2202  std::string err = capturePyErr("check_proc_exceptions");
2203  __SS__ << "An unknown Error occurred while calling "
2204  "check_proc_exceptions function: "
2205  << err << __E__;
2206  __SUP_SS_THROW__;
2207  }
2208 
2209  PyObjectGuard res(resRaw);
2210  std::string pollOutput = captureStderrAndStdout_("check_proc_exceptions");
2211 
2212  if(res.get() == NULL)
2213  {
2214  //DAQInterface raised, e.g. a process repeatedly refused connection
2215  // or a process loss violated the boot-file requirements. The full
2216  // traceback goes to the supervisor log; the operator gets the
2217  // exception message plus DAQInterface's own diagnostic output.
2218  control->running = false;
2219  std::string summary;
2220  std::string traceback =
2221  capturePyErr("check_proc_exceptions", &summary);
2222  //Note: DAQInterface's own diagnostics (print_log) go through the
2223  // swig_artdaq bridge into the message facility, so they are
2224  // already in the Console and supervisor log as Error entries
2225  // from "DAQInterface_partition_N"; pollOutput only holds plain
2226  // Python prints and is usually empty here.
2227  __SUP_COUT_ERR__
2228  << "check_proc_exceptions raised."
2229  << (pollOutput.size() ? "\nCaptured Python stdout:\n" + pollOutput
2230  : "")
2231  << "\nPython traceback:\n"
2232  << traceback << __E__;
2233  __SS__
2234  << "DAQInterface reported a problem with the artdaq processes: "
2235  << summary << (pollOutput.size() ? "\n\n" + pollOutput : "")
2236  << "\n\nSee the preceding DAQInterface_partition_" << partition_
2237  << " error messages in the Console (or the ARTDAQSupervisor "
2238  "log) for the affected process and its logfile."
2239  << __E__;
2240  __SUP_SS_THROW__;
2241  }
2242 
2243  if(PyObject_IsTrue(res.get()) == 1)
2244  {
2245  consecutiveStatusFailures = 0;
2246  __COUT_MULTI_LBL__(1, pollOutput, "check_proc_exceptions");
2247  }
2248  else
2249  {
2250  //False: at least one process did not answer or reported
2251  // "Error." DAQInterface has already logged which. Tolerate
2252  // a few consecutive misses, since the status proxy timeout
2253  // is short and a busy process can miss one poll.
2254  ++consecutiveStatusFailures;
2255  __SUP_COUT_WARN__
2256  << "DAQInterface XML-RPC status poll reported a problem ("
2257  << consecutiveStatusFailures << " of "
2258  << RUNNER_MAX_CONSECUTIVE_STATUS_FAILURES
2259  << " consecutive allowed):\n"
2260  << pollOutput << __E__;
2261  if(consecutiveStatusFailures >=
2262  RUNNER_MAX_CONSECUTIVE_STATUS_FAILURES)
2263  {
2264  control->running = false;
2265  __SS__ << "artdaq process status could not be confirmed for "
2266  << consecutiveStatusFailures
2267  << " consecutive polls. Last DAQInterface output:\n"
2268  << pollOutput << __E__;
2269  __SUP_SS_THROW__;
2270  }
2271  }
2272 
2273  lk.unlock();
2274  getDAQState_();
2275  if(daqinterface_state_ != state_before)
2276  {
2277  control->running = false;
2278  __SS__ << "DAQInterface state unexpectedly changed from "
2279  << state_before << " to " << daqinterface_state_
2280  << ". Check supervisor log file for more info!" << __E__;
2281  __SUP_SS_THROW__;
2282  break;
2283  }
2284  }
2285  }
2286  else
2287  {
2288  __SUP_COUT__ << "daqinterface_ptr_ is null" << __E__;
2289  break;
2290  }
2291  usleep(1000000);
2292  }
2293  control->running = false;
2294  TLOG(TLVL_TRACE) << "Runner thread complete";
2295 } // end daqinterfaceRunner_()
2296 catch(...)
2297 {
2298  __SS__ << "An error occurred in "
2299  "start_runner_/daqinterfaceRunner_ thread "
2300  << __E__;
2301  try
2302  {
2303  throw;
2304  }
2305  catch(const std::runtime_error& e)
2306  {
2307  ss << "Here is the error: " << e.what() << __E__;
2308  }
2309  catch(...)
2310  {
2311  ss << "Unexpected error!" << __E__;
2312  }
2313  __COUT_ERR__ << ss.str();
2314 
2315  {
2316  std::lock_guard<std::mutex> lock(
2317  thread_mutex_); // lock out for remainder of scope
2318  thread_error_message_ = ss.str();
2319  }
2320 
2321  theStateMachine_.setErrorMessage(ss.str());
2322 
2323  sendAsyncExceptionToGateway( //0 for both pause/stop indicates error
2324  ss.str(),
2325  0 /* isPauseException */,
2326  0 /* isStopException */);
2327 
2328 } // end daqinterfaceRunner_() catch
2329 
2330 //==============================================================================
2341 bool ots::ARTDAQSupervisor::stop_runner_(unsigned int timeoutSeconds)
2342 {
2343  if(!runner_control_)
2344  return true;
2345 
2346  //take ownership of this runner's block and thread, so nothing below can be
2347  // confused with a runner started later
2348  std::shared_ptr<RunnerControl> control = std::move(runner_control_);
2349  std::unique_ptr<std::thread> thread = std::move(runner_thread_);
2350 
2351  control->running = false;
2352 
2353  const auto deadline =
2354  std::chrono::steady_clock::now() + std::chrono::seconds(timeoutSeconds);
2355  while(!control->exited && std::chrono::steady_clock::now() < deadline)
2356  std::this_thread::sleep_for(std::chrono::milliseconds(50));
2357 
2358  if(!control->exited)
2359  {
2360  __SUP_COUT_WARN__ << "DAQInterface runner thread did not exit within "
2361  << timeoutSeconds
2362  << " seconds; it is likely blocked inside a Python call "
2363  "holding the interpreter mutex. Abandoning the thread "
2364  "rather than hanging this transition indefinitely."
2365  << __E__;
2366  if(thread && thread->joinable())
2367  thread->detach(); //never destroy a joinable std::thread
2368  //remember this for destroy()/~ARTDAQSupervisor(): the abandoned runner may
2369  // still be inside Python, so the interpreter must not be torn down
2370  runner_abandoned_ = true;
2371  return false;
2372  }
2373 
2374  if(thread && thread->joinable())
2375  thread->join(); //already exited, so this returns promptly
2376  return true;
2377 } // end stop_runner_()
2378 
2379 //==============================================================================
2380 void ots::ARTDAQSupervisor::start_runner_()
2381 {
2382  stop_runner_();
2383 
2384  //a fresh control block per runner: an abandoned runner keeps its own and can
2385  // neither be restarted by this call nor report exit on behalf of this one
2386  runner_control_ = std::make_shared<RunnerControl>();
2387  runner_thread_ = std::make_unique<std::thread>([this, control = runner_control_]() {
2388  //set exited only after daqinterfaceRunner_() has fully returned, which
2389  // includes its function-try-block catch handler (error logging and the
2390  // SOAP send to the Gateway). stop_runner_() may then join promptly.
2391  struct RunnerExitFlag
2392  {
2393  std::atomic<bool>& flag;
2394  ~RunnerExitFlag() { flag = true; }
2395  } runnerExitFlag{control->exited};
2396  daqinterfaceRunner_(control);
2397  });
2398 } // end start_runner_()
2399 
2400 //==============================================================================
2401 std::string ARTDAQSupervisor::getServiceDataFilePath() const
2402 {
2403  return StringMacros::getPersistentSystemVariablesFilePath();
2404 } // end getServiceDataFilePath()
2405 
2406 //==============================================================================
2407 void ARTDAQSupervisor::initArtdaqSystemVariables()
2408 {
2409  loadArtdaqSystemVariables();
2410 
2411  auto& ns = StringMacros::systemVariables_[ARTDAQ_SYSVAR_NAMESPACE];
2412  __SUP_COUT__ << "Artdaq system variables initialized: "
2413  << StringMacros::mapToString(ns) << __E__;
2414 } // end initArtdaqSystemVariables()
2415 
2416 //==============================================================================
2417 void ARTDAQSupervisor::loadArtdaqSystemVariables()
2418 {
2420  __SUP_COUT__ << "Loaded artdaq system variables from " << getServiceDataFilePath()
2421  << __E__;
2422  else
2423  __SUP_COUT__ << "No persisted artdaq system variables file found at "
2424  << getServiceDataFilePath() << __E__;
2425 } // end loadArtdaqSystemVariables()
2426 
2427 //==============================================================================
2428 void ARTDAQSupervisor::saveArtdaqSystemVariables()
2429 {
2430  std::string filePath = getServiceDataFilePath();
2431  std::ofstream file(filePath);
2432  if(!file.is_open())
2433  {
2434  __SUP_SS__ << "Failed to open file for writing artdaq system variables: "
2435  << filePath << __E__;
2436  __SUP_SS_THROW__;
2437  }
2438 
2439  for(auto& [key, value] : StringMacros::systemVariables_[ARTDAQ_SYSVAR_NAMESPACE])
2440  file << key << "=" << value << "\n";
2441 
2442  __SUP_COUT__ << "Saved artdaq system variables to " << filePath << __E__;
2443 } // end saveArtdaqSystemVariables()
2444 
2445 //==============================================================================
2446 void ARTDAQSupervisor::forceSupervisorPropertyValues(void)
2447 {
2448  CorePropertySupervisorBase::addSupervisorProperty(
2449  CorePropertySupervisorBase::SUPERVISOR_PROPERTIES.AutomatedRequestTypes,
2450  "getSystemVariables | getJsonDocuments");
2451 } // end forceSupervisorPropertyValues()
2452 
2453 //==============================================================================
2454 void ARTDAQSupervisor::request(const std::string& requestType,
2455  cgicc::Cgicc& cgiIn,
2456  HttpXmlDocument& xmlOut,
2457  const WebUsers::RequestUserInfo& /*userInfo*/)
2458 try
2459 {
2460  __SUP_COUT__ << "ARTDAQSupervisor request: " << requestType << __E__;
2461 
2462  if(requestType == "getSystemVariables")
2463  {
2464  for(auto& [key, value] : StringMacros::systemVariables_[ARTDAQ_SYSVAR_NAMESPACE])
2465  xmlOut.addTextElementToData("artdaq_" + key, value);
2466  }
2467  else if(requestType == "setSystemVariable")
2468  {
2469  std::string key = CgiDataUtilities::postData(cgiIn, "key");
2470  std::string value = CgiDataUtilities::postData(cgiIn, "value");
2471 
2472  if(key.empty())
2473  {
2474  xmlOut.addTextElementToData("Error", "Variable key must not be empty.");
2475  return;
2476  }
2477  for(char c : key)
2478  if(!std::isalnum(c) && c != '_')
2479  {
2480  xmlOut.addTextElementToData(
2481  "Error",
2482  "Variable key must contain only alphanumeric characters and "
2483  "underscores.");
2484  return;
2485  }
2486 
2487  StringMacros::systemVariables_[ARTDAQ_SYSVAR_NAMESPACE][key] = value;
2488  saveArtdaqSystemVariables();
2489 
2490  __SUP_COUT__ << "Set artdaq system variable " << key << " = " << value << __E__;
2491  xmlOut.addTextElementToData("Success", "Variable '" + key + "' set.");
2492  }
2493  else if(requestType == "getJsonDocuments")
2494  {
2495  auto* ifc = ConfigurationInterface::getInstance();
2496 
2497  std::set<std::string> allTableNames = ifc->getAllTableNames();
2498 
2499  for(const auto& tableName : allTableNames)
2500  {
2501  if(tableName.find(TableBase::JSON_DOC_PREPEND) != 0)
2502  continue;
2503 
2504  std::string docName = tableName.substr(TableBase::JSON_DOC_PREPEND.size());
2505 
2506  TableBase tmpTable(true, tableName);
2507  std::set<TableVersion> versions = ifc->getVersions(&tmpTable);
2508 
2509  std::string versionList;
2510  for(const auto& v : versions)
2511  {
2512  if(!versionList.empty())
2513  versionList += ",";
2514  versionList += v.toString();
2515  }
2516 
2517  xmlOut.addTextElementToData("jsonDoc_name", docName);
2518  xmlOut.addTextElementToData("jsonDoc_versions", versionList);
2519  }
2520  }
2521  else
2522  {
2523  __SUP_SS__ << "Unknown request type '" << requestType << "' for ARTDAQSupervisor."
2524  << __E__;
2525  __SUP_COUT__ << ss.str();
2526  xmlOut.addTextElementToData("Error", ss.str());
2527  }
2528 }
2529 catch(const std::runtime_error& e)
2530 {
2531  __SUP_SS__ << "Error handling request '" << requestType << "': " << e.what() << __E__;
2532  __SUP_COUT_ERR__ << ss.str();
2533  xmlOut.addTextElementToData("Error", ss.str());
2534 }
2535 catch(...)
2536 {
2537  __SUP_SS__ << "Unknown error handling request '" << requestType << "'." << __E__;
2538  __SUP_COUT_ERR__ << ss.str();
2539  xmlOut.addTextElementToData("Error", ss.str());
2540 } // end request()
void request(const std::string &requestType, cgicc::Cgicc &cgiIn, HttpXmlDocument &xmlOut, const WebUsers::RequestUserInfo &userInfo) override
virtual void transitionHalting(toolbox::Event::Reference event) override
virtual void transitionInitializing(toolbox::Event::Reference event) override
static const std::string ARTDAQ_FCL_PATH
Tree-path rule is, if the last link in the path is a group link with a specified group ID,...
static std::string getBootFileContentFromInfo(const ARTDAQInfo &info, const std::string &setupScript, int debugLevel)
static std::string postData(cgicc::Cgicc &cgi, const std::string &needle)
ConfigurationTree getNode(const std::string &nodeString, bool doNotThrowOnBrokenUIDLinks=false) const
"root/parent/parent/"
ConfigurationTree getNode(const std::string &nodeName, bool doNotThrowOnBrokenUIDLinks=false) const
navigating between nodes
const std::string & getValueAsString(bool returnLinkTableValue=false) const
void getValue(T &value) const
ITRACEController * theTRACEController_
only define for an app that receives a command
bool isComplete()
get functions
Definition: ProgressBar.cc:88
void step()
thread safe
Definition: ProgressBar.cc:74
int read()
if stepsToComplete==0, then define any progress as 50%, thread safe
Definition: ProgressBar.cc:120
void complete()
declare complete, thread safe
Definition: ProgressBar.cc:95
bool isFirstIteration(void) const
true on the very first call of a transition (all indices at their starting value)
unsigned int getSubsystemSyncStepIndex(void) const
defines used also by OtsConfigurationWizardSupervisor
void INIT_MF(const char *name)
static std::string setToString(const std::set< T > &setToReturn, const std::string &delimeter=", ")
setToString ~
static bool loadPersistentSystemVariables(void)
loads persisted 'artdaq' namespace systemVariables_; returns false if no file found
Definition: StringMacros.cc:49
static std::string mapToString(const std::map< std::string, T > &mapToReturn, const std::string &primaryDelimeter=", ", const std::string &secondaryDelimeter=": ")
std::string name
Also key in map.