Skip to content

Commit 937efe2

Browse files
authored
Add utility functions for invoking a subprocess (ros2#491)
This new function and associated functions provide a cross-platform mechanism for invoking a command as a subprocess of the currently running process. Signed-off-by: Scott K Logan <logans@cottsay.net>
1 parent 4841a34 commit 937efe2

3 files changed

Lines changed: 257 additions & 0 deletions

File tree

include/rcutils/process.h

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,24 @@ extern "C"
2222
{
2323
#endif
2424

25+
#if defined _WIN32 || defined __CYGWIN__
26+
// When building with MSVC 19.28.29333.0 on Windows 10 (as of 2020-11-11),
27+
// there appears to be a problem with winbase.h (which is included by
28+
// Windows.h). In particular, warnings of the form:
29+
//
30+
// warning C5105: macro expansion producing 'defined' has undefined behavior
31+
//
32+
// See https://developercommunity.visualstudio.com/content/problem/695656/wdk-and-sdk-are-not-compatible-with-experimentalpr.html
33+
// for more information. For now disable that warning when including windows.h
34+
#pragma warning(push)
35+
#pragma warning(disable : 5105)
36+
#include <Windows.h>
37+
#pragma warning(pop)
38+
#endif
39+
2540
#include "rcutils/allocator.h"
2641
#include "rcutils/macros.h"
42+
#include "rcutils/types/string_array.h"
2743
#include "rcutils/visibility_control.h"
2844

2945
/// Retrieve the current process ID.
@@ -54,6 +70,65 @@ RCUTILS_PUBLIC
5470
RCUTILS_WARN_UNUSED
5571
char * rcutils_get_executable_name(rcutils_allocator_t allocator);
5672

73+
/// Information about a subprocess created by this process
74+
typedef struct rcutils_process_s
75+
{
76+
#if defined _WIN32 || defined __CYGWIN__
77+
/// The open handle to the process.
78+
HANDLE handle;
79+
#endif
80+
81+
/// The process ID of the process.
82+
int pid;
83+
84+
/// The allocator used to allocate and free memory for the process.
85+
rcutils_allocator_t allocator;
86+
} rcutils_process_t;
87+
88+
/// Execute a command as a new subprocess.
89+
/**
90+
* This function runs a command by creating a new subprocess of the currently
91+
* running process.
92+
*
93+
* \param[in] args the command line arguments to be run
94+
* \param[in] allocator the allocator to use
95+
* \return The successfully created subprocess, or
96+
* \return NULL on failure.
97+
*/
98+
RCUTILS_PUBLIC
99+
RCUTILS_WARN_UNUSED
100+
rcutils_process_t *
101+
rcutils_start_process(
102+
const rcutils_string_array_t * args,
103+
rcutils_allocator_t * allocator);
104+
105+
/// Release resources allocated when a subprocess was created.
106+
/**
107+
* Closes, cleans up, and deallocates resources which were allocated by
108+
* rcutils_start_process(), including the rcutils_process_t itself.
109+
* \param[in] process the process to be closed and deallocated
110+
*/
111+
RCUTILS_PUBLIC
112+
void rcutils_process_close(rcutils_process_t * process);
113+
114+
/// Blocks until the given subprocess has been has exited.
115+
/**
116+
* Wait for a subprocess to terminate, and optionally retrieve the exit code
117+
* from that process.
118+
* Upon successful invocation of this function, subsequent calls will produce
119+
* undefined behavior. It should typically be followed by a call to
120+
* rcutils_process_close().
121+
* \param[in] process The process to wait for
122+
* \param[out] status The exit code from that process
123+
* \return #RCUTILS_RET_OK if the process exited, or
124+
* \return #RCUTILS_RET_INVALID_ARGUMENT if the process argument is invalid
125+
* \return #RCUTILS_RET_ERROR if an unexpected error occurs.
126+
*/
127+
RCUTILS_PUBLIC
128+
RCUTILS_WARN_UNUSED
129+
rcutils_ret_t
130+
rcutils_process_wait(const rcutils_process_t * process, int * status);
131+
57132
#ifdef __cplusplus
58133
}
59134
#endif

src/process.c

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ extern "C"
3636
#pragma warning(pop)
3737
#else
3838
#include <libgen.h>
39+
#include <sys/wait.h>
3940
#include <unistd.h>
4041
#endif
4142

4243
#include "rcutils/allocator.h"
4344
#include "rcutils/error_handling.h"
45+
#include "rcutils/join.h"
4446
#include "rcutils/process.h"
4547
#include "rcutils/strdup.h"
4648

@@ -111,6 +113,147 @@ char * rcutils_get_executable_name(rcutils_allocator_t allocator)
111113
return executable_name;
112114
}
113115

