Skip to content

Commit

Permalink
Allow configuring logging directory through environment variables (#53)
Browse files Browse the repository at this point in the history
Signed-off-by: Christophe Bedard <[email protected]>
  • Loading branch information
christophebedard authored Oct 13, 2020
1 parent 37bb732 commit e2456ba
Show file tree
Hide file tree
Showing 9 changed files with 380 additions and 48 deletions.
34 changes: 27 additions & 7 deletions rcl_logging_interface/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,41 @@ endif()
find_package(ament_cmake_ros REQUIRED)
find_package(rcutils REQUIRED)

add_library(${PROJECT_NAME} INTERFACE)
target_include_directories(${PROJECT_NAME} INTERFACE
set(${PROJECT_NAME}_sources
"src/logging_dir.c"
)
add_library(${PROJECT_NAME} ${${PROJECT_NAME}_sources})
target_include_directories(${PROJECT_NAME} PUBLIC
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
"$<INSTALL_INTERFACE:include>")
ament_target_dependencies(${PROJECT_NAME} INTERFACE rcutils)
ament_target_dependencies(${PROJECT_NAME} rcutils)
target_compile_definitions(${PROJECT_NAME} PRIVATE "RCL_LOGGING_INTERFACE_BUILDING_DLL")

install(
DIRECTORY include/
DESTINATION include
)
install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME}
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin)

install(DIRECTORY include/${PROJECT_NAME}/
DESTINATION include/${PROJECT_NAME}
)

ament_export_include_directories(include)
ament_export_targets(${PROJECT_NAME})
ament_export_dependencies(rcutils)

if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
ament_lint_auto_find_test_dependencies()

find_package(ament_cmake_gtest REQUIRED)
find_package(rcpputils REQUIRED)
ament_add_gtest(test_get_logging_directory test/test_get_logging_directory.cpp)
if(TARGET test_get_logging_directory)
target_link_libraries(test_get_logging_directory ${PROJECT_NAME})
target_include_directories(test_get_logging_directory PRIVATE include)
ament_target_dependencies(test_get_logging_directory rcutils rcpputils)
endif()
endif()

ament_package()
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ typedef enum
{
RCL_LOGGING_RET_OK = 0,
RCL_LOGGING_RET_ERROR = 2,
RCL_LOGGING_RET_INVALID_ARGUMENT = 11,
RCL_LOGGING_RET_CONFIG_FILE_DOESNT_EXIST = 21,
RCL_LOGGING_RET_CONFIG_FILE_INVALID = 22,
} rcl_logging_ret_t;
Expand Down Expand Up @@ -91,6 +92,30 @@ RCL_LOGGING_INTERFACE_PUBLIC
RCUTILS_WARN_UNUSED
rcl_logging_ret_t rcl_logging_external_set_logger_level(const char * name, int level);

/// Get the logging directory.
/**
* Uses various environment variables to construct a logging directory path.
*
* Use $ROS_LOG_DIR if ROS_LOG_DIR is set and not empty.
* Otherwise, use $ROS_HOME/log, using ~/.ros for ROS_HOME if not set or if empty.
*
* It also expands an initial '~' to the current user's home directory,
* and converts the path separator if necessary.
*
* If successful, the directory C string should be deallocated using the given allocator when it is
* no longer needed.
*
* \param[in] allocator The allocator to use for memory allocation.
* \param[out] directory The C string pointer at which to write the directory path.
* Only meaningful if the call is successful. Must not be nullptr and must point to nullptr.
* \return RCL_LOGGING_RET_OK if successful, or
* \return RCL_LOGGING_RET_INVALID_ARGUMENT if any arguments are invalid, or
* \return RCL_LOGGING_RET_ERROR if an unspecified error occurs.
*/
RCL_LOGGING_INTERFACE_PUBLIC
rcl_logging_ret_t
rcl_logging_get_logging_directory(rcutils_allocator_t allocator, char ** directory);

#ifdef __cplusplus
}
#endif
Expand Down
4 changes: 4 additions & 0 deletions rcl_logging_interface/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@

<depend>rcutils</depend>

<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<test_depend>rcpputils</test_depend>

<export>
<build_type>ament_cmake</build_type>
</export>
Expand Down
107 changes: 107 additions & 0 deletions rcl_logging_interface/src/logging_dir.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright 2020 Open Source Robotics Foundation, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <rcutils/allocator.h>
#include <rcutils/error_handling.h>
#include <rcutils/filesystem.h>
#include <rcutils/format_string.h>
#include <rcutils/get_env.h>
#include <rcutils/strdup.h>

