Skip to content

Commit a5dbd8f

Browse files
authored
Implement a spawn function in a new process module (#2005)
Signed-off-by: Juan Cruz Viotti <[email protected]>
1 parent 22d862f commit a5dbd8f

File tree

15 files changed

+561
-1
lines changed

15 files changed

+561
-1
lines changed

.github/workflows/website-build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ jobs:
1717
cmake -S . -B ./build
1818
-DCMAKE_BUILD_TYPE:STRING=Release
1919
-DSOURCEMETA_CORE_LANG_IO:BOOL=OFF
20+
-DSOURCEMETA_CORE_LANG_PROCESS:BOOL=OFF
2021
-DSOURCEMETA_CORE_TIME:BOOL=OFF
2122
-DSOURCEMETA_CORE_UUID:BOOL=OFF
2223
-DSOURCEMETA_CORE_REGEX:BOOL=OFF

.github/workflows/website-deploy.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jobs:
2727
cmake -S . -B ./build
2828
-DCMAKE_BUILD_TYPE:STRING=Release
2929
-DSOURCEMETA_CORE_LANG_IO:BOOL=OFF
30+
-DSOURCEMETA_CORE_LANG_PROCESS:BOOL=OFF
3031
-DSOURCEMETA_CORE_TIME:BOOL=OFF
3132
-DSOURCEMETA_CORE_UUID:BOOL=OFF
3233
-DSOURCEMETA_CORE_REGEX:BOOL=OFF

CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
44

55
# Options
66
option(SOURCEMETA_CORE_LANG_IO "Build the Sourcemeta Core language I/O library" ON)
7+
option(SOURCEMETA_CORE_LANG_PROCESS "Build the Sourcemeta Core language Process library" ON)
78
option(SOURCEMETA_CORE_LANG_PARALLEL "Build the Sourcemeta Core language parallel library" ON)
89
option(SOURCEMETA_CORE_TIME "Build the Sourcemeta Core time library" ON)
910
option(SOURCEMETA_CORE_UUID "Build the Sourcemeta Core UUID library" ON)
@@ -58,6 +59,10 @@ if(SOURCEMETA_CORE_LANG_IO)
5859
add_subdirectory(src/lang/io)
5960
endif()
6061

62+
if(SOURCEMETA_CORE_LANG_PROCESS)
63+
add_subdirectory(src/lang/process)
64+
endif()
65+
6166
if(SOURCEMETA_CORE_LANG_PARALLEL)
6267
find_package(Threads REQUIRED)
6368
add_subdirectory(src/lang/parallel)
@@ -160,6 +165,10 @@ if(SOURCEMETA_CORE_TESTS)
160165
add_subdirectory(test/io)
161166
endif()
162167

168+
if(SOURCEMETA_CORE_LANG_PROCESS)
169+
add_subdirectory(test/process)
170+
endif()
171+
163172
if(SOURCEMETA_CORE_LANG_PARALLEL)
164173
add_subdirectory(test/parallel)
165174
endif()

config.cmake.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ list(APPEND SOURCEMETA_CORE_COMPONENTS ${Core_FIND_COMPONENTS})
55
list(APPEND SOURCEMETA_CORE_COMPONENTS ${core_FIND_COMPONENTS})
66
if(NOT SOURCEMETA_CORE_COMPONENTS)
77
list(APPEND SOURCEMETA_CORE_COMPONENTS io)
8+
list(APPEND SOURCEMETA_CORE_COMPONENTS process)
89
list(APPEND SOURCEMETA_CORE_COMPONENTS parallel)
910
list(APPEND SOURCEMETA_CORE_COMPONENTS time)
1011
list(APPEND SOURCEMETA_CORE_COMPONENTS uuid)
@@ -28,6 +29,8 @@ include(CMakeFindDependencyMacro)
2829
foreach(component ${SOURCEMETA_CORE_COMPONENTS})
2930
if(component STREQUAL "io")
3031
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake")
32+
elseif(component STREQUAL "process")
33+
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_process.cmake")
3134
elseif(component STREQUAL "parallel")
3235
find_dependency(Threads)
3336
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_parallel.cmake")

src/lang/process/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME process
2+
PRIVATE_HEADERS error.h
3+
SOURCES spawn.cc)
4+
5+
if(SOURCEMETA_CORE_INSTALL)
6+
sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME process)
7+
endif()
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#ifndef SOURCEMETA_CORE_PROCESS_H_
2+
#define SOURCEMETA_CORE_PROCESS_H_
3+
4+
#ifndef SOURCEMETA_CORE_PROCESS_EXPORT
5+
#include <sourcemeta/core/process_export.h>
6+
#endif
7+
8+
// NOLINTBEGIN(misc-include-cleaner)
9+
#include <sourcemeta/core/process_error.h>
10+
// NOLINTEND(misc-include-cleaner)
11+
12+
#include <filesystem> // std::filesystem
13+
#include <initializer_list> // std::initializer_list
14+
#include <span> // std::span
15+
#include <string_view> // std::string_view
16+
17+
/// @defgroup process Process
18+
/// @brief Process related utilities
19+
///
20+
/// This functionality is included as follows:
21+
///
22+
/// ```cpp
23+
/// #include <sourcemeta/core/process.h>
24+
/// ```
25+
26+
namespace sourcemeta::core {
27+
28+
/// @ingroup process
29+
///
30+
/// Spawn a program piping its output to the current stdio configuration.
31+
/// The directory parameter specifies the working directory for the spawned
32+
/// process. It must be an absolute path to an existing directory.
33+
///
34+
/// ```cpp
35+
/// #include <sourcemeta/core/process.h>
36+
/// #include <cassert>
37+
///
38+
/// const auto exit_code{sourcemeta::core::spawn("echo", {"foo"})};
39+
/// assert(exit_code == 0);
40+
/// ```
41+
SOURCEMETA_CORE_PROCESS_EXPORT
42+
auto spawn(const std::string &program,
43+
std::initializer_list<std::string_view> arguments,
44+
const std::filesystem::path &directory =
45+
std::filesystem::current_path()) -> int;
46+
47+
/// @ingroup process
48+
///
49+
/// Spawn a program piping its output to the current stdio configuration.
50+
/// This overload accepts a span for dynamic argument lists.
51+
/// The directory parameter specifies the working directory for the spawned
52+
/// process. It must be an absolute path to an existing directory.
53+
///
54+
/// ```cpp
55+
/// #include <sourcemeta/core/process.h>
56+
/// #include <vector>
57+
/// #include <string_view>
58+
/// #include <cassert>
59+
///
60+
/// std::vector<std::string_view> arguments{"foo", "bar"};
61+
/// const auto exit_code{sourcemeta::core::spawn("echo", arguments)};
62+
/// assert(exit_code == 0);
63+
/// ```
64+
SOURCEMETA_CORE_PROCESS_EXPORT
65+
auto spawn(
66+
const std::string &program, std::span<const std::string_view> arguments,
67+
const std::filesystem::path &directory = std::filesystem::current_path())
68+
-> int;
69+
70+
} // namespace sourcemeta::core
71+
72+
#endif
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#ifndef SOURCEMETA_CORE_PROCESS_ERROR_H_
2+
#define SOURCEMETA_CORE_PROCESS_ERROR_H_
3+
4+
#ifndef SOURCEMETA_CORE_PROCESS_EXPORT
5+
#include <sourcemeta/core/process_export.h>
6+
#endif
7+
8+
#include <exception> // std::exception
9+
#include <initializer_list> // std::initializer_list
10+
#include <span> // std::span
11+
#include <string> // std::string
12+
#include <string_view> // std::string_view
13+
#include <utility> // std::move
14+
#include <vector> // std::vector
15+
16+
namespace sourcemeta::core {
17+
18+
// Exporting symbols that depends on the standard C++ library is considered
19+
// safe.
20+
// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN
21+
#if defined(_MSC_VER)
22+
#pragma warning(disable : 4251 4275)
23+
#endif
24+
25+
/// @ingroup process
26+
/// An executable program could not be found
27+
class SOURCEMETA_CORE_PROCESS_EXPORT ProcessProgramNotNotFoundError
28+
: public std::exception {
29+
public:
30+
ProcessProgramNotNotFoundError(std::string program)
31+
: program_{std::move(program)} {}
32+
33+
[[nodiscard]] auto what() const noexcept -> const char * override {
34+
return "Could not locate the requested program";
35+
}
36+
37+
[[nodiscard]] auto program() const noexcept -> std::string_view {
38+
return this->program_;
39+
}
40+
41+
private:
42+
std::string program_;
43+
};
44+
45+
/// @ingroup process
46+
/// A spawned process terminated abnormally
47+
class SOURCEMETA_CORE_PROCESS_EXPORT ProcessSpawnError : public std::exception {
48+
public:
49+
ProcessSpawnError(std::string program,
50+
std::initializer_list<std::string_view> arguments)
51+
: program_{std::move(program)},
52+
arguments_{arguments.begin(), arguments.end()} {}
53+
54+
ProcessSpawnError(std::string program,
55+
std::span<const std::string_view> arguments)
56+
: program_{std::move(program)},
57+
arguments_{arguments.begin(), arguments.end()} {}
58+
59+
[[nodiscard]] auto what() const noexcept -> const char * override {
60+
return "Process terminated abnormally";
61+
}
62+
63+
[[nodiscard]] auto program() const noexcept -> std::string_view {
64+
return this->program_;
65+
}
66+
67+
[[nodiscard]] auto arguments() const noexcept
68+
-> const std::vector<std::string> & {
69+
return this->arguments_;
70+
}
71+
72+
private:
73+
std::string program_;
74+
std::vector<std::string> arguments_;
75+
};
76+
77+
#if defined(_MSC_VER)
78+
#pragma warning(default : 4251 4275)
79+
#endif
80+
81+
} // namespace sourcemeta::core
82+
83+
#endif

