Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .github/workflows/website-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
cmake -S . -B ./build
-DCMAKE_BUILD_TYPE:STRING=Release
-DSOURCEMETA_CORE_LANG_IO:BOOL=OFF
-DSOURCEMETA_CORE_LANG_PROCESS:BOOL=OFF
-DSOURCEMETA_CORE_TIME:BOOL=OFF
-DSOURCEMETA_CORE_UUID:BOOL=OFF
-DSOURCEMETA_CORE_REGEX:BOOL=OFF
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/website-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
cmake -S . -B ./build
-DCMAKE_BUILD_TYPE:STRING=Release
-DSOURCEMETA_CORE_LANG_IO:BOOL=OFF
-DSOURCEMETA_CORE_LANG_PROCESS:BOOL=OFF
-DSOURCEMETA_CORE_TIME:BOOL=OFF
-DSOURCEMETA_CORE_UUID:BOOL=OFF
-DSOURCEMETA_CORE_REGEX:BOOL=OFF
Expand Down
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")

# Options
option(SOURCEMETA_CORE_LANG_IO "Build the Sourcemeta Core language I/O library" ON)
option(SOURCEMETA_CORE_LANG_PROCESS "Build the Sourcemeta Core language Process library" ON)
option(SOURCEMETA_CORE_LANG_PARALLEL "Build the Sourcemeta Core language parallel library" ON)
option(SOURCEMETA_CORE_TIME "Build the Sourcemeta Core time library" ON)
option(SOURCEMETA_CORE_UUID "Build the Sourcemeta Core UUID library" ON)
Expand Down Expand Up @@ -58,6 +59,10 @@ if(SOURCEMETA_CORE_LANG_IO)
add_subdirectory(src/lang/io)
endif()

if(SOURCEMETA_CORE_LANG_PROCESS)
add_subdirectory(src/lang/process)
endif()

if(SOURCEMETA_CORE_LANG_PARALLEL)
find_package(Threads REQUIRED)
add_subdirectory(src/lang/parallel)
Expand Down Expand Up @@ -161,6 +166,10 @@ if(SOURCEMETA_CORE_TESTS)
add_subdirectory(test/io)
endif()

if(SOURCEMETA_CORE_LANG_PROCESS)
add_subdirectory(test/process)
endif()

if(SOURCEMETA_CORE_LANG_PARALLEL)
add_subdirectory(test/parallel)
endif()
Expand Down
3 changes: 3 additions & 0 deletions config.cmake.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ list(APPEND SOURCEMETA_CORE_COMPONENTS ${Core_FIND_COMPONENTS})
list(APPEND SOURCEMETA_CORE_COMPONENTS ${core_FIND_COMPONENTS})
if(NOT SOURCEMETA_CORE_COMPONENTS)
list(APPEND SOURCEMETA_CORE_COMPONENTS io)
list(APPEND SOURCEMETA_CORE_COMPONENTS process)
list(APPEND SOURCEMETA_CORE_COMPONENTS parallel)
list(APPEND SOURCEMETA_CORE_COMPONENTS time)
list(APPEND SOURCEMETA_CORE_COMPONENTS uuid)
Expand All @@ -28,6 +29,8 @@ include(CMakeFindDependencyMacro)
foreach(component ${SOURCEMETA_CORE_COMPONENTS})
if(component STREQUAL "io")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake")
elseif(component STREQUAL "process")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_process.cmake")
elseif(component STREQUAL "parallel")
find_dependency(Threads)
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_parallel.cmake")
Expand Down
7 changes: 7 additions & 0 deletions src/lang/process/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME process
PRIVATE_HEADERS error.h
SOURCES spawn.cc)

if(SOURCEMETA_CORE_INSTALL)
sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME process)
endif()
72 changes: 72 additions & 0 deletions src/lang/process/include/sourcemeta/core/process.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#ifndef SOURCEMETA_CORE_PROCESS_H_
#define SOURCEMETA_CORE_PROCESS_H_

#ifndef SOURCEMETA_CORE_PROCESS_EXPORT
#include <sourcemeta/core/process_export.h>
#endif

// NOLINTBEGIN(misc-include-cleaner)
#include <sourcemeta/core/process_error.h>
// NOLINTEND(misc-include-cleaner)

#include <filesystem> // std::filesystem
#include <initializer_list> // std::initializer_list
#include <span> // std::span
#include <string_view> // std::string_view

/// @defgroup process Process
/// @brief Process related utilities
///
/// This functionality is included as follows:
///
/// ```cpp
/// #include <sourcemeta/core/process.h>
/// ```