116+
rcutils_process_t *
117+
rcutils_start_process(
118+
const rcutils_string_array_t * args,
119+
rcutils_allocator_t * allocator)
120+
{
121+
RCUTILS_CHECK_ARGUMENT_FOR_NULL(args, NULL);
122+
RCUTILS_CHECK_ARGUMENT_FOR_NULL(allocator, NULL);
123+
if (args->size < 1) {
124+
RCUTILS_SET_ERROR_MSG("args list is empty");
125+
return NULL;
126+
}
127+
RCUTILS_CHECK_ALLOCATOR_WITH_MSG(
128+
allocator, "allocator is invalid", return NULL);
129+
130+
rcutils_process_t * process = allocator->zero_allocate(
131+
1, sizeof(rcutils_process_t), allocator->state);
132+
if (NULL == process) {
133+
return NULL;
134+
}
135+
process->allocator = *allocator;
136+
137+
#if defined _WIN32 || defined __CYGWIN__
138+
LPSTR cmd = rcutils_join(args, " ", *allocator);
139+
if (NULL == cmd) {
140+
rcutils_process_close(process);
141+
return NULL;
142+
}
143+
144+
STARTUPINFO start_info = {sizeof(start_info)};
145+
PROCESS_INFORMATION process_info = {0};
146+
if (!CreateProcess(
147+
NULL, cmd, NULL, NULL, TRUE, 0,
148+
NULL, NULL, &start_info, &process_info))
149+
{
150+
RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING("Failed to create process: %lu", GetLastError());
151+
allocator->deallocate(cmd, &allocator->state);
152+
rcutils_process_close(process);
153+
return NULL;
154+
}
155+
156+
allocator->deallocate(cmd, &allocator->state);
157+
158+
CloseHandle(process_info.hThread);
159+
160+
process->handle = process_info.hProcess;
161+
process->pid = process_info.dwProcessId;
162+
163+
return process;
164+
#else
165+
char **argv = allocator->zero_allocate(args->size + 1, sizeof(*argv), &allocator->state);
166+
if (NULL == argv) {
167+
return NULL;
168+
}
169+
memcpy(argv, args->data, args->size * sizeof(*argv));
170+
171+
process->pid = fork();
172+
if (-1 == process->pid) {
173+
int error = errno;
174+
RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING(
175+
"Failed to fork process: %d (%s)", error, strerror(error));
176+
allocator->deallocate(argv, &allocator->state);
177+
rcutils_process_close(process);
178+
return NULL;
179+
} else if (0 != process->pid) {
180+
allocator->deallocate(argv, &allocator->state);
181+
return process;
182+
}
183+
184+
(void)execvp(argv[0], argv);
185+
186+
int error = errno;
187+
RCUTILS_SAFE_FWRITE_TO_STDERR_WITH_FORMAT_STRING(
188+
"Failed to execute process: %d (%s)", error, strerror(error));
189+
allocator->deallocate(argv, &allocator->state);
190+
exit(127);
191+
#endif
192+
}
193+
194+
void
195+
rcutils_process_close(rcutils_process_t * process)
196+
{
197+
if (NULL == process) {
198+
return;
199+
}
200+
201+
rcutils_allocator_t allocator = process->allocator;
202+
RCUTILS_CHECK_ALLOCATOR_WITH_MSG(
203+
&allocator, "allocator is invalid", return );
204+
205+
#if defined _WIN32 || defined __CYGWIN__
206+
CloseHandle(process->handle);
207+
#endif
208+
209+
allocator.deallocate(process, allocator.state);
210+
}
211+
212+
rcutils_ret_t
213+
rcutils_process_wait(const rcutils_process_t * process, int * exit_code)
214+
{
215+
RCUTILS_CHECK_ARGUMENT_FOR_NULL(process, RCUTILS_RET_INVALID_ARGUMENT);
216+
217+
#if defined _WIN32 || defined __CYGWIN__
218+
DWORD status;
219+
220+
if (WAIT_FAILED == WaitForSingleObject(process->handle, INFINITE)) {
221+
return RCUTILS_RET_ERROR;
222+
}
223+
224+
if (NULL != exit_code) {
225+
if (!GetExitCodeProcess(process->handle, &status)) {
226+
return RCUTILS_RET_ERROR;
227+
}
228+
*exit_code = status;
229+
}
230+
#else
231+
int status;
232+
233+
int ret = waitpid(process->pid, &status, 0);
234+
if (-1 == ret) {
235+
int error = errno;
236+
RCUTILS_SAFE_FWRITE_TO_STDERR_WITH_FORMAT_STRING(
237+
"Failed to wait for process %d: %d (%s)", process->pid, error, strerror(error));
238+
return RCUTILS_RET_ERROR;
239+
}
240+
241+
if (NULL != exit_code) {
242+
if (WIFSIGNALED(status)) {
243+
*exit_code = -WTERMSIG(status);
244+
} else if (WIFEXITED(status)) {
245+
*exit_code = WEXITSTATUS(status);
246+
} else if (WIFSTOPPED(status)) {
247+
*exit_code = -WSTOPSIG(status);
248+
} else {
249+
return RCUTILS_RET_ERROR;
250+
}
251+
}
252+
#endif
253+
254+
return RCUTILS_RET_OK;
255+
}
256+
114257
#ifdef __cplusplus
115258
}
116259
#endif