#include "rcl_logging_interface/rcl_logging_interface.h"

rcl_logging_ret_t
rcl_logging_get_logging_directory(rcutils_allocator_t allocator, char ** directory)
{
if (NULL == directory) {
RCUTILS_SET_ERROR_MSG("directory argument must not be null");
return RCL_LOGGING_RET_INVALID_ARGUMENT;
}
if (NULL != *directory) {
RCUTILS_SET_ERROR_MSG("directory argument must point to null");
return RCL_LOGGING_RET_INVALID_ARGUMENT;
}

const char * log_dir_env;
const char * err = rcutils_get_env("ROS_LOG_DIR", &log_dir_env);
if (NULL != err) {
RCUTILS_SET_ERROR_MSG("rcutils_get_env failed");
return RCL_LOGGING_RET_ERROR;
}
if ('\0' != *log_dir_env) {
*directory = rcutils_strdup(log_dir_env, allocator);
if (NULL == *directory) {
RCUTILS_SET_ERROR_MSG("rcutils_strdup failed");
return RCL_LOGGING_RET_ERROR;
}
} else {
const char * ros_home_dir_env;
err = rcutils_get_env("ROS_HOME", &ros_home_dir_env);
if (NULL != err) {
RCUTILS_SET_ERROR_MSG("rcutils_get_env failed");
return RCL_LOGGING_RET_ERROR;
}
char * ros_home_dir;
if ('\0' == *ros_home_dir_env) {
ros_home_dir = rcutils_join_path("~", ".ros", allocator);
if (NULL == ros_home_dir) {
RCUTILS_SET_ERROR_MSG("rcutils_join_path failed");
return RCL_LOGGING_RET_ERROR;
}
} else {
ros_home_dir = rcutils_strdup(ros_home_dir_env, allocator);
if (NULL == ros_home_dir) {
RCUTILS_SET_ERROR_MSG("rcutils_strdup failed");
return RCL_LOGGING_RET_ERROR;
}
}
*directory = rcutils_join_path(ros_home_dir, "log", allocator);
allocator.deallocate(ros_home_dir, allocator.state);
if (NULL == *directory) {
RCUTILS_SET_ERROR_MSG("rcutils_join_path failed");
return RCL_LOGGING_RET_ERROR;
}
}

// Expand home directory
if ('~' == (*directory)[0]) {
const char * homedir = rcutils_get_home_dir();
if (NULL == homedir) {
allocator.deallocate(*directory, allocator.state);
RCUTILS_SET_ERROR_MSG("failed to get the home directory");
return RCL_LOGGING_RET_ERROR;
}
char * directory_not_expanded = *directory;
*directory = rcutils_format_string_limit(
allocator,
strlen(homedir) + strlen(directory_not_expanded),
"%s%s",
homedir,
directory_not_expanded + 1);
allocator.deallocate(directory_not_expanded, allocator.state);
if (NULL == *directory) {
RCUTILS_SET_ERROR_MSG("rcutils_format_string failed");
return RCL_LOGGING_RET_ERROR;
}
}

char * directory_maybe_not_native = *directory;
*directory = rcutils_to_native_path(directory_maybe_not_native, allocator);
allocator.deallocate(directory_maybe_not_native, allocator.state);
if (NULL == *directory) {
RCUTILS_SET_ERROR_MSG("rcutils_to_native_path failed");
return RCL_LOGGING_RET_ERROR;
}
return RCL_LOGGING_RET_OK;
}
183 changes: 183 additions & 0 deletions rcl_logging_interface/test/test_get_logging_directory.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Copyright 2020 Open Source Robotics Foundation, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <rcpputils/filesystem_helper.hpp>
#include <rcpputils/get_env.hpp>
#include <rcutils/allocator.h>
#include <rcutils/env.h>
#include <rcutils/error_handling.h>

#include <iostream>
#include <string>

#include "gtest/gtest.h"
#include "rcl_logging_interface/rcl_logging_interface.h"

// This is a helper class that resets an environment
// variable when leaving scope
class RestoreEnvVar
{
public:
explicit RestoreEnvVar(const std::string & name)
: name_(name),
value_(rcpputils::get_env_var(name.c_str()))
{
}

~RestoreEnvVar()
{
if (!rcutils_set_env(name_.c_str(), value_.c_str())) {
std::cerr << "Failed to restore value of environment variable: " << name_ << std::endl;
}
}

private:
const std::string name_;
const std::string value_;
};