namespace sourcemeta::core {

/// @ingroup process
///
/// Spawn a program piping its output to the current stdio configuration.
/// The directory parameter specifies the working directory for the spawned
/// process. It must be an absolute path to an existing directory.
///
/// ```cpp
/// #include <sourcemeta/core/process.h>
/// #include <cassert>
///
/// const auto exit_code{sourcemeta::core::spawn("echo", {"foo"})};
/// assert(exit_code == 0);
/// ```
SOURCEMETA_CORE_PROCESS_EXPORT
auto spawn(const std::string &program,
std::initializer_list<std::string_view> arguments,
const std::filesystem::path &directory =
std::filesystem::current_path()) -> int;

/// @ingroup process
///
/// Spawn a program piping its output to the current stdio configuration.
/// This overload accepts a span for dynamic argument lists.
/// The directory parameter specifies the working directory for the spawned
/// process. It must be an absolute path to an existing directory.
///
/// ```cpp
/// #include <sourcemeta/core/process.h>
/// #include <vector>
/// #include <string_view>
/// #include <cassert>
///
/// std::vector<std::string_view> arguments{"foo", "bar"};
/// const auto exit_code{sourcemeta::core::spawn("echo", arguments)};
/// assert(exit_code == 0);
/// ```
SOURCEMETA_CORE_PROCESS_EXPORT
auto spawn(
const std::string &program, std::span<const std::string_view> arguments,
const std::filesystem::path &directory = std::filesystem::current_path())
-> int;

} // namespace sourcemeta::core

#endif
83 changes: 83 additions & 0 deletions src/lang/process/include/sourcemeta/core/process_error.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#ifndef SOURCEMETA_CORE_PROCESS_ERROR_H_
#define SOURCEMETA_CORE_PROCESS_ERROR_H_

#ifndef SOURCEMETA_CORE_PROCESS_EXPORT
#include <sourcemeta/core/process_export.h>
#endif

#include <exception> // std::exception
#include <initializer_list> // std::initializer_list
#include <span> // std::span
#include <string> // std::string
#include <string_view> // std::string_view
#include <utility> // std::move
#include <vector> // std::vector

namespace sourcemeta::core {

// Exporting symbols that depends on the standard C++ library is considered
// safe.
// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN
#if defined(_MSC_VER)
#pragma warning(disable : 4251 4275)
#endif

/// @ingroup process
/// An executable program could not be found
class SOURCEMETA_CORE_PROCESS_EXPORT ProcessProgramNotNotFoundError
: public std::exception {
public:
ProcessProgramNotNotFoundError(std::string program)
: program_{std::move(program)} {}

[[nodiscard]] auto what() const noexcept -> const char * override {
return "Could not locate the requested program";
}

[[nodiscard]] auto program() const noexcept -> std::string_view {
return this->program_;
}

private:
std::string program_;
};

/// @ingroup process
/// A spawned process terminated abnormally
class SOURCEMETA_CORE_PROCESS_EXPORT ProcessSpawnError : public std::exception {
public:
ProcessSpawnError(std::string program,
std::initializer_list<std::string_view> arguments)
: program_{std::move(program)},
arguments_{arguments.begin(), arguments.end()} {}

ProcessSpawnError(std::string program,
std::span<const std::string_view> arguments)
: program_{std::move(program)},
arguments_{arguments.begin(), arguments.end()} {}

[[nodiscard]] auto what() const noexcept -> const char * override {
return "Process terminated abnormally";
}

[[nodiscard]] auto program() const noexcept -> std::string_view {
return this->program_;
}

[[nodiscard]] auto arguments() const noexcept
-> const std::vector<std::string> & {
return this->arguments_;
}

private:
std::string program_;
std::vector<std::string> arguments_;
};

#if defined(_MSC_VER)
#pragma warning(default : 4251 4275)
#endif

} // namespace sourcemeta::core

#endif
152 changes: 152 additions & 0 deletions src/lang/process/spawn.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#include <sourcemeta/core/process.h>

#include <cassert> // assert
#include <filesystem> // std::filesystem
#include <initializer_list> // std::initializer_list
#include <span> // std::span
#include <vector> // std::vector

#if defined(_WIN32) && !defined(__MSYS__) && !defined(__CYGWIN__) && \
!defined(__MINGW32__) && !defined(__MINGW64__)
#define WIN32_LEAN_AND_MEAN
#include <sstream> // std::ostringstream
#include <windows.h> // CreateProcess, PROCESS_INFORMATION, STARTUPINFO, WaitForSingleObject, GetExitCodeProcess
#else
#include <spawn.h> // posix_spawnp, posix_spawnattr_t, posix_spawnattr_init, posix_spawnattr_destroy, posix_spawn_file_actions_t, posix_spawn_file_actions_init, posix_spawn_file_actions_destroy, pid_t
#include <sys/wait.h> // waitpid, WIFEXITED, WEXITSTATUS