test/test_process.cpp

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,42 @@ TEST(TestProcess, test_get_executable_name) {
4242
EXPECT_STREQ("test_process", exec_name);
4343
allocator.deallocate(exec_name, allocator.state);
4444
}
45+
46+
TEST(TestProcess, test_process_creation) {
47+
rcutils_allocator_t allocator = rcutils_get_default_allocator();
48+
rcutils_allocator_t bad_allocator = rcutils_get_zero_initialized_allocator();
49+
rcutils_allocator_t time_bomb_allocator = get_time_bomb_allocator();
50+
rcutils_string_array_t args = rcutils_get_zero_initialized_string_array();
51+
rcutils_process_t * process = NULL;
52+
rcutils_ret_t ret = RCUTILS_RET_OK;
53+
int exit_code = 42;
54+
55+
ret = rcutils_string_array_init(&args, 1, &allocator);
56+
ASSERT_EQ(RCUTILS_RET_OK, ret);
57+
args.data[0] = strdup("whoami");
58+
59+
EXPECT_EQ(nullptr, rcutils_start_process(NULL, &allocator));
60+
rcutils_reset_error();
61+
62+
EXPECT_EQ(nullptr, rcutils_start_process(&args, NULL));
63+
rcutils_reset_error();
64+
65+
EXPECT_EQ(nullptr, rcutils_start_process(&args, &bad_allocator));
66+
rcutils_reset_error();
67+
68+
set_time_bomb_allocator_calloc_count(time_bomb_allocator, 0);
69+
set_time_bomb_allocator_malloc_count(time_bomb_allocator, 0);
70+
EXPECT_EQ(nullptr, rcutils_start_process(&args, &time_bomb_allocator));
71+
72+
process = rcutils_start_process(&args, &allocator);
73+
EXPECT_NE(nullptr, process);
74+
75+
ret = rcutils_process_wait(process, &exit_code);
76+
EXPECT_EQ(RCUTILS_RET_OK, ret);
77+
EXPECT_EQ(0, exit_code);
78+
79+
rcutils_process_close(process);
80+
81+
ret = rcutils_string_array_fini(&args);
82+
ASSERT_EQ(RCUTILS_RET_OK, ret);
83+
}

0 commit comments

Comments
 (0)