src/lang/process/spawn.cc

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#include <sourcemeta/core/process.h>
2+
3+
#include <cassert> // assert
4+
#include <filesystem> // std::filesystem
5+
#include <initializer_list> // std::initializer_list
6+
#include <span> // std::span
7+
#include <vector> // std::vector
8+
9+
#if defined(_WIN32) && !defined(__MSYS__) && !defined(__CYGWIN__) && \
10+
!defined(__MINGW32__) && !defined(__MINGW64__)
11+
#define WIN32_LEAN_AND_MEAN
12+
#include <sstream> // std::ostringstream
13+
#include <windows.h> // CreateProcess, PROCESS_INFORMATION, STARTUPINFO, WaitForSingleObject, GetExitCodeProcess
14+
#else
15+
#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
16+
#include <sys/wait.h> // waitpid, WIFEXITED, WEXITSTATUS
17+
18+
#if defined(__MSYS__) || defined(__CYGWIN__) || defined(__MINGW32__) || \
19+
defined(__MINGW64__)
20+
#include <unistd.h> // chdir
21+
#endif
22+
23+
extern char **environ;
24+
#endif
25+
26+
namespace sourcemeta::core {
27+
28+
auto spawn(const std::string &program,
29+
std::span<const std::string_view> arguments,
30+
const std::filesystem::path &directory) -> int {
31+
assert(directory.is_absolute());
32+
assert(std::filesystem::exists(directory));
33+
assert(std::filesystem::is_directory(directory));
34+
35+
#if defined(_WIN32) && !defined(__MSYS__) && !defined(__CYGWIN__) && \
36+
!defined(__MINGW32__) && !defined(__MINGW64__)
37+
std::ostringstream command_line;
38+
command_line << program;
39+
40+
for (const auto &argument : arguments) {
41+
command_line << " ";
42+
// Quote arguments that contain spaces
43+
const std::string arg_str{argument};
44+
if (arg_str.find(' ') != std::string::npos) {
45+
command_line << "\"" << arg_str << "\"";
46+
} else {
47+
command_line << arg_str;
48+
}
49+
}
50+
51+
std::string cmd_line_str = command_line.str();
52+
std::vector<char> cmd_line(cmd_line_str.begin(), cmd_line_str.end());
53+
cmd_line.push_back('\0');
54+
55+
STARTUPINFOA startup_info{};
56+
startup_info.cb = sizeof(startup_info);
57+
PROCESS_INFORMATION process_info{};
58+
const std::string working_dir = directory.string();
59+
const BOOL success =
60+
CreateProcessA(nullptr, // lpApplicationName
61+
cmd_line.data(), // lpCommandLine (modifiable)
62+
nullptr, // lpProcessAttributes
63+
nullptr, // lpThreadAttributes
64+
TRUE, // bInheritHandles
65+
0, // dwCreationFlags
66+
nullptr, // lpEnvironment
67+
working_dir.c_str(), // lpCurrentDirectory
68+
&startup_info, // lpStartupInfo
69+
&process_info // lpProcessInformation
70+
);
71+
72+
if (!success) {
73+
throw ProcessProgramNotNotFoundError{program};
74+
}
75+
76+
WaitForSingleObject(process_info.hProcess, INFINITE);
77+
78+
DWORD exit_code;
79+
if (!GetExitCodeProcess(process_info.hProcess, &exit_code)) {
80+
CloseHandle(process_info.hProcess);
81+
CloseHandle(process_info.hThread);
82+
throw ProcessSpawnError{program, arguments};
83+
}
84+
85+
CloseHandle(process_info.hProcess);
86+
CloseHandle(process_info.hThread);
87+
88+
return static_cast<int>(exit_code);
89+
#else
90+
std::vector<const char *> argv;
91+
argv.reserve(arguments.size() + 2);
92+
argv.push_back(program.c_str());
93+
94+
for (const auto &argument : arguments) {
95+
argv.push_back(argument.data());
96+
}
97+
98+
argv.push_back(nullptr);
99+
100+
posix_spawnattr_t attributes;
101+
posix_spawnattr_init(&attributes);
102+
103+
posix_spawn_file_actions_t file_actions;
104+
posix_spawn_file_actions_init(&file_actions);
105+
106+
#if defined(__MSYS__) || defined(__CYGWIN__) || defined(__MINGW32__) || \
107+
defined(__MINGW64__)
108+
const std::filesystem::path original_directory{
109+
std::filesystem::current_path()};
110+
std::filesystem::current_path(directory);
111+
#else
112+
posix_spawn_file_actions_addchdir_np(&file_actions, directory.c_str());
113+
#endif
114+
115+
pid_t process_id;
116+
const int spawn_result{
117+
posix_spawnp(&process_id, program.c_str(), &file_actions, &attributes,
118+
const_cast<char *const *>(argv.data()), environ)};
119+
120+
posix_spawn_file_actions_destroy(&file_actions);
121+
posix_spawnattr_destroy(&attributes);
122+
123+
#if defined(__MSYS__) || defined(__CYGWIN__) || defined(__MINGW32__) || \
124+
defined(__MINGW64__)
125+
std::filesystem::current_path(original_directory);
126+
#endif
127+
128+
if (spawn_result != 0) {
129+
throw ProcessProgramNotNotFoundError{program};
130+
}
131+
132+
int status;
133+
waitpid(process_id, &status, 0);
134+
135+
if (WIFEXITED(status)) {
136+
return WEXITSTATUS(status);
137+
}
138+
139+
throw ProcessSpawnError{program, arguments};
140+
#endif
141+
}
142+
143+
auto spawn(const std::string &program,
144+
std::initializer_list<std::string_view> arguments,
145+
const std::filesystem::path &directory) -> int {
146+
return spawn(
147+
program,
148+
std::span<const std::string_view>{arguments.begin(), arguments.size()},
149+
directory);
150+
}
151+
152+
} // namespace sourcemeta::core

test/parallel/parallel_for_each_test.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#include <gtest/gtest.h>
22

3-
#include "sourcemeta/core/parallel_for_each.h"
3+
#include <sourcemeta/core/parallel.h>
44

55
#include <algorithm>
66
#include <atomic>

0 commit comments

Comments
 (0)