#if defined(__MSYS__) || defined(__CYGWIN__) || defined(__MINGW32__) || \
defined(__MINGW64__)
#include <unistd.h> // chdir
#endif

extern char **environ;
#endif

namespace sourcemeta::core {

auto spawn(const std::string &program,
std::span<const std::string_view> arguments,
const std::filesystem::path &directory) -> int {
assert(directory.is_absolute());
assert(std::filesystem::exists(directory));
assert(std::filesystem::is_directory(directory));

#if defined(_WIN32) && !defined(__MSYS__) && !defined(__CYGWIN__) && \
!defined(__MINGW32__) && !defined(__MINGW64__)
std::ostringstream command_line;
command_line << program;

for (const auto &argument : arguments) {
command_line << " ";
// Quote arguments that contain spaces
const std::string arg_str{argument};
if (arg_str.find(' ') != std::string::npos) {
command_line << "\"" << arg_str << "\"";
} else {
command_line << arg_str;
}
}

std::string cmd_line_str = command_line.str();
std::vector<char> cmd_line(cmd_line_str.begin(), cmd_line_str.end());
cmd_line.push_back('\0');

STARTUPINFOA startup_info{};
startup_info.cb = sizeof(startup_info);
PROCESS_INFORMATION process_info{};
const std::string working_dir = directory.string();
const BOOL success =
CreateProcessA(nullptr, // lpApplicationName
cmd_line.data(), // lpCommandLine (modifiable)
nullptr, // lpProcessAttributes
nullptr, // lpThreadAttributes
TRUE, // bInheritHandles
0, // dwCreationFlags
nullptr, // lpEnvironment
working_dir.c_str(), // lpCurrentDirectory
&startup_info, // lpStartupInfo
&process_info // lpProcessInformation
);

if (!success) {
throw ProcessProgramNotNotFoundError{program};
}

WaitForSingleObject(process_info.hProcess, INFINITE);

DWORD exit_code;
if (!GetExitCodeProcess(process_info.hProcess, &exit_code)) {
CloseHandle(process_info.hProcess);
CloseHandle(process_info.hThread);
throw ProcessSpawnError{program, arguments};
}

CloseHandle(process_info.hProcess);
CloseHandle(process_info.hThread);

return static_cast<int>(exit_code);
#else
std::vector<const char *> argv;
argv.reserve(arguments.size() + 2);
argv.push_back(program.c_str());

for (const auto &argument : arguments) {
argv.push_back(argument.data());
}

argv.push_back(nullptr);

posix_spawnattr_t attributes;
posix_spawnattr_init(&attributes);

posix_spawn_file_actions_t file_actions;
posix_spawn_file_actions_init(&file_actions);

#if defined(__MSYS__) || defined(__CYGWIN__) || defined(__MINGW32__) || \
defined(__MINGW64__)
const std::filesystem::path original_directory{
std::filesystem::current_path()};
std::filesystem::current_path(directory);
#else
posix_spawn_file_actions_addchdir_np(&file_actions, directory.c_str());
#endif

pid_t process_id;
const int spawn_result{
posix_spawnp(&process_id, program.c_str(), &file_actions, &attributes,
const_cast<char *const *>(argv.data()), environ)};

posix_spawn_file_actions_destroy(&file_actions);
posix_spawnattr_destroy(&attributes);

#if defined(__MSYS__) || defined(__CYGWIN__) || defined(__MINGW32__) || \
defined(__MINGW64__)
std::filesystem::current_path(original_directory);
#endif

if (spawn_result != 0) {
throw ProcessProgramNotNotFoundError{program};
}

int status;
waitpid(process_id, &status, 0);

if (WIFEXITED(status)) {
return WEXITSTATUS(status);
}

throw ProcessSpawnError{program, arguments};
#endif
}

auto spawn(const std::string &program,
std::initializer_list<std::string_view> arguments,
const std::filesystem::path &directory) -> int {
return spawn(
program,
std::span<const std::string_view>{arguments.begin(), arguments.size()},
directory);
}

} // namespace sourcemeta::core
2 changes: 1 addition & 1 deletion test/parallel/parallel_for_each_test.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#include <gtest/gtest.h>

#include "sourcemeta/core/parallel_for_each.h"
#include <sourcemeta/core/parallel.h>

#include <algorithm>
#include <atomic>
Expand Down
Loading