Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 42 additions & 18 deletions src/mtconnect/configuration/agent_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,9 @@ namespace mtconnect::configuration {
cerr << "Loading configuration from:" << *path << endl;

m_configFile = fs::canonical(*path);
addPathFront(m_configPaths, m_configFile.parent_path());
addPathBack(m_dataPaths, m_configFile.parent_path());
m_configPath = m_configFile.parent_path();
addPathFront(m_configPaths, m_configPath);
addPathBack(m_dataPaths, m_configPath);

ifstream file(m_configFile.c_str());
std::stringstream buffer;
Expand Down Expand Up @@ -617,30 +618,51 @@ namespace mtconnect::configuration {
// Get file names and patterns from the options.
auto file_name = *GetOption<string>(options, "file_name");
auto archive_pattern = *GetOption<string>(options, "archive_pattern");


// Default the log directory to the configuration file path.
logDirectory = m_configPath;
logFileName = fs::path(file_name);
logArchivePattern = fs::path(archive_pattern);
if (!logArchivePattern.has_filename())

// Determine the log directory based on the provided file name and archive pattern
if (logFileName.is_absolute())
logDirectory = logFileName.parent_path();
else if (logArchivePattern.is_absolute())
logDirectory = logArchivePattern.parent_path();

// If the log file name is relative and has a parent path, use it to determine the log directory
if (logFileName.is_relative() && logFileName.has_parent_path())
{
logArchivePattern = logArchivePattern / archiveFileName(get<string>(options["file_name"]));
logDirectory = logDirectory / logFileName.parent_path();
logFileName = logFileName.filename();
}
else if (logArchivePattern.is_relative() && logArchivePattern.has_parent_path())
{
logDirectory = logDirectory / logArchivePattern.parent_path();
logArchivePattern = logArchivePattern.filename();
}

if (logArchivePattern.is_relative())
logArchivePattern = fs::current_path() / logArchivePattern;
// Make sure the log archive pattern includes a file name, use the default file name as the base.
if (!logArchivePattern.has_filename())
logArchivePattern = logArchivePattern / archiveFileName(get<string>(options["file_name"]));

// Get the log directory from the archive path.
logDirectory = logArchivePattern.parent_path();
// Make the logArchivePattern and logFileName absolute paths
logDirectory = logDirectory.lexically_normal();

// If the file name does not specify a log directory, use the
// archive directory
logFileName = fs::path(file_name);
if (!logFileName.has_parent_path())
if (logArchivePattern.is_relative())
logArchivePattern = logDirectory / logArchivePattern;
logArchivePattern = logArchivePattern.lexically_normal();
auto archiveDir = logArchivePattern.parent_path();
fs::create_directories(logArchivePattern.parent_path());

if (logFileName.is_relative())
logFileName = logDirectory / logFileName;
else if (logFileName.is_relative())
logFileName = fs::current_path() / logFileName;
logFileName = logFileName.lexically_normal();
fs::create_directories(logFileName.parent_path());

// Create a text file sink
auto sink = boost::make_shared<text_sink>(
kw::file_name = logFileName, kw::target_file_name = logArchivePattern.filename(),
kw::file_name = logFileName.string(), kw::target_file_name = logArchivePattern.string(),
kw::auto_flush = true, kw::rotation_size = logRotationSize,
kw::open_mode = ios_base::out | ios_base::app, kw::format = formatter);

Expand All @@ -649,7 +671,8 @@ namespace mtconnect::configuration {

// Set up where the rotated files will be stored
sink->locked_backend()->set_file_collector(logr::sinks::file::make_collector(
kw::target = logDirectory, kw::max_size = maxLogFileSize, kw::max_files = max_index));
kw::target = archiveDir.string(), kw::max_size = maxLogFileSize,
kw::max_files = max_index));

if (rotationLogInterval > 0)
{
Expand Down Expand Up @@ -1248,7 +1271,8 @@ namespace mtconnect::configuration {
}
catch (exception &e)
{
LOG(info) << "Cannot load plugin " << name << " from " << path << " Reason: " << e.what();
LOG(debug) << "Plugin " << name << " from " << path << " not found, Reason: " << e.what()
<< ", trying next path if available.";
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/mtconnect/configuration/agent_config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ namespace mtconnect {
else
{
LOG(debug) << "Cannot find file '" << file << "' "
<< " in path " << path;
<< " in path " << path << ", continuing...";
}
}

Expand All @@ -312,7 +312,7 @@ namespace mtconnect {
if (!ec)
paths.emplace_back(con);
else
LOG(debug) << "Cannot file path: " << path << ", " << ec.message();
LOG(debug) << "Cannot find path: " << path << ", " << ec.message() << ", skipping...";
}

void addPathFront(std::list<std::filesystem::path> &paths, std::filesystem::path path)
Expand Down Expand Up @@ -374,6 +374,7 @@ namespace mtconnect {
std::string m_devicesFile;
std::filesystem::path m_exePath;
std::filesystem::path m_working;
std::filesystem::path m_configPath;

std::list<std::filesystem::path> m_configPaths;
std::list<std::filesystem::path> m_dataPaths;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ namespace mtconnect {
Requirement("Rotation", ValueType::VECTOR, 3, false),
Requirement("TranslationDataSet", ValueType::DATA_SET, false),
Requirement("RotationDataSet", ValueType::DATA_SET, false)});
transformation->setOrder({"Translation", "TranslationDataSet", "Rotation", "RotationDataSet"});
transformation->setOrder(
{"Translation", "TranslationDataSet", "Rotation", "RotationDataSet"});

auto coordinateSystem = make_shared<Factory>(Requirements {
Requirement("id", true), Requirement("name", false), Requirement("nativeName", false),
Expand Down
3 changes: 2 additions & 1 deletion src/mtconnect/device_model/configuration/motion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ namespace mtconnect {
Requirement("Rotation", ValueType::VECTOR, 3, false),
Requirement("TranslationDataSet", ValueType::DATA_SET, false),
Requirement("RotationDataSet", ValueType::DATA_SET, false)});
transformation->setOrder({"Translation", "TranslationDataSet", "Rotation", "RotationDataSet"});
transformation->setOrder(
{"Translation", "TranslationDataSet", "Rotation", "RotationDataSet"});

static auto motion = make_shared<Factory>(Requirements {
Requirement("id", true), Requirement("parentIdRef", false),
Expand Down
11 changes: 6 additions & 5 deletions src/mtconnect/device_model/configuration/solid_model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ namespace mtconnect {
if (!solidModel)
{
static auto transformation = make_shared<Factory>(
Requirements {Requirement("Translation", ValueType::VECTOR, 3, false),
Requirement("Rotation", ValueType::VECTOR, 3, false),
Requirement("TranslationDataSet", ValueType::DATA_SET, false),
Requirement("RotationDataSet", ValueType::DATA_SET, false)});
transformation->setOrder({"Translation", "TranslationDataSet", "Rotation", "RotationDataSet"});
Requirements {Requirement("Translation", ValueType::VECTOR, 3, false),
Requirement("Rotation", ValueType::VECTOR, 3, false),
Requirement("TranslationDataSet", ValueType::DATA_SET, false),
Requirement("RotationDataSet", ValueType::DATA_SET, false)});
transformation->setOrder(
{"Translation", "TranslationDataSet", "Rotation", "RotationDataSet"});

solidModel = make_shared<Factory>(
Requirements {{"id", true},
Expand Down
2 changes: 1 addition & 1 deletion src/mtconnect/entity/xml_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ namespace mtconnect::entity {
}
else
{
LOG(warning) << "Unexpected element: " << nodeQName(child);
// LOG(warning) << "Unexpected element: " << nodeQName(child);
errors.emplace_back(
new EntityError("Invalid element '" + nodeQName(child) + "'", qname));
}
Expand Down
16 changes: 15 additions & 1 deletion src/mtconnect/parser/xml_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -202,13 +202,27 @@ namespace mtconnect::parser {
{
auto device =
entity::XmlParser::parseXmlNode(Device::getRoot(), nodeset->nodeTab[i], errors);

if (device)
{
deviceList.emplace_back(dynamic_pointer_cast<Device>(device));
}
else
{
LOG(error) << "Failed to parse device, skipping";
}

if (!errors.empty())
{
for (auto &e : errors)
LOG(warning) << "Error parsing device: " << e->what();
{
if (device)
LOG(warning) << "When loading device " << device->get<string>("name")
<< ", A problem was skipped: "
<< e->what();
else
LOG(error) << "Failed to load device: " << e->what();
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/mtconnect/pipeline/shdr_token_mapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,8 @@ namespace mtconnect {
}
catch (entity::PropertyError &e)
{
LOG(warning) << "Cannot convert value for data item id '" << dataItem->getId()
<< "': " << *token << " - " << e.what();
LOG(debug) << "Cannot convert value for data item id '" << dataItem->getId()
<< "': " << *token << " - " << e.what();
if (schemaVersion >= SCHEMA_VERSION(2, 5) && validation)
{
props.insert_or_assign("quality", "INVALID"s);
Expand Down
3 changes: 3 additions & 0 deletions src/mtconnect/sink/rest_sink/parameter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ namespace mtconnect::sink::rest_sink {
Parameter(const std::string &n, ParameterType t = STRING, UrlPart p = PATH)
: m_name(n), m_type(t), m_part(p)
{}
Parameter(const std::string_view &n, ParameterType t = STRING, UrlPart p = PATH)
: m_name(n), m_type(t), m_part(p)
{}
Parameter(const Parameter &o) = default;

/// @brief to support std::set interface
Expand Down
4 changes: 4 additions & 0 deletions src/mtconnect/sink/rest_sink/response.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <boost/beast/http/status.hpp>

#include <filesystem>
#include <list>
#include <unordered_map>

#include "cached_file.hpp"
Expand Down Expand Up @@ -64,6 +65,9 @@ namespace mtconnect {
std::optional<std::string> m_requestId; ///< Request id from websocket sub

CachedFilePtr m_file; ///< Cached file if a file is being returned

/// @brief Additional per-response header fields (e.g. for CORS preflight)
std::list<std::pair<std::string, std::string>> m_fields;
};

using ResponsePtr = std::unique_ptr<Response>;
Expand Down
72 changes: 64 additions & 8 deletions src/mtconnect/sink/rest_sink/routing.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@

#pragma once

#include <boost/algorithm/string.hpp>
#include <boost/beast/http/verb.hpp>

#include <iostream>

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#include <iostream> appears unused in this header. Consider removing it to avoid unnecessary recompiles and header bloat.

Suggested change
#include <iostream>

Copilot uses AI. Check for mistakes.
#include <list>
#include <optional>
#include <regex>
Expand Down Expand Up @@ -84,7 +86,8 @@ namespace mtconnect::sink::rest_sink {
m_pattern(pattern),
m_command(request),
m_function(function),
m_swagger(swagger)
m_swagger(swagger),
m_catchAll(true)
{}

/// @brief Added summary and description to the routing
Expand Down Expand Up @@ -295,6 +298,15 @@ namespace mtconnect::sink::rest_sink {
return true;
}

/// @brief check if the routing's path pattern matches a given path (ignoring verb)
/// @param[in] path the request path to test
/// @return `true` if the path matches this routing's pattern
bool matchesPath(const std::string &path) const
{
std::smatch m;
return std::regex_match(path, m, m_pattern);
}

/// @brief check if this is related to a swagger API
/// @returns `true` if related to swagger
auto isSwagger() const { return m_swagger; }
Expand All @@ -304,6 +316,10 @@ namespace mtconnect::sink::rest_sink {
/// @brief Get the routing `verb`
const auto &getVerb() const { return m_verb; }

/// @brief Check if the route is a catch-all (every path segment is a parameter)
/// @returns `true` if all path segments are parameters (e.g. `/{device}`)
auto isCatchAll() const { return m_catchAll; }

/// @brief Get the optional command associated with the routing
/// @returns optional routing
const auto &getCommand() const { return m_command; }
Expand All @@ -319,21 +335,60 @@ namespace mtconnect::sink::rest_sink {
protected:
void pathParameters(std::string s)
{
std::regex reg("\\{([^}]+)\\}");
std::smatch match;
std::stringstream pat;

while (regex_search(s, match, reg))
using namespace boost::algorithm;
using SplitList = std::list<boost::iterator_range<std::string::iterator>>;

SplitList parts;
auto pos = s.find_first_not_of('/');
if (pos != std::string::npos)
{
pat << match.prefix() << "([^/]+)";
m_pathParameters.emplace_back(match[1]);
s = match.suffix().str();
auto range = boost::make_iterator_range(s.begin() + pos, s.end());
split(parts, range, [](char c) { return c == '/'; });

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pathParameters currently calls boost::algorithm::split(parts, range, [](char c) { return c == '/'; });, but Boost's split expects a finder (e.g., boost::is_any_of("/")), not a char -> bool predicate. As written this is likely to fail to compile; use boost::is_any_of("/") (and an appropriate container type, e.g. std::vector<iterator_range<...>>) for consistency with other code (see SessionImpl::parseQueries).

Suggested change
split(parts, range, [](char c) { return c == '/'; });
split(parts, range, boost::is_any_of("/"));

Copilot uses AI. Check for mistakes.
}

bool hasLiteral = false;
for (auto &p : parts)
{
auto start = p.begin();
auto end = p.end();

auto openBrace = std::find(start, end, '{');
decltype(openBrace) closeBrace {end};
if (openBrace != end && std::distance(openBrace, end) > 2)
closeBrace = std::find(openBrace + 1, end, '}');

pat << "/";
if (openBrace != end && closeBrace != end)
{
if (openBrace > start)
{
pat << std::string_view(start, openBrace);
hasLiteral = true;
}
std::string_view param(openBrace + 1, closeBrace);
pat << "([^/]+)";
if (closeBrace + 1 < end)
{
pat << std::string_view(closeBrace + 1, end);
hasLiteral = true;
}
m_pathParameters.emplace_back(param);
Comment on lines +365 to +377

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::string_view(start, openBrace) / std::string_view(openBrace + 1, closeBrace) / etc. are constructed from iterators, but std::string_view does not have iterator-pair constructors. This will not compile; create the view from a pointer+length (e.g. &*it + std::distance) or append via pat.write() / std::string(start, openBrace) instead.

Copilot uses AI. Check for mistakes.
}
else
{
pat << std::string_view(start, end);
hasLiteral = true;
}
}
pat << s;
pat << "/?";

m_patternText = pat.str();
m_pattern = std::regex(m_patternText);

// A route is catch-all if it has parameters but no literal path segments
m_catchAll = !m_pathParameters.empty() && !hasLiteral;
}

void queryParameters(std::string s)
Expand Down Expand Up @@ -513,5 +568,6 @@ namespace mtconnect::sink::rest_sink {
std::optional<std::string> m_description;

bool m_swagger = false;
bool m_catchAll = false;
};
} // namespace mtconnect::sink::rest_sink
Loading
Loading