Commit bc97d01c by Edward Thomson

Introduce git_process class that invokes processes

parent 0181b4cb
/*
* Copyright (C) the libgit2 contributors. All rights reserved.
*
* This file is part of libgit2, distributed under the GNU GPL v2 with
* a Linking Exception. For full terms see the included COPYING file.
*/
#ifndef INCLUDE_process_h__
#define INCLUDE_process_h__
typedef struct git_process git_process;
typedef struct {
int capture_in : 1,
capture_out : 1,
capture_err : 1,
exclude_env : 1;
char *cwd;
} git_process_options;
typedef enum {
GIT_PROCESS_STATUS_NONE,
GIT_PROCESS_STATUS_NORMAL,
GIT_PROCESS_STATUS_ERROR
} git_process_result_status;
#define GIT_PROCESS_RESULT_INIT { GIT_PROCESS_STATUS_NONE }
typedef struct {
git_process_result_status status;
int exitcode;
int signal;
} git_process_result;
#define GIT_PROCESS_OPTIONS_INIT { 0 }
/**
* Create a new process. The command to run should be specified as the
* element of the `arg` array.
*
* This function will add the given environment variables (in `env`)
* to the current environment. Operations on environment variables
* are not thread safe, so you may not modify the environment during
* this call. You can avoid this by setting `exclude_env` in the
* options and providing the entire environment yourself.
*
* @param out location to store the process
* @param args the command (with arguments) to run
* @param args_len the length of the args array
* @param env environment variables to add (or NULL)
* @param env_len the length of the env len
* @param opts the options for creating the process
* @return 0 or an error code
*/
extern int git_process_new(
git_process **out,
const char **args,
size_t args_len,
const char **env,
size_t env_len,
git_process_options *opts);
#ifdef GIT_WIN32
/* Windows path parsing is tricky; this helper function is for testing. */
extern int git_process__cmdline(
git_str *out,
const char **in,
size_t in_len);
#endif
/**
* Start the process.
*
* @param process the process to start
* @return 0 or an error code
*/
extern int git_process_start(git_process *process);
/**
* Read from the process's stdout. The process must have been created with
* `capture_out` set to true.
*
* @param process the process to read from
* @param buf the buf to read into
* @param count maximum number of bytes to read
* @return number of bytes read or an error code
*/
extern ssize_t git_process_read(git_process *process, void *buf, size_t count);
/**
* Write to the process's stdin. The process must have been created with
* `capture_in` set to true.
*
* @param process the process to write to
* @param buf the buf to write
* @param count maximum number of bytes to write
* @return number of bytes written or an error code
*/
extern ssize_t git_process_write(git_process *process, const void *buf, size_t count);
/**
* Wait for the process to finish.
*
* @param result the result of the process or NULL
* @param process the process to wait on
*/
extern int git_process_wait(git_process_result *result, git_process *process);
/**
* Close the input pipe from the child.
*
* @param process the process to close the pipe on
*/
extern int git_process_close_in(git_process *process);
/**
* Close the output pipe from the child.
*
* @param process the process to close the pipe on
*/
extern int git_process_close_out(git_process *process);
/**
* Close the error pipe from the child.
*
* @param process the process to close the pipe on
*/
extern int git_process_close_err(git_process *process);
/**
* Close all resources that are used by the process. This does not
* wait for the process to complete.
*
* @parma process the process to close
*/
extern int git_process_close(git_process *process);
/**
* Place a human-readable error message in the given git buffer.
*
* @param msg the buffer to store the message
* @param result the process result that produced an error
*/
extern int git_process_result_msg(git_str *msg, git_process_result *result);
/**
* Free a process structure
*
* @param process the process to free
*/
extern void git_process_free(git_process *process);
#endif
@ECHO OFF
FOR /F "tokens=*" %%a IN ('more') DO ECHO %%a
#!/bin/sh
echo "Hello, world."
#include "clar_libgit2.h"
#include "process.h"
#include "vector.h"
static git_str env_cmd = GIT_STR_INIT;
static git_str accumulator = GIT_STR_INIT;
static git_vector env_result = GIT_VECTOR_INIT;
void test_process_env__initialize(void)
{
#ifdef GIT_WIN32
git_str_printf(&env_cmd, "%s/env.cmd", cl_fixture("process"));
#else
git_str_puts(&env_cmd, "/usr/bin/env");
#endif
cl_git_pass(git_vector_init(&env_result, 32, git__strcmp_cb));
}
void test_process_env__cleanup(void)
{
git_vector_free(&env_result);
git_str_dispose(&accumulator);
git_str_dispose(&env_cmd);
}
static void run_env(const char **env_array, size_t env_len, bool exclude_env)
{
const char *args_array[] = { env_cmd.ptr };
git_process *process;
git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
git_process_result result = GIT_PROCESS_RESULT_INIT;
char buf[1024], *tok;
ssize_t ret;
opts.capture_out = 1;
opts.exclude_env = exclude_env;
cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), env_array, env_len, &opts));
cl_git_pass(git_process_start(process));
while ((ret = git_process_read(process, buf, 1024)) > 0)
cl_git_pass(git_str_put(&accumulator, buf, (size_t)ret));
cl_assert_equal_i(0, ret);
cl_git_pass(git_process_wait(&result, process));
cl_assert_equal_i(GIT_PROCESS_STATUS_NORMAL, result.status);
cl_assert_equal_i(0, result.exitcode);
cl_assert_equal_i(0, result.signal);
for (tok = strtok(accumulator.ptr, "\n"); tok; tok = strtok(NULL, "\n")) {
#ifdef GIT_WIN32
if (strlen(tok) && tok[strlen(tok) - 1] == '\r')
tok[strlen(tok) - 1] = '\0';
#endif
cl_git_pass(git_vector_insert(&env_result, tok));
}
git_process_close(process);
git_process_free(process);
}
void test_process_env__can_add_env(void)
{
const char *env_array[] = { "TEST_NEW_ENV=added", "TEST_OTHER_ENV=also_added" };
run_env(env_array, 2, false);
cl_git_pass(git_vector_search(NULL, &env_result, "TEST_NEW_ENV=added"));
cl_git_pass(git_vector_search(NULL, &env_result, "TEST_OTHER_ENV=also_added"));
}
void test_process_env__can_propagate_env(void)
{
cl_setenv("TEST_NEW_ENV", "propagated");
run_env(NULL, 0, false);
cl_git_pass(git_vector_search(NULL, &env_result, "TEST_NEW_ENV=propagated"));
}
void test_process_env__can_remove_env(void)
{
const char *env_array[] = { "TEST_NEW_ENV=" };
char *str;
size_t i;
cl_setenv("TEST_NEW_ENV", "propagated");
run_env(env_array, 1, false);
git_vector_foreach(&env_result, i, str)
cl_assert(git__prefixcmp(str, "TEST_NEW_ENV=") != 0);
}
void test_process_env__can_clear_env(void)
{
const char *env_array[] = { "TEST_NEW_ENV=added", "TEST_OTHER_ENV=also_added" };
cl_setenv("SOME_EXISTING_ENV", "propagated");
run_env(env_array, 2, true);
/*
* We can't simply test that the environment is precisely what we
* provided. Some systems (eg win32) will add environment variables
* to all processes.
*/
cl_assert_equal_i(GIT_ENOTFOUND, git_vector_search(NULL, &env_result, "SOME_EXISTING_ENV=propagated"));
}
#include "clar_libgit2.h"
#include "process.h"
#include "vector.h"
#ifndef SIGPIPE
# define SIGPIPE 42
#endif
static git_str helloworld_cmd = GIT_STR_INIT;
static git_str cat_cmd = GIT_STR_INIT;
static git_str pwd_cmd = GIT_STR_INIT;
void test_process_start__initialize(void)
{
#ifdef GIT_WIN32
git_str_printf(&helloworld_cmd, "%s/helloworld.bat", cl_fixture("process"));
git_str_printf(&cat_cmd, "%s/cat.bat", cl_fixture("process"));
git_str_printf(&pwd_cmd, "%s/pwd.bat", cl_fixture("process"));
#else
git_str_printf(&helloworld_cmd, "%s/helloworld.sh", cl_fixture("process"));
#endif
}
void test_process_start__cleanup(void)
{
git_str_dispose(&pwd_cmd);
git_str_dispose(&cat_cmd);
git_str_dispose(&helloworld_cmd);
}
void test_process_start__returncode(void)
{
#ifdef GIT_WIN32
const char *args_array[] = { "C:\\Windows\\System32\\cmd.exe", "/c", "exit", "1" };
#elif __APPLE__
const char *args_array[] = { "/usr/bin/false" };
#else
const char *args_array[] = { "/bin/false" };
#endif
git_process *process;
git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
git_process_result result = GIT_PROCESS_RESULT_INIT;
cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), NULL, 0, &opts));
cl_git_pass(git_process_start(process));
cl_git_pass(git_process_wait(&result, process));
cl_assert_equal_i(GIT_PROCESS_STATUS_NORMAL, result.status);
cl_assert_equal_i(1, result.exitcode);
cl_assert_equal_i(0, result.signal);
git_process_free(process);
}
void test_process_start__not_found(void)
{
#ifdef GIT_WIN32
const char *args_array[] = { "C:\\a\\b\\z\\y\\not_found" };
#else
const char *args_array[] = { "/a/b/z/y/not_found" };
#endif
git_process *process;
git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), NULL, 0, &opts));
cl_git_fail(git_process_start(process));
git_process_free(process);
}
static void write_all(git_process *process, char *buf)
{
size_t buf_len = strlen(buf);
ssize_t ret;
while (buf_len) {
ret = git_process_write(process, buf, buf_len);
cl_git_pass(ret < 0 ? (int)ret : 0);
buf += ret;
buf_len -= ret;
}
}
static void read_all(git_str *out, git_process *process)
{
char buf[32];
size_t buf_len = 32;
ssize_t ret;
while ((ret = git_process_read(process, buf, buf_len)) > 0)
cl_git_pass(git_str_put(out, buf, ret));
cl_git_pass(ret < 0 ? (int)ret : 0);
}
void test_process_start__redirect_stdio(void)
{
#ifdef GIT_WIN32
const char *args_array[] = { "C:\\Windows\\System32\\cmd.exe", "/c", cat_cmd.ptr };
#else
const char *args_array[] = { "/bin/cat" };
#endif
git_process *process;
git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
git_process_result result = GIT_PROCESS_RESULT_INIT;
git_str buf = GIT_STR_INIT;
opts.capture_in = 1;
opts.capture_out = 1;
cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), NULL, 0, &opts));
cl_git_pass(git_process_start(process));
write_all(process, "Hello, world.\r\nHello!\r\n");
cl_git_pass(git_process_close_in(process));
read_all(&buf, process);
cl_assert_equal_s("Hello, world.\r\nHello!\r\n", buf.ptr);
cl_git_pass(git_process_wait(&result, process));
cl_assert_equal_i(GIT_PROCESS_STATUS_NORMAL, result.status);
cl_assert_equal_i(0, result.exitcode);
cl_assert_equal_i(0, result.signal);
git_str_dispose(&buf);
git_process_free(process);
}
void test_process_start__catch_signal(void)
{
#ifndef GIT_WIN32
const char *args_array[] = { helloworld_cmd.ptr };
git_process *process;
git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
git_process_result result = GIT_PROCESS_RESULT_INIT;
opts.capture_out = 1;
cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), NULL, 0, &opts));
cl_git_pass(git_process_start(process));
cl_git_pass(git_process_close(process));
cl_git_pass(git_process_wait(&result, process));
cl_assert_equal_i(GIT_PROCESS_STATUS_ERROR, result.status);
cl_assert_equal_i(0, result.exitcode);
cl_assert_equal_i(SIGPIPE, result.signal);
git_process_free(process);
#endif
}
void test_process_start__can_chdir(void)
{
#ifdef GIT_WIN32
const char *args_array[] = { "C:\\Windows\\System32\\cmd.exe", "/c", pwd_cmd.ptr };
char *startwd = "C:\\";
#else
const char *args_array[] = { "/bin/pwd" };
char *startwd = "/";
#endif
git_process *process;
git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
git_process_result result = GIT_PROCESS_RESULT_INIT;
git_str buf = GIT_STR_INIT;
opts.cwd = startwd;
opts.capture_out = 1;
cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), NULL, 0, &opts));
cl_git_pass(git_process_start(process));
read_all(&buf, process);
git_str_rtrim(&buf);
cl_assert_equal_s(startwd, buf.ptr);
cl_git_pass(git_process_wait(&result, process));
cl_assert_equal_i(GIT_PROCESS_STATUS_NORMAL, result.status);
cl_assert_equal_i(0, result.exitcode);
cl_assert_equal_i(0, result.signal);
git_str_dispose(&buf);
git_process_free(process);
}
void test_process_start__cannot_chdir_to_nonexistent_dir(void)
{
#ifdef GIT_WIN32
const char *args_array[] = { "C:\\Windows\\System32\\cmd.exe", "/c", pwd_cmd.ptr };
char *startwd = "C:\\a\\b\\z\\y\\not_found";
#else
const char *args_array[] = { "/bin/pwd" };
char *startwd = "/a/b/z/y/not_found";
#endif
git_process *process;
git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
opts.cwd = startwd;
opts.capture_out = 1;
cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), NULL, 0, &opts));
cl_git_fail(git_process_start(process));
git_process_free(process);
}
#include "clar_libgit2.h"
#include "process.h"
#include "vector.h"
#ifdef GIT_WIN32
static git_str result;
# define assert_cmdline(expected, given) do { \
cl_git_pass(git_process__cmdline(&result, given, ARRAY_SIZE(given))); \
cl_assert_equal_s(expected, result.ptr); \
git_str_dispose(&result); \
} while(0)
#endif
void test_process_win32__cmdline_is_whitespace_delimited(void)
{
#ifdef GIT_WIN32
const char *one[] = { "one" };
const char *two[] = { "one", "two" };
const char *three[] = { "one", "two", "three" };
const char *four[] = { "one", "two", "three", "four" };
assert_cmdline("one", one);
assert_cmdline("one two", two);
assert_cmdline("one two three", three);
assert_cmdline("one two three four", four);
#endif
}
void test_process_win32__cmdline_escapes_whitespace(void)
{
#ifdef GIT_WIN32
const char *spaces[] = { "one with spaces" };
const char *tabs[] = { "one\twith\ttabs" };
const char *multiple[] = { "one with many spaces" };
assert_cmdline("one\" \"with\" \"spaces", spaces);
assert_cmdline("one\"\t\"with\"\t\"tabs", tabs);
assert_cmdline("one\" \"with\" \"many\" \"spaces", multiple);
#endif
}
void test_process_win32__cmdline_escapes_quotes(void)
{
#ifdef GIT_WIN32
const char *one[] = { "echo", "\"hello world\"" };
assert_cmdline("echo \\\"hello\" \"world\\\"", one);
#endif
}
void test_process_win32__cmdline_escapes_backslash(void)
{
#ifdef GIT_WIN32
const char *one[] = { "foo\\bar", "foo\\baz" };
const char *two[] = { "c:\\program files\\foo bar\\foo bar.exe", "c:\\path\\to\\other\\", "/a", "/b" };
assert_cmdline("foo\\\\bar foo\\\\baz", one);
assert_cmdline("c:\\\\program\" \"files\\\\foo\" \"bar\\\\foo\" \"bar.exe c:\\\\path\\\\to\\\\other\\\\ /a /b", two);
#endif
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment