PythonApplicationManager.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::PythonApplicationManager
17 * @author Rainer Kartmann ( rainer dot kartmann at kit dot edu )
18 * @date 2021
19 * @copyright http://www.gnu.org/licenses/gpl-2.0.txt
20 * GNU General Public License
21 */
22
24
25#include <algorithm>
26
27#include <SimoxUtility/algorithm/string.h>
28
32
33namespace armarx
34{
35
36 const simox::meta::EnumNames<PythonApplicationManager::VenvType>
41 };
42
43 void
45 const std::string& prefix)
46 {
47 defs->required(
48 armarxPackageName, prefix + "10_ArmarXPackageName", "Name of the ArmarX package.");
49 defs->optional(armarxPythonPackagesDir,
50 prefix + "11_ArmarXPythonPackagesDir",
51 "Directory where python packages are located (relative to the root of the "
52 "ArmarX package).");
53
54 defs->required(pythonPackageName,
55 prefix + "20_PythonPackageName",
56 "Name of the Python package in the ArmarXPackage/python/ directory.");
57 defs->required(pythonScriptPath,
58 prefix + "21_PythonScriptPath",
59 "Path to the python script (inside the python package).");
60 defs->optional(pythonScriptArgumentsString,
61 prefix + "22_PythonScriptArgs",
62 "Whitespace separated list of arguments.");
63 defs->optional(pythonPathEntriesString,
64 prefix + "23_PythonPathEntries",
65 "Colon-separated list of paths to add to the PYTHONPATH.");
66 defs->optional(pythonPoetry, prefix + "24_PythonPoetry", "Use python poetry.");
67
68 defs->optional(venvName, prefix + "30_venv.Name", "Name of the virtual environment.");
69 {
70 std::stringstream ss;
71 ss << "Type of the virtual environment."
72 << "\n\t- " << VenvTypeNames.to_name(VenvType::Auto)
73 << " \tDerive automatically."
74 << "\n\t- " << VenvTypeNames.to_name(VenvType::Dedicated)
75 << "\tSearch inside the python package root directory."
76 << "\n\t- " << VenvTypeNames.to_name(VenvType::Shared)
77 << " \tSearch in the shared_envs directory.";
78 defs->optional(venvType, prefix + "31_venv.Type", ss.str()).map(VenvTypeNames);
79 }
80
81
82 defs->optional(
84 prefix + "40_WorkingDirectory",
85 "If set, this path is used as working directory for the python script (overriding "
86 "BinaryPathAsWorkingDirectory)."
87 "\n${HOME} for env vars, $C{RobotAPI:BINARY_DIR} for CMakePackageFinder vars");
88 }
89
90 void
92 {
94
95 if (props.getProperty<std::string>(prefix + "40_WorkingDirectory").isSet())
96 {
98 props.getProperty<std::string>("WorkingDirectory").getValueAndReplaceAllVars();
99 };
100
102 simox::alg::split(pythonScriptArgumentsString, " ", true, true);
103 pythonPathEntriesVector = simox::alg::split(pythonPathEntriesString, ":", true, true);
104 }
105
108 {
111
112 properties.defineProperties(defs, "py.");
113
114 defs->defineOptionalProperty<bool>(
115 "BinaryPathAsWorkingDirectory",
116 false,
117 "If true the path of the binary is set as the working directory.");
118 defs->defineOptionalProperty<bool>(
119 "RestartInCaseOfCrash",
120 false,
121 "Whether the application should be restarted in case it crashed.");
122
123 defs->defineOptionalProperty<bool>(
124 "DisconnectInCaseOfCrash",
125 true,
126 "Whether this component should disconnect as long as the application is not running.");
127 defs->defineOptionalProperty<int>(
128 "FakeObjectStartDelay",
129 0,
130 "Delay in ms after which the fake armarx object is started on which other apps can "
131 "depend. Not used if property FakeObjectDelayedStartKeyword is used.");
132 defs->defineOptionalProperty<std::string>(
133 "FakeObjectDelayedStartKeyword",
134 "",
135 "If not empty, the start up of the fake armarx object will be delayed until this "
136 "keyword is found in the stdout of the subprocess.");
137 defs->defineOptionalProperty<int>(
138 "KillDelay",
139 2000,
140 "Delay ins ms before the subprocess is killed after sending the stop signal");
141 defs->defineOptionalProperty<bool>(
142 "PythonUnbuffered",
143 true,
144 "If true, PYTHONUNBUFFERED=1 is added to the environment variables.");
145 defs->defineOptionalProperty<bool>("RedirectToArmarXLog",
146 true,
147 "If true, all outputs from the subprocess are printed "
148 "with ARMARX_LOG, otherwise with std::cout");
149 defs->defineOptionalProperty<Ice::StringSeq>(
150 "AdditionalEnvVars",
151 {},
152 "Comma-seperated list of env-var assignment, e.g. MYVAR=1,ADDPATH=/tmp");
153 defs->defineOptionalProperty<Ice::StringSeq>(
154 "Dependencies",
155 {},
156 "Comma-seperated list of Ice Object dependencies. The external app will only be "
157 "started after all dependencies have been found.");
158
159 return defs;
160 }
161
162 std::filesystem::path
164 {
165 CMakePackageFinder finder("ArmarXCore");
166 ARMARX_CHECK(finder.packageFound());
167
168 std::filesystem::path armarxCoreRootPath = finder.getPackageDir();
169 std::filesystem::path armarxCliPath = armarxCoreRootPath / "etc" / "python";
170 return armarxCliPath;
171 }
172
173 void
175 {
177
178 properties.read(*this, "py.");
179 paths.derive(properties);
180 ARMARX_INFO << paths;
181
182 {
183 std::stringstream originalPythonPath;
184 if (char* pp = getenv("PYTHONPATH"))
185 {
186 originalPythonPath << pp;
187 }
188 std::vector<std::string> pythonPath = simox::alg::split(originalPythonPath.str(), ":");
189
190 std::stringstream message;
191 {
192 /* We have to remove the entry of the ArmarX command line interface
193 * (".../armarx/ArmarXCore/etc/python").
194 *
195 * Reason: The ArmarX command line tool (armarx start, armarx gui, ...)
196 * is now running from a proper virtual environment. So inside the "armarx gui"
197 * process, the path of that venv is in the PYTHONPATH.
198 * This can cause "import armarx" to try to import the command line tool
199 * instead of the ArmarX python bindings (which are probably installed
200 * in the called virtual environment).
201 */
202 const std::filesystem::path armarxCliPath = getArmarXCliPath();
203 if (auto it =
204 std::find(pythonPath.begin(), pythonPath.end(), armarxCliPath.string());
205 it != pythonPath.end())
206 {
207 message << "(Removed '" << *it << "' from PYTHONPATH.)";
208 pythonPath.erase(it);
209 }
210 }
211 pythonPath.push_back(this->paths.pythonPackagePath.string());
212 pythonPath.push_back(properties.pythonPathEntriesString);
213
214 const std::string newPythonpath = simox::alg::join(pythonPath, ":");
215
216 ARMARX_INFO << "Setting PYTHONPATH:\n\t" << newPythonpath
217 << "\nOriginal PYTHONPATH:\n\t" << originalPythonPath.str() << "\n"
218 << message.str();
219 ;
220 setenv("PYTHONPATH", newPythonpath.c_str(), 1);
221 }
222
223 inputOk = true;
224
226 }
227
228 void
230 {
232
233 if (inputOk)
234 {
236 }
237 }
238
239 void
241 {
243
244 if (inputOk)
245 {
247 }
248 }
249
250 void
252 {
254
255 if (inputOk)
256 {
258 }
259 }
260
261 std::string
263 {
264 return paths.workingDir;
265 }
266
267 std::string
269 {
270 return paths.pythonBinPath;
271 }
272
273 void
275 {
276 // Script path
277 args.push_back(paths.pythonScriptPath);
278 // Script args
279 args.insert(args.end(),
280 properties.pythonScriptArgumentsVector.begin(),
281 properties.pythonScriptArgumentsVector.end());
282 }
283
284 std::ostream&
285 operator<<(std::ostream& os, const PythonApplicationManager::Paths& paths)
286 {
287 os << "ArmarX package path: \t" << paths.armarxPackagePath;
288 os << "\nPython package path: \t" << paths.pythonPackagePath;
289 os << "\nPython script path: \t" << paths.pythonScriptPath;
290 os << "\nVenv path: \t" << paths.venvPath;
291 os << "\nPython binary path: \t" << paths.pythonBinPath;
292 os << "\nWorking directory: \t" << paths.workingDir;
293 return os;
294 }
295
296 void
298 {
300
301 namespace fs = std::filesystem;
302
306
307 if (properties.pythonPoetry)
308 {
309 std::string cmd = std::string("cd ") +
310 reinterpret_cast<const char*>(pythonPackagePath.u8string().c_str()) +
311 ";poetry env info -p --ansi";
312
313 std::array<char, 128> buffer;
314
315 // https://stackoverflow.com/questions/76867698/what-does-ignoring-attributes-on-template-argument-mean-in-this-context
316 // std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd.c_str(), "r"), pclose);
317 std::unique_ptr<FILE, int(*)(FILE*)> pipe(popen(cmd.c_str(), "r"), pclose);
318 if (!pipe)
319 {
320 throw std::runtime_error("popen() failed!");
321 }
322 while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr)
323 {
324 std::string s = buffer.data();
325 s.erase(std::remove(s.begin(), s.end(), '\n'), s.end());
326 venvPath += s;
327 }
328 }
329 else
330 {
331 venvPath = findVenvPath(properties);
332 }
334
335 if (properties.workingDir.empty())
336 {
337 workingDir = pythonScriptPath.parent_path();
338 }
339 else
340 {
341 workingDir = properties.workingDir;
342 }
343 }
344
347 const PythonApplicationManager::Properties& properties) const
348 {
349 armarx::CMakePackageFinder finder(properties.armarxPackageName);
350 if (!finder.packageFound())
351 {
352 std::stringstream ss;
353 ss << "Could not find ArmarX package '" << properties.armarxPackageName << "'.";
354 throw armarx::LocalException(ss.str());
355 }
356 return finder.getPackageDir();
357 }
358
361 const PythonApplicationManager::Properties& properties) const
362 {
363 return armarxPackagePath / properties.armarxPythonPackagesDir /
364 properties.pythonPackageName;
365 }
366
369 const PythonApplicationManager::Properties& properties) const
370 {
371 std::vector<path> candidates{pythonPackagePath / properties.pythonPackageName /
372 properties.pythonScriptPath,
373 pythonPackagePath / properties.pythonScriptPath};
374
375 if (std::optional<path> p = checkCandidateFiles(candidates))
376 {
377 return p.value();
378 }
379 else
380 {
381 std::stringstream ss;
382 ss << "Could not find python script '" << properties.pythonScriptPath << "' "
383 << "in python package '" << properties.pythonPackageName << "' "
384 << "in ArmarX package '" << properties.armarxPackageName << "'. \n"
385 << "Checked candidates: ";
386 for (const path& candidate : candidates)
387 {
388 ss << "\n" << candidate;
389 }
390 throw armarx::LocalException(ss.str());
391 }
392 }
393
396 const PythonApplicationManager::Properties& properties) const
397 {
399
401 switch (properties.venvType)
402 {
404 venvPath = getDedicatedVenvPath(properties);
405 break;
406
407 case VenvType::Shared:
408 venvPath = getSharedVenvPath(properties);
409 break;
410
411 case VenvType::Auto:
412 {
413 std::vector<path> candidates{
414 getDedicatedVenvPath(properties),
415 getSharedVenvPath(properties),
416 };
417 if (std::optional<path> p = checkCandidateDirectories(candidates))
418 {
419 venvPath = p.value();
420 }
421 else
422 {
423 std::stringstream ss;
424 ss << "Could not find dedicated or shared venv '" << properties.venvName << "' "
425 << "in python package '" << properties.pythonPackageName << "' "
426 << "in ArmarX package '" << properties.armarxPackageName << "'. \n"
427 << "Checked candidates: ";
428 for (const path& candidate : candidates)
429 {
430 ss << "\n" << candidate;
431 }
432 throw armarx::LocalException(ss.str());
433 }
434 }
435 break;
436 }
437
438 if (std::filesystem::is_directory(venvPath))
439 {
440 return venvPath;
441 }
442 else
443 {
444 std::stringstream ss;
445 ss << "Could not find "
446 << simox::alg::to_lower(VenvTypeNames.to_name(properties.venvType)) << " venv '"
447 << properties.venvName << "' ";
448 switch (properties.venvType)
449 {
450 case VenvType::Auto:
451 break;
453 ss << "in python package '" << properties.pythonPackageName << "' ";
454 break;
455 case VenvType::Shared:
456 break;
457 }
458 ss << "in ArmarX package '" << properties.armarxPackageName << "'. \n";
459 ss << "Expected path: \n" << venvPath;
460 throw armarx::LocalException(ss.str());
461 }
462 }
463
464 auto
466 const PythonApplicationManager::Properties& properties) const -> path
467 {
469
470 const path pythonBinPath = venvPath / "bin" / "python";
471
472 if (std::filesystem::is_regular_file(pythonBinPath))
473 {
474 return pythonBinPath;
475 }
476 else
477 {
478 std::stringstream ss;
479 ss << "Could not find python binary "
480 << "in " << simox::alg::to_lower(VenvTypeNames.to_name(properties.venvType))
481 << " venv '" << properties.venvName << "' ";
482 switch (properties.venvType)
483 {
484 case VenvType::Auto:
485 break;
487 ss << "in python package '" << properties.pythonPackageName << "' ";
488 break;
489 case VenvType::Shared:
490 break;
491 }
492 ss << "in ArmarX package '" << properties.armarxPackageName << "'. \n";
493 ss << "Expected path: \n" << pythonBinPath;
494 throw armarx::LocalException(ss.str());
495 }
496 }
497
498 auto
500 -> path
501 {
502 return pythonPackagePath / properties.venvName;
503 }
504
505 auto
507 {
508 return armarxPackagePath / properties.armarxPythonPackagesDir / sharedVenvsDir /
509 properties.venvName;
510 }
511
512 std::optional<PythonApplicationManager::Paths::path>
514 const std::vector<PythonApplicationManager::Paths::path>& candidates)
515 {
516 return checkCandidatePaths(
517 candidates, [](const path& p) { return std::filesystem::is_regular_file(p); });
518 }
519
520 std::optional<PythonApplicationManager::Paths::path>
522 const std::vector<PythonApplicationManager::Paths::path>& candidates)
523 {
524 return checkCandidatePaths(candidates,
525 [](const path& p) { return std::filesystem::is_directory(p); });
526 }
527
528 std::optional<PythonApplicationManager::Paths::path>
530 const std::vector<PythonApplicationManager::Paths::path>& candidates,
531 std::function<bool(PythonApplicationManager::Paths::path)> existsFn)
532 {
533 auto it = std::find_if(candidates.begin(), candidates.end(), existsFn);
534 if (it != candidates.end())
535 {
536 return *it;
537 }
538 else
539 {
540 return std::nullopt;
541 }
542 }
543
544} // namespace armarx
The CMakePackageFinder class provides an interface to the CMake Package finder capabilities.
bool packageFound() const
Returns whether or not this package was found with cmake.
std::string getPackageDir() const
Returns the top level path of a source package.
Default component property definition container.
Definition Component.h:70
std::string getConfigIdentifier()
Retrieve config identifier for this component as set in constructor.
Definition Component.cpp:90
Abstract PropertyUser class.
Property< PropertyType > getProperty(const std::string &name)
Property creation and retrieval.
static const simox::meta::EnumNames< VenvType > VenvTypeNames
std::string deriveApplicationPath() const override
std::string deriveWorkingDir() const override
armarx::PropertyDefinitionsPtr createPropertyDefinitions() override
void addApplicationArguments(Ice::StringSeq &args) override
#define ARMARX_CHECK(expression)
Shortcut for ARMARX_CHECK_EXPRESSION.
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:181
This file offers overloads of toIce() and fromIce() functions for STL container types.
std::ostream & operator<<(std::ostream &os, const PythonApplicationManager::Paths &paths)
std::filesystem::path getArmarXCliPath()
IceUtil::Handle< class PropertyDefinitionContainer > PropertyDefinitionsPtr
PropertyDefinitions smart pointer type.
static std::optional< path > checkCandidatePaths(const std::vector< path > &candidates, std::function< bool(path)> existsFn)
path findArmarXPackagePath(const Properties &properties) const
path getSharedVenvPath(const Properties &properties) const
static std::optional< path > checkCandidateDirectories(const std::vector< path > &candidates)
path findVenvPath(const Properties &properties) const
path findPythonPackagePath(const Properties &properties) const
path findPythonBinaryPath(const Properties &properties) const
static std::optional< path > checkCandidateFiles(const std::vector< path > &candidates)
path getDedicatedVenvPath(const Properties &properties) const
path findPythonScriptPath(const Properties &properties) const
void read(PropertyUser &props, const std::string &prefix="")
void defineProperties(armarx::PropertyDefinitionsPtr defs, const std::string &prefix="")
std::string pythonScriptArgumentsString
Whitespace separated list of arguments.
#define ARMARX_TRACE
Definition trace.h:77