ExternalApplicationManager.cpp
Go to the documentation of this file.
1/*
2 * This file is part of ArmarX.
3 *
4 * ArmarX is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 2 as
6 * published by the Free Software Foundation.
7 *
8 * ArmarX is distributed in the hope that it will be useful, but
9 * WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * @package ArmarXCore::ArmarXObjects::ExternalApplicationManager
17 * @author Stefan Reither ( stef dot reither at web dot de )
18 * @date 2016
19 * @copyright http://www.gnu.org/licenses/gpl-2.0.txt
20 * GNU General Public License
21 */
22
24
25#include <signal.h>
26
27#include <filesystem>
28#include <thread>
29
30#include <sys/prctl.h>
31
33#include <boost/regex.hpp>
34
35#include <IceUtil/UUID.h>
36
37#include <SimoxUtility/algorithm/string/string_tools.h>
38
47
48namespace armarx
49{
50
52 std::string prefix) :
54 {
56 "ApplicationPath",
57 "Either the name of the application if its directory is part of the PATH environment "
58 "variable of the system or it is located in an ArmarX-package, or the relative or "
59 "absolute path to the application.")
60 .setCaseInsensitive(false);
61 defineOptionalProperty<std::string>("ApplicationArguments",
62 "",
63 "Comma separated list of command line parameters. "
64 "Commas in double-quoted strings are ignored.");
66 "ArmarXPackage",
67 "",
68 "Set this property if the application is located in an ArmarX-package.");
70 "WorkingDirectory",
71 "",
72 "If set, this path is used as working directory for the external app. This overrides "
73 "BinaryPathAsWorkingDirectory. ${HOME} for env vars, $C{RobotAPI:BINARY_DIR} for "
74 "CMakePackageFinder vars");
75
77 "BinaryPathAsWorkingDirectory",
78 false,
79 "If true the path of the binary is set as the working directory.");
81 "RestartInCaseOfCrash",
82 false,
83 "Whether the application should be restarted in case it crashed.");
84
86 "DisconnectInCaseOfCrash",
87 true,
88 "Whether this component should disconnect as long as the application is not running.");
90 "FakeObjectStartDelay",
91 0,
92 "Delay in ms after which the fake armarx object is started on which other apps can "
93 "depend. Not used if property FakeObjectDelayedStartKeyword is used.");
95 "FakeObjectDelayedStartKeyword",
96 "",
97 "If not empty, the start up of the fake armarx object will be delayed until this "
98 "keyword is found in the stdout of the subprocess.");
100 "KillDelay",
101 2000,
102 "Delay ins ms before the subprocess is killed after sending the stop signal");
104 "PythonUnbuffered",
105 true,
106 "If true, PYTHONUNBUFFERED=1 is added to the environment variables.");
107 defineOptionalProperty<bool>("RedirectToArmarXLog",
108 true,
109 "If true, all outputs from the subprocess are printed with "
110 "ARMARX_LOG, otherwise with std::cout");
112 "AdditionalEnvVars",
113 {},
114 "Comma-seperated list of env-var assignment, e.g. MYVAR=1,ADDPATH=/tmp");
116 "Dependencies",
117 {},
118 "Comma-seperated list of Ice Object dependencies. The external app will only be "
119 "started after all dependencies have been found.");
120 }
121
122 void
124 {
125 const std::string argsStr = getProperty<std::string>("ApplicationArguments").getValue();
126
127 const boost::regex re(",(?=(?:[^\\\"]*\\\"[^\\\"]*\\\")*[^\\\"]*$)");
128
129 boost::match_results<std::string::const_iterator> what;
130 boost::match_flag_type flags = boost::match_default;
131 std::string::const_iterator s = argsStr.begin();
132 std::string::const_iterator e = argsStr.end();
133 size_t begin = 0;
134 while (boost::regex_search(s, e, what, re, flags))
135 {
136 int pos = what.position();
137 auto arg = argsStr.substr(begin, pos);
138 if (!arg.empty())
139 {
140 args.push_back(arg);
141 }
142 std::string::difference_type l = what.length();
143 std::string::difference_type p = what.position();
144 begin += l + p;
145 s += p + l;
146 }
147 std::string lastArg = argsStr.substr(begin);
148 if (!lastArg.empty())
149 {
150 args.push_back(lastArg);
151 }
152 }
153
154 void
156 {
157 getProperty(redirectToArmarXLog, "RedirectToArmarXLog");
158 getProperty(restartWhenCrash, "RestartInCaseOfCrash");
159 getProperty(disconnectWhenCrash, "DisconnectInCaseOfCrash");
160
161 updateLogSenderComponentName();
162
163 workingDir = deriveWorkingDir();
164 application = deriveApplicationPath();
165
166 std::filesystem::path appPath(application);
167 ARMARX_INFO << "Application absolute path: " << appPath;
168 Logging::setTag(appPath.stem().string());
169
170 args.insert(args.begin(), application);
172
173 if (getProperty<bool>("PythonUnbuffered"))
174 {
175 envVars.push_back("PYTHONUNBUFFERED=1");
176 }
177 if (getProperty<Ice::StringSeq>("AdditionalEnvVars").isSet())
178 {
179 auto addEnvVrs = getProperty<Ice::StringSeq>("AdditionalEnvVars").getValue();
180
181 for (const auto& var : addEnvVrs)
182 {
183 ARMARX_INFO << "Adding additional environment variable (raw): " << QUOTED(var);
184
185 const auto envVarResolved = armarx::core::system::EnvExpander::expand(var);
186 envVars.push_back(envVarResolved);
187 }
188 }
189
190 for (auto& var : envVars)
191 {
192 auto split = Split(var, "=");
193 if (split.size() >= 2)
194 {
195 ARMARX_INFO << "Setting env var: " << split.at(0) << "=" << split.at(1);
196 setenv(split.at(0).c_str(), split.at(1).c_str(), 1);
197 }
198 }
199
200 getProperty(startUpKeyword, "FakeObjectDelayedStartKeyword");
201 starterUUID = "ExternalApplicationManagerStarter" + IceUtil::generateUUID();
202 depObjUUID = "ExternalApplicationManagerDependency" + IceUtil::generateUUID();
203
204 usingProxy(depObjUUID);
206 }
207
208 void
210 {
211 ARMARX_DEBUG << "External App Manager started";
212 // startApplication();
213 }
214
215 void
217 {
218 ARMARX_DEBUG << "Disconnecting " << getName();
219 }
220
221 void
226
233
234 void
235 ExternalApplicationManager::updateLogSenderComponentName()
236 {
238 Application::getInstance()->getProperty<std::string>("ApplicationName").isSet())
239 {
241 Application::getInstance()->getProperty<std::string>("ApplicationName").getValue());
242 }
243 else if (getProperty<std::string>("ObjectName").isSet())
244 {
246 }
247 else
248 {
250 }
251 }
252
253 std::string
255 {
256 std::string workingDir;
257 try
258 {
259 workingDir = getProperty<bool>("BinaryPathAsWorkingDirectory")
260 ? std::filesystem::path(application).parent_path().string()
261 : std::filesystem::current_path().string();
262 }
263 catch (std::exception& e)
264 {
265 ARMARX_ERROR << "caught exception in line " << __LINE__ << " what:\n" << e.what();
266 throw;
267 }
268
269 if (getProperty<std::string>("WorkingDirectory").isSet())
270 {
271 workingDir = getProperty<std::string>("WorkingDirectory").getValueAndReplaceAllVars();
272 try
273 {
274 std::filesystem::current_path(workingDir);
275 }
276 catch (std::exception& e)
277 {
278 ARMARX_ERROR << "caught exception in line " << __LINE__
279 << " when setting the current working directory to '" << workingDir
280 << "'. except.what:\n"
281 << e.what();
282 throw;
283 }
284 ARMARX_INFO << "Using '" << workingDir << "' as working directory";
285 }
286
287 return workingDir;
288 }
289
290 std::string
292 {
293 std::filesystem::path applicationPath =
294 getProperty<std::string>("ApplicationPath").getValueAndReplaceAllVars();
295
296 if (applicationPath.is_absolute())
297 {
298 if (!std::filesystem::exists(applicationPath))
299 {
300 throw LocalException() << applicationPath << " does not exist.";
301 }
302 return applicationPath;
303 }
304
305 if (getProperty<std::string>("ArmarXPackage").isSet())
306 {
307 CMakePackageFinder finder(getProperty<std::string>("ArmarXPackage").getValue());
308 if (finder.packageFound())
309 {
310 const std::filesystem::path path =
311 (finder.getBinaryDir() / applicationPath).lexically_normal();
312 if (!std::filesystem::exists(path))
313 {
314 throw LocalException()
315 << path << " does not exist in ArmarX-package " << finder.getName();
316 }
317 return path;
318 }
319 else
320 {
321 throw LocalException() << finder.getName() << " is not an ArmarX-package";
322 }
323 }
324 else
325 {
326 std::string applicationPathStr = applicationPath.string();
327 ArmarXDataPath::ResolveHomePath(applicationPathStr);
329 ArmarXDataPath::ReplaceEnvVars(applicationPathStr);
330 applicationPath = applicationPathStr;
331
332 if (!std::filesystem::exists(applicationPath))
333 {
334 const std::string path = boost::process::search_path(applicationPath);
335 if (path.empty())
336 {
337 throw LocalException()
338 << applicationPath << " does not represent an executable.";
339 }
340 else
341 {
342 return path;
343 }
344 }
345 else
346 {
347 return applicationPath;
348 }
349 }
350 }
351
352 void
354 {
355 stopApplication();
356 startApplication();
357 }
358
359 void
361 {
362 stopApplication();
363 }
364
365 std::string
367 {
368 return application;
369 }
370
371 bool
373 {
374 return isAppRunning;
375 }
376
377 void
378 ExternalApplicationManager::waitForApplication()
379 {
380 if (isAppRunning)
381 {
382 try
383 {
384 wait_for_exit(*childProcess);
385 cleanUp();
386
387 if (restartWhenCrash && !appStoppedOnPurpose)
388 {
389 ARMARX_INFO << "Restarting application ...";
390 startApplication();
391 }
392 else
393 {
394 isAppRunning = false;
395 if (this->disconnectWhenCrash)
396 {
397 this->getArmarXManager()->removeObjectBlocking(starterUUID);
398 }
399 }
400 }
401 catch (...)
402 {
403 }
404 }
405 }
406
407 void
409 {
411 depObj->setParent(this);
412
413 this->getArmarXManager()->addObject(depObj, false, depObjUUID);
414 ARMARX_INFO << "Adding dummy app with name " << depObjUUID;
415 }
416
417 void
419 {
420 starter = new ExternalApplicationManagerStarter();
421 starter->setParent(this);
422 starter->setDependencies(getProperty<Ice::StringSeq>("Dependencies").getValue());
423
424 this->getArmarXManager()->addObject(starter, false, starterUUID);
425 }
426
427 void
428 ExternalApplicationManager::setupStream(StreamMetaData& meta)
429 {
430
431
432 meta.read = [&, this](const boost::system::error_code& error, std::size_t length)
433 {
434 if (!error)
435 {
436 std::ostringstream ss;
437 ss << &meta.input_buffer;
438 std::string s = ss.str();
439
440 std::filesystem::path appPath(application);
441 if (redirectToArmarXLog)
442 {
443 *((ARMARX_LOG_S).setBacktrace(false)->setTag(LogTag(appPath.stem().string())))
444 << meta.level << s;
445 }
446 else
447 {
448 std::cout << s << std::flush;
449 }
450
451 if (!startUpKeyword.empty() && !depObj)
452 {
453 if (Contains(s, startUpKeyword))
454 {
456 }
457 }
458 boost::asio::async_read_until(meta.pend, meta.input_buffer, "\n", meta.read);
459 }
460 else if (error == boost::asio::error::not_found)
461 {
462 std::cout << "Did not receive ending character!" << std::endl;
463 }
464 };
465 boost::asio::async_read_until(meta.pend, meta.input_buffer, "\n", meta.read);
466 auto run = [&]() { meta.io_service.run(); };
467
468 std::thread{run}.detach();
469 }
470
471 void
472 ExternalApplicationManager::startApplication()
473 {
474 appStoppedOnPurpose = false;
475
476 Ice::StringSeq managedObjects = this->getArmarXManager()->getManagedObjectNames();
477 bool dummyAlreadyAdded = false;
478 for (std::string s : managedObjects)
479 {
480 if (s == depObjUUID)
481 {
482 dummyAlreadyAdded = true;
483 }
484 }
485
486 outMetaData.reset(new StreamMetaData());
487 errMetaData.reset(new StreamMetaData());
488
489 {
490 outMetaData->sink = boost::iostreams::file_descriptor_sink(
491 outMetaData->pipe.sink, boost::iostreams::close_handle);
492 errMetaData->sink = boost::iostreams::file_descriptor_sink(
493 errMetaData->pipe.sink, boost::iostreams::close_handle);
494
495
496 if (hasProperty("ApplicationArguments") &&
497 !getProperty<std::string>("ApplicationArguments").getValue().empty())
498 {
499 ARMARX_INFO << "Application arguments: "
500 << getProperty<std::string>("ApplicationArguments").getValue();
501 }
502 ARMARX_INFO << "Commandline: " << simox::alg::join(args, " ");
503 childProcess.reset(new boost::process::child(boost::process::execute(
504 boost::process::initializers::set_args(args),
505 boost::process::initializers::start_in_dir(workingDir),
506 boost::process::initializers::bind_stdout(outMetaData->sink),
507 boost::process::initializers::bind_stderr(errMetaData->sink),
508 boost::process::initializers::inherit_env(),
509 // Guarantees that the child process gets killed, even when this process recieves SIGKILL(9) instead of SIGINT(2)
510 boost::process::initializers::on_exec_setup(
511 [](boost::process::executor&) { ::prctl(PR_SET_PDEATHSIG, SIGKILL); }))));
512 }
513
514 outMetaData->level = MessageTypeT::INFO;
515 errMetaData->level = MessageTypeT::WARN;
516 setupStream(*outMetaData);
517 setupStream(*errMetaData);
518
519
520 isAppRunning = true;
521
522 waitTask = new RunningTask<ExternalApplicationManager>(
523 this, &ExternalApplicationManager::waitForApplication);
524 waitTask->start();
525
526 usleep(1000 * getProperty<int>("FakeObjectStartDelay").getValue());
527 if (!dummyAlreadyAdded && startUpKeyword.empty())
528 {
530 }
531
532 ARMARX_INFO << "Application started";
533 }
534
535 void
536 ExternalApplicationManager::stopApplication()
537 {
538
539 if (isAppRunning)
540 {
541 int processId = (*childProcess).pid;
542 ARMARX_INFO << "Stopping application with PID " << processId << " with SIGINT";
543 appStoppedOnPurpose = true;
544
545 if (this->disconnectWhenCrash && this->getArmarXManager())
546 {
547 this->getArmarXManager()->removeObjectBlocking(depObjUUID);
548 depObj = nullptr;
549 }
550
551 ::killpg(processId, SIGINT);
552
553 ARMARX_VERBOSE << "waiting for application " << processId;
554 if (!waitForProcessToFinish(processId, getProperty<int>("KillDelay").getValue()))
555 {
556 ARMARX_VERBOSE << "Application wont stop - killing it now";
557 boost::process::terminate(*childProcess); // Sends SIGTERM to the process
558 }
559
560 cleanUp();
561 isAppRunning = false;
562 }
563 }
564
565 void
566 ExternalApplicationManager::cleanUp()
567 {
568 if (waitTask)
569 {
570 waitTask->stop();
571 }
572 errMetaData->io_service.stop();
573 outMetaData->io_service.stop();
574 ARMARX_VERBOSE << "Application cleaned up";
575 }
576
577 bool
578 ExternalApplicationManager::waitForProcessToFinish(int pid, int timeoutMS)
579 {
580 if (pid <= 0)
581 {
582 return false;
583 }
584
585 double startTime = TimeUtil::GetTime().toMilliSecondsDouble();
586
587 while ((startTime + timeoutMS > TimeUtil::GetTime().toMilliSecondsDouble()) &&
588 ::kill(pid, 0) != -1)
589 {
591 }
592
593 if (::kill(pid, 0) == -1)
594 {
595 return true;
596 }
597 else
598 {
599 return false;
600 }
601 }
602
603 ExternalApplicationManager::StreamMetaData::StreamMetaData() :
604 pipe(boost::process::create_pipe()), pend(pipe_end(io_service, pipe.source))
605 {
606 }
607
608 ExternalApplicationManager::StreamMetaData::StreamMetaData(
609 const ExternalApplicationManager::StreamMetaData& data) :
610 pipe(boost::process::create_pipe()), pend(pipe_end(io_service, pipe.source))
611 {
612 }
613
614} // namespace armarx
#define QUOTED(x)
static ApplicationPtr getInstance()
Retrieve shared pointer to the application object.
static void ResolveHomePath(std::string &path)
Resolves a path like ~/myfile.txt or $HOME/myfile.txt to /home/user/myfile.txt.
static bool ReplaceEnvVars(std::string &string)
ReplaceEnvVars replaces environment variables in a string with their values, if the env.
The CMakePackageFinder class provides an interface to the CMake Package finder capabilities.
std::string getBinaryDir() const
std::string getName() const
Returns the name of the given package.
bool packageFound() const
Returns whether or not this package was found with cmake.
static bool ReplaceCMakePackageFinderVars(std::string &string)
Replaces occurrences like $C{PACKAGE_NAME:VAR_NAME} with their CMakePackageFinder value.
ComponentPropertyDefinitions(std::string prefix, bool hasObjectNameParameter=true)
Definition Component.cpp:46
std::string getConfigIdentifier()
Retrieve config identifier for this component as set in constructor.
Definition Component.cpp:90
Property< PropertyType > getProperty(const std::string &name)
void terminateApplication(const Ice::Current &) override
virtual void addApplicationArguments(Ice::StringSeq &args)
std::string getPathToApplication(const Ice::Current &) override
armarx::PropertyDefinitionsPtr createPropertyDefinitions() override
bool isApplicationRunning(const Ice::Current &) override
void restartApplication(const Ice::Current &) override
static void SetComponentName(const std::string &componentName)
void setTag(const LogTag &tag)
Definition Logging.cpp:54
bool usingProxy(const std::string &name, const std::string &endpoints="")
Registers a proxy for retrieval after initialization and adds it to the dependency list.
std::string getName() const
Retrieve name of object.
ArmarXManagerPtr getArmarXManager() const
Returns the ArmarX manager used to add and remove components.
std::string prefix
Prefix of the properties such as namespace, domain, component name, etc.
PropertyDefinition< PropertyType > & defineOptionalProperty(const std::string &name, PropertyType defaultValue, const std::string &description="", PropertyDefinitionBase::PropertyConstness constness=PropertyDefinitionBase::eConstant)
PropertyDefinition< PropertyType > & defineRequiredProperty(const std::string &name, const std::string &description="", PropertyDefinitionBase::PropertyConstness constness=PropertyDefinitionBase::eConstant)
bool hasProperty(const std::string &name)
Property< PropertyType > getProperty(const std::string &name)
Property creation and retrieval.
static IceUtil::Time GetTime(TimeMode timeMode=TimeMode::VirtualTime)
Get the current time.
Definition TimeUtil.cpp:42
static void MSSleep(int durationMS)
lock the calling thread for a given duration (like usleep(...) but using Timeserver time)
Definition TimeUtil.cpp:100
static std::string expand(const std::string &input)
Return the expanded form of the comma-separated assignment list without modifying the process environ...
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:181
#define ARMARX_ERROR
The logging level for unexpected behaviour, that must be fixed.
Definition Logging.h:196
#define ARMARX_DEBUG
The logging level for output that is only interesting while debugging.
Definition Logging.h:184
#define ARMARX_LOG_S
This macro creates a new temporary instance which can then be used to log data using the << operator.
Definition Logging.h:145
#define ARMARX_VERBOSE
The logging level for verbose information.
Definition Logging.h:187
Defines initializers.
T getValue(nlohmann::json &userConfig, nlohmann::json &defaultConfig, const std::string &entryName)
Definition utils.h:80
This file offers overloads of toIce() and fromIce() functions for STL container types.
std::vector< std::string > split(const std::string &source, const std::string &splitBy, bool trimElements=false, bool removeEmptyElements=false)
std::vector< std::string > Split(const std::string &source, const std::string &splitBy, bool trimElements=false, bool removeEmptyElements=false)
IceUtil::Handle< class PropertyDefinitionContainer > PropertyDefinitionsPtr
PropertyDefinitions smart pointer type.
bool Contains(const ContainerType &container, const ElementType &searchElement)
Definition algorithm.h:330
Vertex source(const detail::edge_base< Directed, Vertex > &e, const PCG &)
bool empty(const std::string &s)
Definition cxxopts.hpp:234