2019-06-05 18:59:45 +02:00
|
|
|
/* Node-type for subprocess node-types.
|
|
|
|
*
|
2022-03-15 09:18:01 -04:00
|
|
|
* Author: Steffen Vogel <post@steffenvogel.de>
|
2022-03-15 09:28:57 -04:00
|
|
|
* SPDX-FileCopyrightText: 2014-2023 Institute for Automation of Complex Power Systems, RWTH Aachen University
|
2022-07-04 18:20:03 +02:00
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
2019-06-05 18:59:45 +02:00
|
|
|
*/
|
|
|
|
|
|
|
|
#include <string>
|
2021-08-10 10:12:48 -04:00
|
|
|
#include <unistd.h>
|
2019-06-05 18:59:45 +02:00
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
#include <villas/format.hpp>
|
2021-08-10 10:12:48 -04:00
|
|
|
#include <villas/node/config.hpp>
|
2023-09-07 11:46:39 +02:00
|
|
|
#include <villas/node/exceptions.hpp>
|
2019-06-05 18:59:45 +02:00
|
|
|
#include <villas/nodes/exec.hpp>
|
2021-06-21 16:11:42 -04:00
|
|
|
#include <villas/utils.hpp>
|
2019-06-05 18:59:45 +02:00
|
|
|
|
2019-06-23 10:51:26 +02:00
|
|
|
using namespace villas;
|
2021-05-10 00:12:30 +02:00
|
|
|
using namespace villas::node;
|
2019-06-05 18:59:45 +02:00
|
|
|
using namespace villas::utils;
|
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
ExecNode::~ExecNode() {
|
|
|
|
if (stream_in)
|
|
|
|
fclose(stream_in);
|
2021-08-10 10:12:48 -04:00
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
if (stream_out)
|
|
|
|
fclose(stream_out);
|
2021-08-10 10:12:48 -04:00
|
|
|
}
|
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
int ExecNode::parse(json_t *json) {
|
|
|
|
int ret = Node::parse(json);
|
|
|
|
if (ret)
|
|
|
|
return ret;
|
|
|
|
|
|
|
|
json_error_t err;
|
|
|
|
int f = 1, s = -1;
|
|
|
|
|
|
|
|
json_t *json_exec;
|
|
|
|
json_t *json_env = nullptr;
|
|
|
|
json_t *json_format = nullptr;
|
|
|
|
|
|
|
|
const char *wd = nullptr;
|
|
|
|
|
|
|
|
ret = json_unpack_ex(
|
|
|
|
json, &err, 0, "{ s: o, s?: o, s?: b, s?: o, s?: b, s?: s }", "exec",
|
|
|
|
&json_exec, "format", &json_format, "flush", &f, "environment", &json_env,
|
|
|
|
"shell", &s, "working_directory", &wd);
|
|
|
|
if (ret)
|
|
|
|
throw ConfigError(json, err, "node-config-node-exec");
|
|
|
|
|
|
|
|
flush = f != 0;
|
|
|
|
shell = s < 0 ? json_is_string(json_exec) : s != 0;
|
|
|
|
|
|
|
|
arguments.clear();
|
|
|
|
environment.clear();
|
|
|
|
|
|
|
|
if (json_is_string(json_exec)) {
|
|
|
|
if (!shell)
|
|
|
|
throw ConfigError(
|
|
|
|
json_exec, "node-config-node-exec-shell",
|
|
|
|
"The exec setting must be an array if shell mode is disabled.");
|
|
|
|
|
|
|
|
command = json_string_value(json_exec);
|
|
|
|
} else if (json_is_array(json_exec)) {
|
|
|
|
if (shell)
|
|
|
|
throw ConfigError(
|
|
|
|
json_exec, "node-config-node-exec-shell",
|
|
|
|
"The exec setting must be a string if shell mode is enabled.");
|
|
|
|
|
|
|
|
if (json_array_size(json_exec) < 1)
|
|
|
|
throw ConfigError(json_exec, "node-config-node-exec-exec",
|
|
|
|
"At least one argument must be given");
|
|
|
|
|
|
|
|
size_t i;
|
|
|
|
json_t *json_arg;
|
|
|
|
json_array_foreach(json_exec, i, json_arg) {
|
|
|
|
if (!json_is_string(json_arg))
|
|
|
|
throw ConfigError(json_arg, "node-config-node-exec-exec",
|
|
|
|
"All arguments must be of string type");
|
|
|
|
|
|
|
|
if (i == 0)
|
|
|
|
command = json_string_value(json_arg);
|
|
|
|
|
|
|
|
arguments.push_back(json_string_value(json_arg));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (json_env) {
|
|
|
|
// obj is a JSON object
|
|
|
|
const char *key;
|
|
|
|
json_t *json_value;
|
|
|
|
|
|
|
|
json_object_foreach(json_env, key, json_value) {
|
|
|
|
if (!json_is_string(json_value))
|
|
|
|
throw ConfigError(json_value, "node-config-node-exec-environment",
|
|
|
|
"Environment variables must be of string type");
|
|
|
|
|
|
|
|
environment[key] = json_string_value(json_value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Format
|
|
|
|
auto *fmt = json_format ? FormatFactory::make(json_format)
|
|
|
|
: FormatFactory::make("villas.human");
|
|
|
|
|
|
|
|
formatter = Format::Ptr(fmt);
|
|
|
|
if (!formatter)
|
|
|
|
throw ConfigError(json_format, "node-config-node-exec-format",
|
|
|
|
"Invalid format configuration");
|
|
|
|
|
|
|
|
state = State::PARSED;
|
|
|
|
|
|
|
|
return 0;
|
2019-06-05 18:59:45 +02:00
|
|
|
}
|
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
int ExecNode::prepare() {
|
|
|
|
assert(state == State::CHECKED);
|
2019-06-05 18:59:45 +02:00
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
// Initialize IO
|
|
|
|
formatter->start(getInputSignals(false));
|
2019-06-11 16:41:33 +00:00
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
return Node::prepare();
|
2019-06-05 18:59:45 +02:00
|
|
|
}
|
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
int ExecNode::start() {
|
2024-02-27 17:25:05 +01:00
|
|
|
// Pass configuration file and node-name via environemnt
|
|
|
|
environment["VILLAS_NODE_CONFIG"] = configPath;
|
|
|
|
environment["VILLAS_NODE_NAME"] = name_short;
|
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
// Start subprocess
|
|
|
|
proc = std::make_unique<Popen>(command, arguments, environment, working_dir,
|
|
|
|
shell);
|
|
|
|
logger->debug("Started sub-process with pid={}", proc->getPid());
|
2019-06-05 18:59:45 +02:00
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
stream_in = fdopen(proc->getFdIn(), "r");
|
|
|
|
if (!stream_in)
|
|
|
|
return -1;
|
2019-06-05 18:59:45 +02:00
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
stream_out = fdopen(proc->getFdOut(), "w");
|
|
|
|
if (!stream_out)
|
|
|
|
return -1;
|
2020-06-16 02:35:34 +02:00
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
int ret = Node::start();
|
|
|
|
if (!ret)
|
|
|
|
state = State::STARTED;
|
2020-06-16 02:35:34 +02:00
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
return 0;
|
2019-06-05 18:59:45 +02:00
|
|
|
}
|
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
int ExecNode::stop() {
|
|
|
|
int ret = Node::stop();
|
|
|
|
if (ret)
|
|
|
|
return ret;
|
2019-06-05 18:59:45 +02:00
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
// Stop subprocess
|
|
|
|
logger->debug("Killing sub-process with pid={}", proc->getPid());
|
|
|
|
proc->kill(SIGINT);
|
2019-06-05 18:59:45 +02:00
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
logger->debug("Waiting for sub-process with pid={} to terminate",
|
|
|
|
proc->getPid());
|
|
|
|
proc->close();
|
2019-06-05 18:59:45 +02:00
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
// TODO: Check exit code of subprocess?
|
|
|
|
return 0;
|
2019-06-05 18:59:45 +02:00
|
|
|
}
|
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
int ExecNode::_read(struct Sample *smps[], unsigned cnt) {
|
|
|
|
return formatter->scan(stream_in, smps, cnt);
|
2019-06-05 18:59:45 +02:00
|
|
|
}
|
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
int ExecNode::_write(struct Sample *smps[], unsigned cnt) {
|
|
|
|
int ret;
|
2019-06-05 18:59:45 +02:00
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
ret = formatter->print(stream_out, smps, cnt);
|
|
|
|
if (ret < 0)
|
|
|
|
return ret;
|
2019-06-05 18:59:45 +02:00
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
if (flush)
|
|
|
|
fflush(stream_out);
|
2019-06-05 18:59:45 +02:00
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
return cnt;
|
2019-06-05 18:59:45 +02:00
|
|
|
}
|
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
const std::string &ExecNode::getDetails() {
|
|
|
|
if (details.empty()) {
|
|
|
|
std::string wd = working_dir;
|
|
|
|
if (wd.empty()) {
|
|
|
|
char buf[128];
|
|
|
|
wd = getcwd(buf, sizeof(buf));
|
|
|
|
}
|
|
|
|
|
|
|
|
details = fmt::format("exec={}, shell={}, flush={}, #environment={}, "
|
|
|
|
"#arguments={}, working_dir={}",
|
|
|
|
command, shell ? "yes" : "no", flush ? "yes" : "no",
|
|
|
|
environment.size(), arguments.size(), wd);
|
|
|
|
}
|
|
|
|
|
|
|
|
return details;
|
2019-06-05 18:59:45 +02:00
|
|
|
}
|
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
std::vector<int> ExecNode::getPollFDs() { return {proc->getFdIn()}; }
|
2019-06-05 18:59:45 +02:00
|
|
|
|
2023-08-31 11:25:01 +02:00
|
|
|
// Register node
|
2021-08-10 10:12:48 -04:00
|
|
|
static char n[] = "exec";
|
2024-04-10 08:50:58 +02:00
|
|
|
static char d[] = "Run subprocesses with stdin/stdout communication";
|
2023-09-07 11:46:39 +02:00
|
|
|
static NodePlugin<ExecNode, n, d,
|
|
|
|
(int)NodeFactory::Flags::SUPPORTS_READ |
|
|
|
|
(int)NodeFactory::Flags::SUPPORTS_WRITE |
|
|
|
|
(int)NodeFactory::Flags::SUPPORTS_POLL>
|
|
|
|
p;
|