Skip to content

Commit e811760

Browse files
authored
Handle spaces in start_process arguments on Windows (ros2#494)
Signed-off-by: Scott K Logan <logans@cottsay.net>
1 parent 2f3252e commit e811760

3 files changed

Lines changed: 172 additions & 7 deletions

File tree

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,9 @@ if(BUILD_TESTING)
523523
)
524524
if(TARGET test_process)
525525
target_link_libraries(test_process ${PROJECT_NAME})
526+
target_compile_definitions(test_process PRIVATE
527+
"CMAKE_COMMAND=${CMAKE_COMMAND}")
528+
file(TOUCH "${CMAKE_CURRENT_BINARY_DIR}/file with space.txt")
526529
endif()
527530

528531
ament_add_gtest(test_logging_custom_env test/test_logging_custom_env.cpp

src/process.c

Lines changed: 128 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,115 @@ char * rcutils_get_executable_name(rcutils_allocator_t allocator)
113113
return executable_name;
114114
}
115115

116+
#if defined _WIN32 || defined __CYGWIN__
117+
static
118+
rcutils_ret_t
119+
append_backslashes(rcutils_char_array_t * char_array, size_t num_backslashes)
120+
{
121+
if (num_backslashes <= 0) {
122+
return RCUTILS_RET_OK;
123+
}
124+
125+
size_t current_strlen;
126+
if (char_array->buffer_length == 0) {
127+
current_strlen = 0;
128+
} else {
129+
// The buffer length always contains the trailing \0, so the strlen is one less than that.
130+
current_strlen = char_array->buffer_length - 1;
131+
}
132+
133+
size_t new_length = current_strlen + num_backslashes + 1;
134+
rcutils_ret_t ret = rcutils_char_array_expand_as_needed(char_array, new_length);
135+
if (RCUTILS_RET_OK != ret) {
136+
return ret;
137+
}
138+
139+
memset(char_array->buffer + current_strlen, '\\', num_backslashes);
140+
char_array->buffer[new_length - 1] = '\0';
141+
142+
char_array->buffer_length = new_length;
143+
144+
return RCUTILS_RET_OK;
145+
}
146+
147+
/**
148+
* This algorithm is based on the MSDN blog "everyone quotes command line arguments the wrong way".
149+
* See https://learn.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
150+
*/
151+
static
152+
rcutils_ret_t
153+
build_command_line(
154+
const rcutils_string_array_t * string_array,
155+
rcutils_char_array_t * char_array)
156+
{
157+
rcutils_ret_t ret;
158+
159+
for (size_t i = 0; i < string_array->size; i++) {
160+
// Append argument separator
161+
if (i != 0) {
162+
ret = rcutils_char_array_strncat(char_array, " ", 1);
163+
if (RCUTILS_RET_OK != ret) {
164+
return ret;
165+
}
166+
}
167+
168+
// Append argument which doesn't need to be quoted
169+
size_t arg_length = strlen(string_array->data[i]);
170+
if (strcspn(string_array->data[i], " \t\n\v\"") >= arg_length) {
171+
ret = rcutils_char_array_strncat(char_array, string_array->data[i], arg_length);
172+
if (RCUTILS_RET_OK != ret) {
173+
return ret;
174+
}
175+
continue;
176+
}
177+
178+
// Append opening quote
179+
ret = rcutils_char_array_strncat(char_array, "\"", 1);
180+
if (RCUTILS_RET_OK != ret) {
181+
return ret;
182+
}
183+
184+
for (const char * c = string_array->data[i]; ; c++) {
185+
size_t backslash_count = 0;
186+
while ('\\' == *c) {
187+
backslash_count++;
188+
c++;
189+
}
190+
191+
if ('\0' == *c) {
192+
ret = append_backslashes(char_array, backslash_count * 2);
193+
if (RCUTILS_RET_OK != ret) {
194+
return ret;
195+
}
196+
break;
197+
}
198+
199+
if ('"' == *c) {
200+
backslash_count = backslash_count * 2 + 1;
201+
}
202+
203+
ret = append_backslashes(char_array, backslash_count);
204+
if (RCUTILS_RET_OK != ret) {
205+
return ret;
206+
}
207+
208+
ret = rcutils_char_array_strncat(char_array, c, 1);
209+
if (RCUTILS_RET_OK != ret) {
210+
return ret;
211+
}
212+
}
213+
214+
// Append closing quote
215+
ret = rcutils_char_array_strncat(char_array, "\"", 1);
216+
if (RCUTILS_RET_OK != ret) {
217+
return ret;
218+
}
219+
}
220+
221+
return RCUTILS_RET_OK;
222+
}
223+
#endif
224+
116225
rcutils_process_t *
117226
rcutils_start_process(
118227
const rcutils_string_array_t * args,
@@ -135,25 +244,39 @@ rcutils_start_process(
135244
process->allocator = *allocator;
136245

137246
#if defined _WIN32 || defined __CYGWIN__
138-
LPSTR cmd = rcutils_join(args, " ", *allocator);
139-
if (NULL == cmd) {
247+
rcutils_char_array_t cmd = rcutils_get_zero_initialized_char_array();
248+
rcutils_ret_t ret = rcutils_char_array_init(&cmd, 0, allocator);
249+
if (RCUTILS_RET_OK != ret) {
250+
rcutils_process_close(process);
251+
return NULL;
252+
}
253+
254+
ret = build_command_line(args, &cmd);
255+
if (RCUTILS_RET_OK != ret) {
256+
if (RCUTILS_RET_OK != rcutils_char_array_fini(&cmd)) {
257+
RCUTILS_SAFE_FWRITE_TO_STDERR("Failed to fini array.\n");
258+
}
140259
rcutils_process_close(process);
141260
return NULL;
142261
}
143262

144263
STARTUPINFO start_info = {sizeof(start_info)};
145264
PROCESS_INFORMATION process_info = {0};
146265
if (!CreateProcess(
147-
NULL, cmd, NULL, NULL, TRUE, 0,
266+
NULL, cmd.buffer, NULL, NULL, TRUE, 0,
148267
NULL, NULL, &start_info, &process_info))
149268
{
150269
RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING("Failed to create process: %lu", GetLastError());
151-
allocator->deallocate(cmd, &allocator->state);
270+
if (RCUTILS_RET_OK != rcutils_char_array_fini(&cmd)) {
271+
RCUTILS_SAFE_FWRITE_TO_STDERR("Failed to fini array.\n");
272+
}
152273
rcutils_process_close(process);
153274
return NULL;
154275
}
155276

156-
allocator->deallocate(cmd, &allocator->state);
277+
if (RCUTILS_RET_OK != rcutils_char_array_fini(&cmd)) {
278+
RCUTILS_SAFE_FWRITE_TO_STDERR("Failed to fini array.\n");
279+
}
157280

158281
CloseHandle(process_info.hThread);
159282

test/test_process.cpp

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
#include "rcutils/error_handling.h"
2121
#include "rcutils/process.h"
2222

23+
static const char * const g_cmake_command = RCUTILS_STRINGIFY(CMAKE_COMMAND);
24+
2325
TEST(TestProcess, test_get_pid) {
2426
EXPECT_NE(rcutils_get_pid(), 0);
2527
}
@@ -52,9 +54,12 @@ TEST(TestProcess, test_process_creation) {
5254
rcutils_ret_t ret = RCUTILS_RET_OK;
5355
int exit_code = 42;
5456

55-
ret = rcutils_string_array_init(&args, 1, &allocator);
57+
ret = rcutils_string_array_init(&args, 4, &allocator);
5658
ASSERT_EQ(RCUTILS_RET_OK, ret);
57-
args.data[0] = strdup("whoami");
59+
args.data[0] = strdup(g_cmake_command);
60+
args.data[1] = strdup("-E");
61+
args.data[2] = strdup("echo");
62+
args.data[3] = strdup("");
5863

5964
EXPECT_EQ(nullptr, rcutils_start_process(NULL, &allocator));
6065
rcutils_reset_error();
@@ -78,6 +83,40 @@ TEST(TestProcess, test_process_creation) {
7883

7984
rcutils_process_close(process);
8085

86+
// cmake -E cat "file with space.txt" (returns 0)
87+
ret = rcutils_string_array_resize(&args, 4);
88+
ASSERT_EQ(RCUTILS_RET_OK, ret);
89+
args.data[0] = strdup(g_cmake_command);
90+
args.data[1] = strdup("-E");
91+
args.data[2] = strdup("cat");
92+
args.data[3] = strdup("file with space.txt");
93+
94+
process = rcutils_start_process(&args, &allocator);
95+
EXPECT_NE(nullptr, process);
96+
97+
ret = rcutils_process_wait(process, &exit_code);
98+
EXPECT_EQ(RCUTILS_RET_OK, ret);
99+
EXPECT_EQ(0, exit_code);
100+
101+
rcutils_process_close(process);
102+
103+
// cmake -E false (returns 1)
104+
ret = rcutils_string_array_resize(&args, 3);
105+
ASSERT_EQ(RCUTILS_RET_OK, ret);
106+
args.data[0] = strdup(g_cmake_command);
107+
args.data[1] = strdup("-E");
108+
allocator.deallocate(args.data[2], &allocator.state);
109+
args.data[2] = strdup("false");
110+
111+
process = rcutils_start_process(&args, &allocator);
112+
EXPECT_NE(nullptr, process);
113+
114+
ret = rcutils_process_wait(process, &exit_code);
115+
EXPECT_EQ(RCUTILS_RET_OK, ret);
116+
EXPECT_EQ(1, exit_code);
117+
118+
rcutils_process_close(process);
119+
81120
ret = rcutils_string_array_fini(&args);
82121
ASSERT_EQ(RCUTILS_RET_OK, ret);
83122
}

0 commit comments

Comments
 (0)