launch_config.cpp
Go to the documentation of this file.
1 // Aggregates all information needed to start and monitor nodes
2 // Author: Max Schwarz <max.schwarz@uni-bonn.de>
3 
4 #include "launch_config.h"
5 #include "substitution.h"
6 #include "yaml_params.h"
7 #include "bytes_parser.h"
8 #include "string_utils.h"
9 
10 #include <ros/package.h>
11 #include <ros/names.h>
12 
13 #include <cctype>
14 #include <cstdarg>
15 #include <cstdio>
16 #include <fstream>
17 
18 #include <sys/wait.h>
19 
20 #include <boost/regex.hpp>
21 #include <boost/algorithm/string/case_conv.hpp>
22 #include <boost/algorithm/string/trim.hpp>
23 #include <boost/lexical_cast.hpp>
24 
25 #include <yaml-cpp/yaml.h>
26 
27 namespace rosmon
28 {
29 namespace launch
30 {
31 
32 const char* UNSET_MARKER = "~~~~~ ROSMON-UNSET ~~~~~";
33 
34 ParseContext ParseContext::enterScope(const std::string& prefix)
35 {
36  ParseContext ret = *this;
37  ret.m_prefix = ros::names::clean(ret.m_prefix + prefix) + "/";
38 
39  return ret;
40 }
41 
42 void ParseContext::parseScopeAttributes(TiXmlElement* e, ParseContext& attr_ctx)
43 {
44  if(const char* stopTimeout = e->Attribute("rosmon-stop-timeout"))
45  {
46  double seconds;
47  try
48  {
49  seconds = boost::lexical_cast<double>(attr_ctx.evaluate(stopTimeout));
50  }
51  catch(boost::bad_lexical_cast&)
52  {
53  throw error("bad rosmon-stop-timeout value '{}'", stopTimeout);
54  }
55  if(seconds < 0)
56  throw error("negative rosmon-stop-timeout value '{}'", stopTimeout);
57 
58  m_stopTimeout = seconds;
59  }
60 
61  if(const char* memoryLimit = e->Attribute("rosmon-memory-limit"))
62  {
63  uint64_t memoryLimitByte;
64  bool ok;
65  std::tie(memoryLimitByte, ok) = parseMemory(static_cast<std::string>(memoryLimit));
66  if(!ok)
67  {
68  throw error("{} cannot be parsed as a memory limit", memoryLimit);
69  }
70 
71  m_memoryLimit = memoryLimitByte;
72  }
73 
74  if(const char* cpuLimit = e->Attribute("rosmon-cpu-limit"))
75  {
76  double cpuLimitPct;
77  try
78  {
79  cpuLimitPct = boost::lexical_cast<double>(attr_ctx.evaluate(cpuLimit));
80  }
81  catch(boost::bad_lexical_cast&)
82  {
83  throw error("bad rosmon-cpu-limit value '{}'", cpuLimit);
84  }
85 
86  if(cpuLimitPct < 0)
87  throw error("negative rosmon-cpu-limit value'{}'", cpuLimit);
88 
89  m_cpuLimit = cpuLimitPct;
90  }
91 
92  if(const char* coredumpsEnabled = e->Attribute("enable-coredumps"))
93  {
94  m_coredumpsEnabled = attr_ctx.parseBool(coredumpsEnabled, e->Row());
95  }
96 }
97 
98 std::string ParseContext::evaluate(const std::string& tpl, bool simplifyWhitespace)
99 {
100  std::string simplified;
101  if(simplifyWhitespace)
102  simplified = string_utils::simplifyWhitespace(tpl);
103  else
104  simplified = tpl;
105 
106  try
107  {
108  return parseSubstitutionArgs(simplified, *this);
109  }
110  catch(SubstitutionException& e)
111  {
112  throw error("Substitution error: {}", e.what());
113  }
114 }
115 
116 bool ParseContext::parseBool(const std::string& value, int line)
117 {
118  std::string expansion = evaluate(value);
119 
120  // We are lenient here and accept the pythonic forms "True" and "False"
121  // as well, since roslaunch seems to do the same. Even the roslaunch/XML
122  // spec mentions True/False in the examples, even though they are not
123  // valid options for if/unless and other boolean attributes...
124  // http://wiki.ros.org/roslaunch/XML/rosparam
125 
126  if(expansion == "1" || expansion == "true" || expansion == "True")
127  return true;
128 
129  if(expansion == "0" || expansion == "false" || expansion == "False")
130  return false;
131 
132  throw error("Unknown truth value '%s'", expansion.c_str());
133 }
134 
135 bool ParseContext::shouldSkip(TiXmlElement* e)
136 {
137  const char* if_cond = e->Attribute("if");
138  const char* unless_cond = e->Attribute("unless");
139 
140  if(if_cond && unless_cond)
141  {
142  throw error("both if= and unless= specified, don't know what to do");
143  }
144 
145  if(if_cond)
146  {
147  return !parseBool(if_cond, e->Row());
148  }
149 
150  if(unless_cond)
151  {
152  return parseBool(unless_cond, e->Row());
153  }
154 
155  return false;
156 }
157 
158 void ParseContext::setArg(const std::string& name, const std::string& value, bool override)
159 {
160  auto it = m_args.find(name);
161  if(it == m_args.end())
162  m_args[name] = value;
163  else if(override || it->second == UNSET_MARKER)
164  m_args[name] = value;
165 }
166 
167 void ParseContext::setEnvironment(const std::string& name, const std::string& value)
168 {
169  m_environment[name] = value;
170 }
171 
172 void ParseContext::setRemap(const std::string& from, const std::string& to)
173 {
174  m_remappings[from] = to;
175 }
176 
177 std::string ParseContext::anonName(const std::string& base)
178 {
179  auto it = m_anonNames.find(base);
180  if(it == m_anonNames.end())
181  {
182  auto name = base + "_" + m_config->generateAnonHash();
183 
184  it = m_anonNames.emplace(base, name).first;
185  }
186 
187  return it->second;
188 }
189 
190 
192  : m_rootContext(this)
193  , m_anonGen(std::random_device()())
194 {
195  const char* ROS_NAMESPACE = getenv("ROS_NAMESPACE");
196  if(ROS_NAMESPACE)
197  {
198  // Someone set ROS_NAMESPACE, we should respect it.
199  // This may happen in nested situations, e.g. rosmon launching rosmon.
200  m_rootContext = m_rootContext.enterScope(ROS_NAMESPACE);
201  }
202 }
203 
204 void LaunchConfig::setArgument(const std::string& name, const std::string& value)
205 {
206  m_rootContext.setArg(name, value, true);
207 }
208 
210 {
211  m_rootContext.setStopTimeout(timeout);
212 }
213 
214 void LaunchConfig::setDefaultCPULimit(double CPULimit)
215 {
216  m_rootContext.setCPULimit(CPULimit);
217 }
218 
219 void LaunchConfig::setDefaultMemoryLimit(uint64_t memoryLimit)
220 {
221  m_rootContext.setMemoryLimit(memoryLimit);
222 }
223 
224 void LaunchConfig::parse(const std::string& filename, bool onlyArguments)
225 {
226  m_rootContext.setFilename(filename);
227 
228  TiXmlDocument document(filename);
229 
230  TiXmlBase::SetCondenseWhiteSpace(false);
231 
232  if(!document.LoadFile())
233  {
234  throw m_rootContext.error("Could not load launch file: {}", document.ErrorDesc());
235  }
236 
238  parse(document.RootElement(), &m_rootContext, onlyArguments);
239 
240  // Parse top-level rosmon-specific attributes
241  parseTopLevelAttributes(document.RootElement());
242 
243  if(!onlyArguments)
244  fmt::print("Loaded launch file in {:f}s\n", (ros::WallTime::now() - start).toSec());
245 }
246 
247 void LaunchConfig::parseString(const std::string& input, bool onlyArguments)
248 {
249  m_rootContext.setFilename("[string]");
250 
251  TiXmlDocument document;
252 
253  TiXmlBase::SetCondenseWhiteSpace(false);
254 
255  document.Parse(input.c_str());
256 
257  if(document.Error())
258  {
259  throw m_rootContext.error("Could not parse string input: {}", document.ErrorDesc());
260  }
261 
263  parse(document.RootElement(), &m_rootContext, onlyArguments);
264 
265  // Parse top-level rosmon-specific attributes
266  parseTopLevelAttributes(document.RootElement());
267 
268  if(!onlyArguments)
269  fmt::print("Loaded launch file in {:f}s\n", (ros::WallTime::now() - start).toSec());
270 }
271 
272 void LaunchConfig::parseTopLevelAttributes(TiXmlElement* element)
273 {
274  const char* name = element->Attribute("rosmon-name");
275  if(name)
276  m_rosmonNodeName = name;
277 
278  const char* windowTitle = element->Attribute("rosmon-window-title");
279  if(windowTitle)
281 
282  const char* disableUI = element->Attribute("rosmon-disable-ui");
283  if(disableUI)
284  m_disableUI = m_rootContext.parseBool(disableUI, element->Row());
285 }
286 
287 void LaunchConfig::parse(TiXmlElement* element, ParseContext* ctx, bool onlyArguments)
288 {
289  ctx->parseScopeAttributes(element, *ctx);
290 
291  // First pass: Parse arguments
292  for(TiXmlNode* n = element->FirstChild(); n; n = n->NextSibling())
293  {
294  TiXmlElement* e = n->ToElement();
295  if(!e)
296  continue;
297 
298  if(ctx->shouldSkip(e))
299  continue;
300 
301  ctx->setCurrentElement(e);
302 
303  if(e->ValueStr() == "arg")
304  parseArgument(e, *ctx);
305  }
306 
307  if(onlyArguments)
308  return;
309 
310  // Second pass: everything else
311  for(TiXmlNode* n = element->FirstChild(); n; n = n->NextSibling())
312  {
313  TiXmlElement* e = n->ToElement();
314  if(!e)
315  continue;
316 
317  if(ctx->shouldSkip(e))
318  continue;
319 
320  ctx->setCurrentElement(e);
321 
322  if(e->ValueStr() == "node")
323  parseNode(e, *ctx);
324  else if(e->ValueStr() == "param")
325  parseParam(e, *ctx);
326  else if(e->ValueStr() == "rosparam")
327  parseROSParam(e, *ctx);
328  else if(e->ValueStr() == "group")
329  {
330  ParseContext cctx = *ctx;
331 
332  if(const char* ns = e->Attribute("ns"))
333  cctx = cctx.enterScope(ctx->evaluate(ns));
334 
335  cctx.parseScopeAttributes(e, *ctx);
336 
337  parse(e, &cctx);
338  }
339  else if(e->ValueStr() == "include")
340  parseInclude(e, *ctx);
341  else if(e->ValueStr() == "env")
342  parseEnv(e, *ctx);
343  else if(e->ValueStr() == "remap")
344  parseRemap(e, *ctx);
345  }
346 }
347 
348 void LaunchConfig::parseNode(TiXmlElement* element, ParseContext& attr_ctx)
349 {
350  const char* name = element->Attribute("name");
351  const char* pkg = element->Attribute("pkg");
352  const char* type = element->Attribute("type");
353  const char* args = element->Attribute("args");
354  const char* ns = element->Attribute("ns");
355  const char* respawn = element->Attribute("respawn");
356  const char* respawnDelay = element->Attribute("respawn_delay");
357  const char* required = element->Attribute("required");
358  const char* launchPrefix = element->Attribute("launch-prefix");
359  const char* cwd = element->Attribute("cwd");
360  const char* clearParams = element->Attribute("clear_params");
361  const char* output = element->Attribute("output");
362 
363  if(!name || !pkg || !type)
364  {
365  throw attr_ctx.error("name, pkg, type are mandatory for node elements!");
366  }
367 
368  // Attributes are evaluated *outside* of the node context, so keep that one
369  ParseContext ctx = attr_ctx;
370 
371  if(ns)
372  ctx = ctx.enterScope(ctx.evaluate(ns));
373 
374  std::string fullNamespace = ctx.prefix().substr(0, ctx.prefix().length()-1);
375 
376  // Enter scope
377  ctx = ctx.enterScope(ctx.evaluate(name));
378 
379  // Parse scoped attributes such as rosmon-stop-timeout
380  ctx.parseScopeAttributes(element, attr_ctx);
381 
382  Node::Ptr node = std::make_shared<Node>(
383  attr_ctx.evaluate(name), attr_ctx.evaluate(pkg), attr_ctx.evaluate(type)
384  );
385 
386  // Check name uniqueness
387  {
388  auto it = std::find_if(m_nodes.begin(), m_nodes.end(), [&](const Node::Ptr& n) {
389  return n->namespaceString() == fullNamespace && n->name() == node->name();
390  });
391 
392  if(it != m_nodes.end())
393  {
394  throw ctx.error("node name '{}' is not unique", node->name());
395  }
396  }
397 
398  node->setStopTimeout(ctx.stopTimeout());
399  node->setMemoryLimit(ctx.memoryLimit());
400  node->setCPULimit(ctx.cpuLimit());
401  node->setCoredumpsEnabled(ctx.coredumpsEnabled());
402 
403  if(args)
404  node->addExtraArguments(ctx.evaluate(args));
405 
406  if(!fullNamespace.empty())
407  node->setNamespace(fullNamespace);
408 
409  if(respawn)
410  {
411  node->setRespawn(attr_ctx.parseBool(respawn, element->Row()));
412 
413  if(respawnDelay)
414  {
415  double seconds;
416  try
417  {
418  seconds = boost::lexical_cast<double>(attr_ctx.evaluate(respawnDelay));
419  }
420  catch(boost::bad_lexical_cast&)
421  {
422  throw ctx.error("bad respawn_delay value '{}'", respawnDelay);
423  }
424 
425  node->setRespawnDelay(ros::WallDuration(seconds));
426  }
427  }
428 
429  if(required && attr_ctx.parseBool(required, element->Row()))
430  {
431  node->setRequired(true);
432  }
433 
434  // We have to parse rosparam tags first, see #118
435  for(TiXmlNode* n = element->FirstChild(); n; n = n->NextSibling())
436  {
437  TiXmlElement* e = n->ToElement();
438  if(!e)
439  continue;
440 
441  if(ctx.shouldSkip(e))
442  continue;
443 
444  ctx.setCurrentElement(e);
445 
446  if(e->ValueStr() == "rosparam")
447  parseROSParam(e, ctx);
448  }
449 
450  // Now we can parse everything else.
451  for(TiXmlNode* n = element->FirstChild(); n; n = n->NextSibling())
452  {
453  TiXmlElement* e = n->ToElement();
454  if(!e)
455  continue;
456 
457  if(ctx.shouldSkip(e))
458  continue;
459 
460  ctx.setCurrentElement(e);
461 
462  if(e->ValueStr() == "param")
463  parseParam(e, ctx, PARAM_IN_NODE);
464  else if(e->ValueStr() == "remap")
465  parseRemap(e, ctx);
466  else if(e->ValueStr() == "env")
467  parseEnv(e, ctx);
468  }
469 
470  ctx.setCurrentElement(element);
471 
472  // Set environment *after* parsing the node children (may contain env tags)
473  node->setExtraEnvironment(ctx.environment());
474 
475  if(launchPrefix)
476  node->setLaunchPrefix(attr_ctx.evaluate(launchPrefix));
477 
478  if(cwd)
479  node->setWorkingDirectory(attr_ctx.evaluate(cwd));
480 
481  if(clearParams)
482  node->setClearParams(attr_ctx.parseBool(clearParams, element->Row()));
483 
485  {
486  node->setStdoutDisplayed(false); // output=log is default
487  }
488 
489  if(output)
490  {
491  std::string outputStr = attr_ctx.evaluate(output);
492  if(outputStr == "screen")
493  node->setStdoutDisplayed(true);
494  else if(outputStr == "log")
495  node->setStdoutDisplayed(false);
496  else
497  throw ctx.error("Invalid output attribute value: '{}'", outputStr);
498  }
499 
500  node->setRemappings(ctx.remappings());
501 
502  m_nodes.push_back(node);
503 }
504 
505 static XmlRpc::XmlRpcValue autoXmlRpcValue(const std::string& fullValue)
506 {
507  std::string fullValueLowercase = boost::algorithm::to_lower_copy(fullValue);
508  if(fullValueLowercase == "true")
509  return XmlRpc::XmlRpcValue(true);
510  else if(fullValueLowercase == "false")
511  return XmlRpc::XmlRpcValue(false);
512  else
513  {
514  try { return boost::lexical_cast<int>(fullValue); }
515  catch(boost::bad_lexical_cast&) {}
516 
517  try { return boost::lexical_cast<double>(fullValue); }
518  catch(boost::bad_lexical_cast&) {}
519 
520  return fullValue;
521  }
522 }
523 
524 void LaunchConfig::parseParam(TiXmlElement* element, ParseContext& ctx, ParamContext paramContext)
525 {
526  const char* name = element->Attribute("name");
527  const char* value = element->Attribute("value");
528  const char* command = element->Attribute("command");
529  const char* textfile = element->Attribute("textfile");
530  const char* binfile = element->Attribute("binfile");
531  const char* type = element->Attribute("type");
532 
533  if(!name)
534  {
535  throw ctx.error("name is mandatory for param elements");
536  }
537 
538  int numCommands = (value ? 1 : 0) + (command ? 1 : 0) + (textfile ? 1 : 0) + (binfile ? 1 : 0);
539  if(numCommands > 1) // == 0 is checked below, don't duplicate.
540  {
541  throw ctx.error("<param> tags need exactly one of value=, command=, textfile=, binfile= attributes.");
542  }
543 
544  std::string fullName = ctx.evaluate(name);
545  if(fullName.empty())
546  {
547  throw ctx.error("param name is empty");
548  }
549 
550  // Expand relative paths. roslaunch ignores leading / when inside a
551  // <node> tag - god only knows why.
552  if(fullName[0] != '/' || paramContext == PARAM_IN_NODE)
553  {
554  // Same with "/" (see above)
555  if(paramContext == PARAM_IN_NODE && fullName[0] == '/')
556  {
557  ctx.warning("leading slashes in <param> names are ignored inside <node> contexts for roslaunch compatibility.");
558  fullName = fullName.substr(1);
559  }
560  else if(fullName[0] == '~')
561  {
562  // We silently ignore "~" at the beginning of the name
563  fullName = fullName.substr(1);
564  }
565 
566  fullName = ctx.prefix() + fullName;
567  }
568 
569  std::string errorStr;
570  if(!ros::names::validate(fullName, errorStr))
571  {
572  throw ctx.error("Expanded parameter name '{}' is invalid: {}",
573  fullName, errorStr
574  );
575  }
576 
577  std::string fullType;
578  if(type)
579  fullType = ctx.evaluate(type);
580 
581  if(value)
582  {
583  // Simple case - immediate value.
584  if(fullType == "yaml")
585  {
586  std::string fullValue = ctx.evaluate(value, false);
587 
588  YAML::Node n;
589  try
590  {
591  n = YAML::Load(fullValue);
592  }
593  catch(YAML::ParserException& e)
594  {
595  throw ctx.error("Invalid YAML: {}", e.what());
596  }
597 
598  loadYAMLParams(ctx, n, fullName);
599  }
600  else
601  {
602  // Note: roslaunch strips leading/trailing whitespace of all simple
603  // parameters. Furthermore, line feeds and tabs are replaced with
604  // space characters.
605  m_params[fullName] = paramToXmlRpc(ctx,
607  string_utils::strip(ctx.evaluate(value, false))
608  ),
609  fullType
610  );
611 
612  // A dynamic parameter of the same name gets overwritten now
613  m_paramJobs.erase(fullName);
614  }
615 
616  return;
617  }
618 
619  if(binfile)
620  {
621  // Also simple - binary files are always mapped to base64 XmlRpcValue.
622 
623  std::string fullFile = ctx.evaluate(binfile);
624 
625  m_paramJobs[fullName] = std::async(std::launch::deferred,
626  [=]() -> XmlRpc::XmlRpcValue {
627  std::ifstream stream(fullFile, std::ios::binary | std::ios::ate);
628  if(!stream)
629  throw ctx.error("Could not open file '{}'", fullFile);
630 
631  std::vector<char> data(stream.tellg(), 0);
632  stream.seekg(0, std::ios::beg);
633 
634  stream.read(data.data(), data.size());
635 
636  // Creates base64 XmlRpcValue
637  return {data.data(), static_cast<int>(data.size())};
638  }
639  );
640 
641  m_params.erase(fullName);
642  return;
643  }
644 
645  // Dynamic parameters are more complicated. We split the computation in
646  // two parts:
647  // 1) Compute the value as std::string
648  // 2) Convert to desired type (or dissect into multiple parameters in
649  // case of YAML-typed parameters).
650 
651  auto computeString = std::make_shared<std::future<std::string>>();
652 
653  // On ROS < Lunar, type="..." is not respected for command= and textfile=
654  // attributes. We support that anyway, but print a nice warning for users.
655  // See GH issue #138.
656 
657 #if !ROS_VERSION_MINIMUM(1,13,0)
658  if(type)
659  {
660  ctx.warning(
661  "On ROS versions prior to Lunar, roslaunch does not respect the "
662  "type attribute on <param> tags with command= or textfile= "
663  "actions. However, rosmon does support it and will create the "
664  "properly typed parameter {}.",
665  fullName
666  );
667  }
668 #endif
669 
670  if(command)
671  {
672  // Run a command and retrieve the results.
673  std::string fullCommand = ctx.evaluate(command);
674 
675  // Commands may take a while - that is why we use std::async here.
676  *computeString = std::async(std::launch::deferred,
677  [=]() -> std::string {
678  std::stringstream buffer;
679 
680  int pipe_fd[2];
681  if(pipe(pipe_fd) != 0)
682  throw ctx.error("Could not create pipe: {}", strerror(errno));
683 
684  int pid = fork();
685  if(pid < 0)
686  throw ctx.error("Could not fork: {}", strerror(errno));
687 
688  if(pid == 0)
689  {
690  // Child
691  close(pipe_fd[0]);
692  if(pipe_fd[1] != STDOUT_FILENO)
693  {
694  dup2(pipe_fd[1], STDOUT_FILENO);
695  close(pipe_fd[1]);
696  }
697 
698  char* argp[] = {strdup("sh"), strdup("-c"), strdup(fullCommand.c_str()), nullptr};
699 
700  execvp("sh", argp); // should never return
701  throw ctx.error("Could not execvp '{}': {}", fullCommand, strerror(errno));
702  }
703 
704  close(pipe_fd[1]);
705 
706  timeval timeout;
707  memset(&timeout, 0, sizeof(timeout));
708  timeout.tv_sec = 0;
709  timeout.tv_usec = 500 * 1000;
710 
711  while(true)
712  {
713  fd_set fds;
714  FD_ZERO(&fds);
715  FD_SET(pipe_fd[0], &fds);
716 
717  int ret = select(pipe_fd[0]+1, &fds, nullptr, nullptr, &timeout);
718  if(ret < 0)
719  throw ctx.error("Could not select(): {}", strerror(errno));
720 
721  if(ret == 0)
722  {
723  fmt::print("Still loading parameter '{}'...\n", fullName);
724 
725  timeout.tv_sec = 3;
726  continue;
727  }
728 
729  char buf[1024];
730  ret = read(pipe_fd[0], buf, sizeof(buf)-1);
731  if(ret < 0)
732  throw ctx.error("Could not read: {}", strerror(errno));
733  if(ret == 0)
734  break;
735 
736  buf[ret] = 0;
737  buffer << buf;
738  }
739 
740  close(pipe_fd[0]);
741 
742  int status = 0;
743  if(waitpid(pid, &status, 0) < 0)
744  throw ctx.error("Could not waitpid(): {}", strerror(errno));
745 
746  if(!WIFEXITED(status) || WEXITSTATUS(status) != 0)
747  {
748  throw ctx.error("<param> command failed (exit status {})",
749  WEXITSTATUS(status)
750  );
751  }
752 
753  return buffer.str();
754  }
755  );
756 
757  // A fixed parameter of the same name gets overwritten now
758  m_params.erase(fullName);
759  }
760  else if(textfile)
761  {
762  std::string fullFile = ctx.evaluate(textfile);
763 
764  *computeString = std::async(std::launch::deferred,
765  [=]() -> std::string {
766  std::ifstream stream(fullFile);
767  if(!stream)
768  throw ctx.error("Could not open file '{}'", fullFile);
769 
770  std::stringstream buffer;
771  buffer << stream.rdbuf();
772 
773  return buffer.str();
774  }
775  );
776  }
777  else
778  {
779  throw ctx.error("<param> needs either command, value, binfile, or textfile");
780  }
781 
782  if(fullType == "yaml")
783  {
784  m_yamlParamJobs.push_back(std::async(std::launch::deferred,
785  [=]() -> YAMLResult {
786  std::string yamlString = computeString->get();
787 
788  YAML::Node n;
789  try
790  {
791  n = YAML::Load(yamlString);
792  }
793  catch(YAML::ParserException& e)
794  {
795  throw ctx.error("Read invalid YAML from process or file: {}",
796  e.what()
797  );
798  }
799 
800  return {fullName, n};
801  }
802  ));
803  }
804  else
805  {
806  m_paramJobs[fullName] = std::async(std::launch::deferred,
807  [=]() -> XmlRpc::XmlRpcValue {
808  return paramToXmlRpc(ctx, computeString->get(), fullType);
809  }
810  );
811 
812  // A fixed parameter of the same name gets overwritten now
813  m_params.erase(fullName);
814  }
815 }
816 
817 XmlRpc::XmlRpcValue LaunchConfig::paramToXmlRpc(const ParseContext& ctx, const std::string& value, const std::string& type)
818 {
819  if(type.empty())
820  return autoXmlRpcValue(value);
821 
822  // Fixed type.
823  try
824  {
825  if(type == "int")
826  return boost::lexical_cast<int>(string_utils::strip(value));
827  else if(type == "double")
828  return boost::lexical_cast<double>(string_utils::strip(value));
829  else if(type == "bool" || type == "boolean")
830  {
831  std::string value_lowercase = boost::algorithm::to_lower_copy(
832  string_utils::strip(value)
833  );
834 
835  if(value_lowercase == "true")
836  return true;
837  else if(value_lowercase == "false")
838  return false;
839  else
840  throw ctx.error("invalid boolean value '{}'", value);
841  }
842  else if(type == "str" || type == "string")
843  return value;
844  else
845  {
846  throw ctx.error("invalid param type '{}'", type);
847  }
848  }
849  catch(boost::bad_lexical_cast& e)
850  {
851  throw ctx.error("could not convert param value '{}' to type '{}'",
852  value, type
853  );
854  }
855 }
856 
857 void LaunchConfig::parseROSParam(TiXmlElement* element, ParseContext& ctx)
858 {
859  const char* command = element->Attribute("command");
860 
861  if(!command || strcmp(command, "load") == 0)
862  {
863  const char* file = element->Attribute("file");
864  std::string fullFile;
865 
866  std::string contents;
867  if(file)
868  {
869  fullFile = ctx.evaluate(file);
870  std::ifstream stream(fullFile);
871  if(!stream)
872  throw ctx.error("Could not open file '{}'", fullFile);
873 
874  std::stringstream buffer;
875  buffer << stream.rdbuf();
876 
877  contents = buffer.str();
878  }
879  else
880  {
881  if(const char* t = element->GetText())
882  contents = t;
883  }
884 
885  // roslaunch silently ignores empty files (which are not valid YAML),
886  // so do the same here.
887  if(string_utils::isOnlyWhitespace(contents))
888  return;
889 
890  const char* subst_value = element->Attribute("subst_value");
891  if(subst_value && ctx.parseBool(subst_value, element->Row()))
892  contents = ctx.evaluate(contents, false);
893 
894  YAML::Node n;
895  try
896  {
897  n = YAML::Load(contents);
898  }
899  catch(YAML::ParserException& e)
900  {
901  throw ctx.error("Could not parse YAML: {}", e.what());
902  }
903 
904  ParseContext nameContext = ctx;
905  const char* ns = element->Attribute("ns");
906  if(ns)
907  nameContext = nameContext.enterScope(ctx.evaluate(ns));
908 
909  const char* name = element->Attribute("param");
910  if(name)
911  nameContext = nameContext.enterScope(ctx.evaluate(name));
912 
913  // Remove trailing / from prefix to get param name
914  try
915  {
916  loadYAMLParams(ctx, n, nameContext.prefix().substr(0, nameContext.prefix().length()-1));
917  }
918  catch(ParseException& e)
919  {
920  if(file)
921  {
922  throw ctx.error("error while parsing rosparam input file {}: {}",
923  fullFile, e.what()
924  );
925  }
926  else
927  {
928  throw ctx.error("error while parsing YAML input from launch file: {}",
929  e.what()
930  );
931  }
932  }
933  }
934  else
935  throw ctx.error("Unsupported rosparam command '{}'", command);
936 }
937 
938 void LaunchConfig::loadYAMLParams(const ParseContext& ctx, const YAML::Node& n, const std::string& prefix)
939 {
940  switch(n.Type())
941  {
942  case YAML::NodeType::Map:
943  {
944  // Pass 1: Load any anchor references
945  for(YAML::const_iterator it = n.begin(); it != n.end(); ++it)
946  {
947  if(it->first.as<std::string>() == "<<")
948  {
949  loadYAMLParams(ctx, it->second, prefix);
950  }
951  }
952 
953  // Pass 2: Everything else.
954  for(YAML::const_iterator it = n.begin(); it != n.end(); ++it)
955  {
956  auto key = it->first.as<std::string>();
957  if(key != "<<")
958  {
959  // Load "global" params without prefix (see #130)
960  if(!key.empty() && key[0] == '/')
961  loadYAMLParams(ctx, it->second, key);
962  else
963  loadYAMLParams(ctx, it->second, prefix + "/" + it->first.as<std::string>());
964  }
965  }
966 
967  break;
968  }
969  case YAML::NodeType::Sequence:
970  case YAML::NodeType::Scalar:
971  {
972  m_params[prefix] = yamlToXmlRpc(ctx, n);
973 
974  // A dynamic parameter of the same name gets overwritten now
975  m_paramJobs.erase(prefix);
976  break;
977  }
978  case YAML::NodeType::Null:
979  {
980  // Nothing to do, empty node
981  break;
982  }
983  default:
984  {
985  throw ctx.error("invalid yaml node type");
986  }
987  }
988 }
989 
990 void LaunchConfig::parseInclude(TiXmlElement* element, ParseContext ctx)
991 {
992  const char* file = element->Attribute("file");
993  const char* ns = element->Attribute("ns");
994  const char* passAllArgs = element->Attribute("pass_all_args");
995  const char* clearParams = element->Attribute("clear_params");
996 
997  if(!file)
998  throw ctx.error("<include> file attribute is mandatory");
999 
1000  if(clearParams && ctx.parseBool(clearParams, element->Row()))
1001  {
1002  throw ctx.error("<include clear_params=\"true\" /> is not supported and probably a bad idea.");
1003  }
1004 
1005  std::string fullFile = ctx.evaluate(file);
1006 
1007  ParseContext childCtx = ctx;
1008  if(ns)
1009  childCtx = childCtx.enterScope(ctx.evaluate(ns));
1010 
1011  // Parse any arguments
1012  childCtx.parseScopeAttributes(element, ctx);
1013 
1014  // If pass_all_args is not set, delete the current arguments.
1015  if(!passAllArgs || !ctx.parseBool(passAllArgs, element->Row()))
1016  childCtx.clearArguments();
1017 
1018  // Now set all explicitly mentioned arguments.
1019  for(TiXmlNode* n = element->FirstChild(); n; n = n->NextSibling())
1020  {
1021  TiXmlElement* e = n->ToElement();
1022  if(!e)
1023  continue;
1024 
1025  if(ctx.shouldSkip(e))
1026  continue;
1027 
1028  if(e->ValueStr() == "arg")
1029  {
1030  const char* name = e->Attribute("name");
1031  const char* value = e->Attribute("value");
1032  const char* defaultValue = e->Attribute("default");
1033 
1034  if(!name)
1035  throw ctx.error("<arg> inside include needs a name attribute");
1036 
1037  if(!value && defaultValue)
1038  {
1039  // roslaunch allows this - god knows why.
1040  ctx.warning(
1041  "You are using <arg> inside an <include> tag with the "
1042  "default=XY attribute - which is superfluous. "
1043  "Use value=XY instead for less confusion. "
1044  "Attribute name: {}",
1045  name
1046  );
1047  value = defaultValue;
1048  }
1049 
1050  if(!name || !value)
1051  throw ctx.error("<arg> inside include needs name and value");
1052 
1053  childCtx.setArg(ctx.evaluate(name), ctx.evaluate(value), true);
1054  }
1055  }
1056 
1057  TiXmlDocument document(fullFile);
1058  if(!document.LoadFile())
1059  throw ctx.error("Could not load launch file '{}': {}", fullFile, document.ErrorDesc());
1060 
1061  childCtx.setFilename(fullFile);
1062 
1063  parse(document.RootElement(), &childCtx);
1064 }
1065 
1066 void LaunchConfig::parseArgument(TiXmlElement* element, ParseContext& ctx)
1067 {
1068  const char* name = element->Attribute("name");
1069  const char* value = element->Attribute("value");
1070  const char* def = element->Attribute("default");
1071 
1072  if(!name)
1073  throw ctx.error("<arg> needs name attribute");
1074 
1075  if(value)
1076  {
1077  std::string fullValue = ctx.evaluate(value);
1078  ctx.setArg(name, fullValue, true);
1079  }
1080  else if(def)
1081  {
1082  std::string fullValue = ctx.evaluate(def);
1083  ctx.setArg(name, fullValue, false);
1084  }
1085  else
1086  {
1087  ctx.setArg(name, UNSET_MARKER, false);
1088  }
1089 }
1090 
1091 void LaunchConfig::parseEnv(TiXmlElement* element, ParseContext& ctx)
1092 {
1093  const char* name = element->Attribute("name");
1094  const char* value = element->Attribute("value");
1095 
1096  if(!name || !value)
1097  throw ctx.error("<env> needs name, value attributes");
1098 
1099  ctx.setEnvironment(ctx.evaluate(name), ctx.evaluate(value));
1100 }
1101 
1102 void LaunchConfig::parseRemap(TiXmlElement* element, ParseContext& ctx)
1103 {
1104  const char* from = element->Attribute("from");
1105  const char* to = element->Attribute("to");
1106 
1107  if(!from || !to)
1108  throw ctx.error("remap needs 'from' and 'to' arguments");
1109 
1110  ctx.setRemap(ctx.evaluate(from), ctx.evaluate(to));
1111 }
1112 
1114 {
1115  return fmt::format("{:08X}", m_anonGen());
1116 }
1117 
1118 template<class Iterator>
1119 void safeAdvance(Iterator& it, const Iterator& end, size_t i)
1120 {
1121  for(size_t j = 0; j < i; ++j)
1122  {
1123  if(it == end)
1124  return;
1125  ++it;
1126  }
1127 }
1128 
1130 {
1131  // This function is optimized for speed, since we usually have a lot of
1132  // parameters and some of those take time (>2s) to compute and upload
1133  // (e.g. xacro).
1134 
1135  // The optimization is two-fold: we use std::future to avoid useless
1136  // computations of parameters which are re-set later on anyways, and
1137  // we use a thread pool to evaluate the futures below.
1138 
1139  const int NUM_THREADS = std::thread::hardware_concurrency();
1140 
1141  std::vector<std::thread> threads(NUM_THREADS);
1142 
1143  std::mutex mutex;
1144 
1145  bool caughtExceptionFlag = false;
1146  ParseException caughtException("");
1147 
1148  for(int i = 0; i < NUM_THREADS; ++i)
1149  {
1150  threads[i] = std::thread([this,i,NUM_THREADS,&mutex,&caughtException,&caughtExceptionFlag]() {
1151  try
1152  {
1153  // Thread number i starts at position i and moves in NUM_THREADS
1154  // increments.
1155  auto it = m_paramJobs.begin();
1156  safeAdvance(it, m_paramJobs.end(), i);
1157 
1158  while(it != m_paramJobs.end())
1159  {
1160  XmlRpc::XmlRpcValue val = it->second.get();
1161  {
1162  std::lock_guard<std::mutex> guard(mutex);
1163  m_params[it->first] = val;
1164  }
1165  safeAdvance(it, m_paramJobs.end(), NUM_THREADS);
1166  }
1167 
1168  // YAML parameters have to be handled separately, since we may need
1169  // to splice them into individual XmlRpcValues.
1170  auto yamlIt = m_yamlParamJobs.begin();
1171  safeAdvance(yamlIt, m_yamlParamJobs.end(), i);
1172 
1173  while(yamlIt != m_yamlParamJobs.end())
1174  {
1175  YAMLResult yaml = yamlIt->get();
1176  {
1177  std::lock_guard<std::mutex> guard(mutex);
1178  loadYAMLParams(m_rootContext, yaml.yaml, yaml.name);
1179  }
1180  safeAdvance(yamlIt, m_yamlParamJobs.end(), NUM_THREADS);
1181  }
1182  }
1183  catch(ParseException& e)
1184  {
1185  std::lock_guard<std::mutex> guard(mutex);
1186  caughtException = e;
1187  caughtExceptionFlag = true;
1188  }
1189  });
1190  }
1191 
1192  // and wait for completion for the threads.
1193  for(auto& t : threads)
1194  t.join();
1195 
1196  if(caughtExceptionFlag)
1197  throw caughtException;
1198 
1199  m_paramJobs.clear();
1200 }
1201 
1203 {
1204  m_outputAttrMode = mode;
1205 }
1206 
1207 void LaunchConfig::setWarningOutput(std::ostream* output)
1208 {
1209  m_warningOutput = output;
1210 }
1211 
1212 }
1213 }
void setEnvironment(const std::string &name, const std::string &value)
virtual const char * what() const noexcept
Definition: launch_config.h:42
std::map< std::string, std::string > m_anonNames
bool shouldSkip(TiXmlElement *e)
std::string simplifyWhitespace(const std::string &input)
Compress any sequence of whitespace to single spaces.
void setFilename(const std::string &filename)
Definition: launch_config.h:70
ROSCPP_DECL void start()
ParseContext enterScope(const std::string &prefix)
std::string anonName(const std::string &base)
const std::map< std::string, std::string > environment() const
void setArg(const std::string &name, const std::string &value, bool override)
void setDefaultCPULimit(double CPULimit)
std::shared_ptr< Node > Ptr
Definition: node.h:23
std::map< std::string, ParameterFuture > m_paramJobs
ROSCPP_DECL bool validate(const std::string &name, std::string &error)
virtual const char * what() const noexceptoverride
Definition: substitution.h:29
ROSCPP_DECL std::string clean(const std::string &name)
void setWarningOutput(std::ostream *warningStream)
void parseROSParam(TiXmlElement *element, ParseContext &ctx)
void parseEnv(TiXmlElement *element, ParseContext &ctx)
std::string convertWhitespace(const std::string &input)
Convert any whitespace to space characters.
const std::string & prefix() const
Definition: launch_config.h:64
std::vector< std::future< YAMLResult > > m_yamlParamJobs
void parseString(const std::string &input, bool onlyArguments=false)
void setDefaultMemoryLimit(uint64_t memoryLimit)
const char * UNSET_MARKER
void parseArgument(TiXmlElement *element, ParseContext &ctx)
std::map< std::string, std::string > m_args
ROSLIB_DECL std::string command(const std::string &cmd)
std::map< std::string, std::string > m_environment
std::string evaluate(const std::string &tpl, bool simplifyWhitespace=true)
ROSCPP_DECL bool ok()
void setCPULimit(double limit)
void parse(const std::string &filename, bool onlyArguments=false)
void setCurrentElement(TiXmlElement *e)
Definition: launch_config.h:73
void parseInclude(TiXmlElement *element, ParseContext ctx)
uint64_t memoryLimit() const
void setOutputAttrMode(OutputAttr mode)
static XmlRpc::XmlRpcValue autoXmlRpcValue(const std::string &fullValue)
void warning(const char *fmt, const Args &...args) const
void parseParam(TiXmlElement *element, ParseContext &ctx, ParamContext paramContext=PARAM_GENERAL)
static WallTime now()
void parseTopLevelAttributes(TiXmlElement *element)
ParseException error(const char *fmt, const Args &...args) const
XmlRpc::XmlRpcValue yamlToXmlRpc(const ParseContext &ctx, const YAML::Node &n)
Definition: yaml_params.cpp:40
void setArgument(const std::string &name, const std::string &value)
std::string parseSubstitutionArgs(const std::string &input, ParseContext &context)
std::vector< Node::Ptr > m_nodes
void setRemap(const std::string &from, const std::string &to)
std::string windowTitle() const
void parseNode(TiXmlElement *element, ParseContext &ctx)
void setMemoryLimit(uint64_t limit)
void setStopTimeout(double timeout)
std::string strip(const std::string &input)
void parseRemap(TiXmlElement *element, ParseContext &ctx)
bool parseBool(const std::string &value, int line)
std::tuple< uint64_t, bool > parseMemory(const std::string &memory)
std::map< std::string, std::string > m_remappings
void setDefaultStopTimeout(double timeout)
const std::map< std::string, std::string > & remappings()
void safeAdvance(Iterator &it, const Iterator &end, size_t i)
XmlRpc::XmlRpcValue paramToXmlRpc(const ParseContext &ctx, const std::string &value, const std::string &type="")
void loadYAMLParams(const ParseContext &ctx, const YAML::Node &n, const std::string &prefix)
void parseScopeAttributes(TiXmlElement *e, ParseContext &attr_ctx)
bool isOnlyWhitespace(const std::string &input)
Check if string is whitespace only (includes &#39; &#39;)


rosmon_core
Author(s): Max Schwarz
autogenerated on Sat Jan 9 2021 03:35:43