TEST(test_logging_directory, directory)
{
RestoreEnvVar home_var("HOME");
RestoreEnvVar userprofile_var("USERPROFILE");
ASSERT_EQ(true, rcutils_set_env("HOME", nullptr));
ASSERT_EQ(true, rcutils_set_env("USERPROFILE", nullptr));
ASSERT_EQ(true, rcutils_set_env("ROS_LOG_DIR", nullptr));
ASSERT_EQ(true, rcutils_set_env("ROS_HOME", nullptr));

rcutils_allocator_t allocator = rcutils_get_default_allocator();

// Invalid argument if given a nullptr
EXPECT_EQ(
RCL_LOGGING_RET_INVALID_ARGUMENT, rcl_logging_get_logging_directory(allocator, nullptr));
EXPECT_TRUE(rcutils_error_is_set());
rcutils_reset_error();
// Invalid argument if the C string is not nullptr
char * could_leak = const_cast<char *>("/could/be/leaked");
EXPECT_EQ(
RCL_LOGGING_RET_INVALID_ARGUMENT, rcl_logging_get_logging_directory(allocator, &could_leak));
EXPECT_TRUE(rcutils_error_is_set());
rcutils_reset_error();

// Fails without any relevant env vars at all (HOME included)
char * directory = nullptr;
EXPECT_EQ(RCL_LOGGING_RET_ERROR, rcl_logging_get_logging_directory(allocator, &directory));
EXPECT_TRUE(rcutils_error_is_set());
rcutils_reset_error();
directory = nullptr;

// Default case without ROS_LOG_DIR or ROS_HOME being set (but with HOME)
rcpputils::fs::path fake_home("/fake_home_dir");
ASSERT_EQ(true, rcutils_set_env("HOME", fake_home.string().c_str()));
ASSERT_EQ(true, rcutils_set_env("USERPROFILE", fake_home.string().c_str()));
rcpputils::fs::path default_dir = fake_home / ".ros" / "log";
EXPECT_EQ(RCL_LOGGING_RET_OK, rcl_logging_get_logging_directory(allocator, &directory));
EXPECT_STREQ(directory, default_dir.string().c_str());
allocator.deallocate(directory, allocator.state);
directory = nullptr;

// Use $ROS_LOG_DIR if it is set
const char * my_log_dir_raw = "/my/ros_log_dir";
rcpputils::fs::path my_log_dir(my_log_dir_raw);
ASSERT_EQ(true, rcutils_set_env("ROS_LOG_DIR", my_log_dir.string().c_str()));
EXPECT_EQ(RCL_LOGGING_RET_OK, rcl_logging_get_logging_directory(allocator, &directory));
EXPECT_STREQ(directory, my_log_dir.string().c_str());
allocator.deallocate(directory, allocator.state);
directory = nullptr;
// Make sure it converts path separators when necessary
ASSERT_EQ(true, rcutils_set_env("ROS_LOG_DIR", my_log_dir_raw));
EXPECT_EQ(RCL_LOGGING_RET_OK, rcl_logging_get_logging_directory(allocator, &directory));
EXPECT_STREQ(directory, my_log_dir.string().c_str());
allocator.deallocate(directory, allocator.state);
directory = nullptr;
// Setting ROS_HOME won't change anything since ROS_LOG_DIR is used first
ASSERT_EQ(true, rcutils_set_env("ROS_HOME", "/this/wont/be/used"));
EXPECT_EQ(RCL_LOGGING_RET_OK, rcl_logging_get_logging_directory(allocator, &directory));
EXPECT_STREQ(directory, my_log_dir.string().c_str());
allocator.deallocate(directory, allocator.state);
directory = nullptr;
ASSERT_EQ(true, rcutils_set_env("ROS_HOME", nullptr));
// Empty is considered unset
ASSERT_EQ(true, rcutils_set_env("ROS_LOG_DIR", ""));
EXPECT_EQ(RCL_LOGGING_RET_OK, rcl_logging_get_logging_directory(allocator, &directory));
EXPECT_STREQ(directory, default_dir.string().c_str());
allocator.deallocate(directory, allocator.state);
directory = nullptr;
// Make sure '~' is expanded to the home directory
ASSERT_EQ(true, rcutils_set_env("ROS_LOG_DIR", "~/logdir"));
EXPECT_EQ(RCL_LOGGING_RET_OK, rcl_logging_get_logging_directory(allocator, &directory));
rcpputils::fs::path fake_log_dir = fake_home / "logdir";
EXPECT_STREQ(directory, fake_log_dir.string().c_str());
allocator.deallocate(directory, allocator.state);
directory = nullptr;
// But it should only be expanded if it's at the beginning
rcpputils::fs::path prefixed_fake_log_dir("/prefix/~/logdir");
ASSERT_EQ(true, rcutils_set_env("ROS_LOG_DIR", prefixed_fake_log_dir.string().c_str()));
EXPECT_EQ(RCL_LOGGING_RET_OK, rcl_logging_get_logging_directory(allocator, &directory));
EXPECT_STREQ(directory, prefixed_fake_log_dir.string().c_str());
allocator.deallocate(directory, allocator.state);
directory = nullptr;
ASSERT_EQ(true, rcutils_set_env("ROS_LOG_DIR", "~"));
EXPECT_EQ(RCL_LOGGING_RET_OK, rcl_logging_get_logging_directory(allocator, &directory));
EXPECT_STREQ(directory, fake_home.string().c_str());
allocator.deallocate(directory, allocator.state);
directory = nullptr;
rcpputils::fs::path home_trailing_slash(fake_home.string() + "/");
ASSERT_EQ(true, rcutils_set_env("ROS_LOG_DIR", "~/"));
EXPECT_EQ(RCL_LOGGING_RET_OK, rcl_logging_get_logging_directory(allocator, &directory));
EXPECT_STREQ(directory, home_trailing_slash.string().c_str());
allocator.deallocate(directory, allocator.state);
directory = nullptr;

ASSERT_EQ(true, rcutils_set_env("ROS_LOG_DIR", nullptr));

// Without ROS_LOG_DIR, use $ROS_HOME/log
rcpputils::fs::path fake_ros_home = fake_home / ".fakeroshome";
ASSERT_EQ(true, rcutils_set_env("ROS_HOME", fake_ros_home.string().c_str()));
EXPECT_EQ(RCL_LOGGING_RET_OK, rcl_logging_get_logging_directory(allocator, &directory));
rcpputils::fs::path fake_ros_home_log_dir = fake_ros_home / "log";
EXPECT_STREQ(directory, fake_ros_home_log_dir.string().c_str());
allocator.deallocate(directory, allocator.state);
directory = nullptr;
// Make sure it converts path separators when necessary
const char * my_ros_home_raw = "/my/ros/home";
ASSERT_EQ(true, rcutils_set_env("ROS_HOME", my_ros_home_raw));
EXPECT_EQ(RCL_LOGGING_RET_OK, rcl_logging_get_logging_directory(allocator, &directory));
rcpputils::fs::path my_ros_home_log_dir = rcpputils::fs::path(my_ros_home_raw) / "log";
EXPECT_STREQ(directory, my_ros_home_log_dir.string().c_str());
allocator.deallocate(directory, allocator.state);
directory = nullptr;
// Empty is considered unset
ASSERT_EQ(true, rcutils_set_env("ROS_HOME", ""));
EXPECT_EQ(RCL_LOGGING_RET_OK, rcl_logging_get_logging_directory(allocator, &directory));
EXPECT_STREQ(directory, default_dir.string().c_str());
allocator.deallocate(directory, allocator.state);
directory = nullptr;
// Make sure '~' is expanded to the home directory
ASSERT_EQ(true, rcutils_set_env("ROS_HOME", "~/.fakeroshome"));
EXPECT_EQ(RCL_LOGGING_RET_OK, rcl_logging_get_logging_directory(allocator, &directory));
EXPECT_STREQ(directory, fake_ros_home_log_dir.string().c_str());
allocator.deallocate(directory, allocator.state);
directory = nullptr;
// But it should only be expanded if it's at the beginning
rcpputils::fs::path prefixed_fake_ros_home("/prefix/~/.fakeroshome");
rcpputils::fs::path prefixed_fake_ros_home_log_dir = prefixed_fake_ros_home / "log";
ASSERT_EQ(true, rcutils_set_env("ROS_HOME", prefixed_fake_ros_home.string().c_str()));
EXPECT_EQ(RCL_LOGGING_RET_OK, rcl_logging_get_logging_directory(allocator, &directory));
EXPECT_STREQ(directory, prefixed_fake_ros_home_log_dir.string().c_str());
allocator.deallocate(directory, allocator.state);
directory = nullptr;

ASSERT_EQ(true, rcutils_set_env("ROS_HOME", nullptr));
}
Loading

0 comments on commit e2456ba

Please sign in to comment.