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
/*
* 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.
*/
#include <stdio.h>
#include <sys/wait.h>
#include <git2.h>
#include "git2_util.h"
#include "vector.h"
#include "process.h"
#include "strlist.h"
extern char **environ;
struct git_process {
char **args;
char **env;
char *cwd;
unsigned int capture_in : 1,
capture_out : 1,
capture_err : 1;
pid_t pid;
int child_in;
int child_out;
int child_err;
git_process_result_status status;
};
GIT_INLINE(bool) is_delete_env(const char *env)
{
char *c = index(env, '=');
if (c == NULL)
return false;
return *(c+1) == '\0';
}
static int merge_env(
char ***out,
const char **env,
size_t env_len,
bool exclude_env)
{
git_vector merged = GIT_VECTOR_INIT;
char **kv, *dup;
size_t max, cnt;
int error = 0;
for (max = env_len, kv = environ; !exclude_env && *kv; kv++)
max++;
if ((error = git_vector_init(&merged, max, NULL)) < 0)
goto on_error;
for (cnt = 0; env && cnt < env_len; cnt++) {
if (is_delete_env(env[cnt]))
continue;
dup = git__strdup(env[cnt]);
GIT_ERROR_CHECK_ALLOC(dup);
if ((error = git_vector_insert(&merged, dup)) < 0)
goto on_error;
}
if (!exclude_env) {
for (kv = environ; *kv; kv++) {
if (env && git_strlist_contains_key(env, env_len, *kv, '='))
continue;
dup = git__strdup(*kv);
GIT_ERROR_CHECK_ALLOC(dup);
if ((error = git_vector_insert(&merged, dup)) < 0)
goto on_error;
}
}
if (merged.length == 0) {
*out = NULL;
error = 0;
goto on_error;
}
git_vector_insert(&merged, NULL);
*out = (char **)merged.contents;
return 0;
on_error:
git_vector_free_deep(&merged);
return error;
}
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)
{
git_process *process;
GIT_ASSERT_ARG(out && args && args_len > 0);
*out = NULL;
process = git__calloc(sizeof(git_process), 1);
GIT_ERROR_CHECK_ALLOC(process);
if (git_strlist_copy_with_null(&process->args, args, args_len) < 0 ||
merge_env(&process->env, env, env_len, opts->exclude_env) < 0) {
git_process_free(process);
return -1;
}
if (opts) {
process->capture_in = opts->capture_in;
process->capture_out = opts->capture_out;
process->capture_err = opts->capture_err;
if (opts->cwd) {
process->cwd = git__strdup(opts->cwd);
GIT_ERROR_CHECK_ALLOC(process->cwd);
}
}
process->child_in = -1;
process->child_out = -1;
process->child_err = -1;
process->status = -1;
*out = process;
return 0;
}
#define CLOSE_FD(fd) \
if (fd >= 0) { \
close(fd); \
fd = -1; \
}
static int try_read(size_t *out, int fd, void *buf, size_t len)
{
size_t read_len = 0;
int ret = -1;
while (ret && read_len < len) {
ret = read(fd, buf + read_len, len - read_len);
if (ret < 0 && errno != EAGAIN && errno != EINTR) {
git_error_set(GIT_ERROR_OS, "could not read child status");
return -1;
}
read_len += ret;
}
*out = read_len;
return 0;
}
static int read_status(int fd)
{
size_t status_len = sizeof(int) * 3, read_len = 0;
char buffer[status_len], fn[128];
int error, fn_error, os_error, fn_len = 0;
if ((error = try_read(&read_len, fd, buffer, status_len)) < 0)
return error;
/* Immediate EOF indicates the exec succeeded. */
if (read_len == 0)
return 0;
if (read_len < status_len) {
git_error_set(GIT_ERROR_INVALID, "child status truncated");
return -1;
}
memcpy(&fn_error, &buffer[0], sizeof(int));
memcpy(&os_error, &buffer[sizeof(int)], sizeof(int));
memcpy(&fn_len, &buffer[sizeof(int) * 2], sizeof(int));
if (fn_len > 0) {
fn_len = min(fn_len, (int)(ARRAY_SIZE(fn) - 1));
if ((error = try_read(&read_len, fd, fn, fn_len)) < 0)
return error;
fn[fn_len] = '\0';
} else {
fn[0] = '\0';
}
if (fn_error) {
errno = os_error;
git_error_set(GIT_ERROR_OS, "could not %s", fn[0] ? fn : "(unknown)");
}
return fn_error;
}
static bool try_write(int fd, const void *buf, size_t len)
{
size_t write_len;
int ret;
for (write_len = 0; write_len < len; ) {
ret = write(fd, buf + write_len, len - write_len);
if (ret <= 0)
break;
write_len += ret;
}
return (len == write_len);
}
static void write_status(int fd, const char *fn, int error, int os_error)
{
size_t status_len = sizeof(int) * 3, fn_len;
char buffer[status_len];
fn_len = strlen(fn);
if (fn_len > INT_MAX)
fn_len = INT_MAX;
memcpy(&buffer[0], &error, sizeof(int));
memcpy(&buffer[sizeof(int)], &os_error, sizeof(int));
memcpy(&buffer[sizeof(int) * 2], &fn_len, sizeof(int));
/* Do our best effort to write all the status. */
if (!try_write(fd, buffer, status_len))
return;
if (fn_len)
try_write(fd, fn, fn_len);
}
int git_process_start(git_process *process)
{
int in[2] = { -1, -1 }, out[2] = { -1, -1 },
err[2] = { -1, -1 }, status[2] = { -1, -1 };
int fdflags, state, error;
pid_t pid;
/* Set up the pipes to read from/write to the process */
if ((process->capture_in && pipe(in) < 0) ||
(process->capture_out && pipe(out) < 0) ||
(process->capture_err && pipe(err) < 0)) {
git_error_set(GIT_ERROR_OS, "could not create pipe");
goto on_error;
}
/* Set up a self-pipe for status from the forked process. */
if (pipe(status) < 0 ||
(fdflags = fcntl(status[1], F_GETFD)) < 0 ||
fcntl(status[1], F_SETFD, fdflags | FD_CLOEXEC) < 0) {
git_error_set(GIT_ERROR_OS, "could not create pipe");
goto on_error;
}
switch (pid = fork()) {
case -1:
git_error_set(GIT_ERROR_OS, "could not fork");
goto on_error;
/* Child: start the process. */
case 0:
/* Close the opposing side of the pipes */
CLOSE_FD(status[0]);
if (process->capture_in) {
CLOSE_FD(in[1]);
dup2(in[0], STDIN_FILENO);
}
if (process->capture_out) {
CLOSE_FD(out[0]);
dup2(out[1], STDOUT_FILENO);
}
if (process->capture_err) {
CLOSE_FD(err[0]);
dup2(err[1], STDERR_FILENO);
}
if (process->cwd && (error = chdir(process->cwd)) < 0) {
write_status(status[1], "chdir", error, errno);
exit(0);
}
/*
* Exec the process and write the results back if the
* call fails. If it succeeds, we'll close the status
* pipe (via CLOEXEC) and the parent will know.
*/
error = execve(process->args[0],
process->args,
process->env);
write_status(status[1], "execve", error, errno);
exit(0);
/* Parent: make sure the child process exec'd correctly. */
default:
/* Close the opposing side of the pipes */
CLOSE_FD(status[1]);
if (process->capture_in) {
CLOSE_FD(in[0]);
process->child_in = in[1];
}
if (process->capture_out) {
CLOSE_FD(out[1]);
process->child_out = out[0];
}
if (process->capture_err) {
CLOSE_FD(err[1]);
process->child_err = err[0];
}
/* Try to read the status */
process->status = status[0];
if ((error = read_status(status[0])) < 0) {
waitpid(process->pid, &state, 0);
goto on_error;
}
process->pid = pid;
return 0;
}
on_error:
CLOSE_FD(in[0]); CLOSE_FD(in[1]);
CLOSE_FD(out[0]); CLOSE_FD(out[1]);
CLOSE_FD(err[0]); CLOSE_FD(err[1]);
CLOSE_FD(status[0]); CLOSE_FD(status[1]);
return -1;
}
ssize_t git_process_read(git_process *process, void *buf, size_t count)
{
ssize_t ret;
GIT_ASSERT_ARG(process);
GIT_ASSERT(process->capture_out);
if (count > SSIZE_MAX)
count = SSIZE_MAX;
if ((ret = read(process->child_out, buf, count)) < 0) {
git_error_set(GIT_ERROR_OS, "could not read from child process");
return -1;
}
return ret;
}
ssize_t git_process_write(git_process *process, const void *buf, size_t count)
{
ssize_t ret;
GIT_ASSERT_ARG(process);
GIT_ASSERT(process->capture_in);
if (count > SSIZE_MAX)
count = SSIZE_MAX;
if ((ret = write(process->child_in, buf, count)) < 0) {
git_error_set(GIT_ERROR_OS, "could not write to child process");
return -1;
}
return ret;
}
int git_process_close_in(git_process *process)
{
if (!process->capture_in) {
git_error_set(GIT_ERROR_INVALID, "input is not open");
return -1;
}
CLOSE_FD(process->child_in);
return 0;
}
int git_process_close_out(git_process *process)
{
if (!process->capture_out) {
git_error_set(GIT_ERROR_INVALID, "output is not open");
return -1;
}
CLOSE_FD(process->child_out);
return 0;
}
int git_process_close_err(git_process *process)
{
if (!process->capture_err) {
git_error_set(GIT_ERROR_INVALID, "error is not open");
return -1;
}
CLOSE_FD(process->child_err);
return 0;
}
int git_process_close(git_process *process)
{
CLOSE_FD(process->child_in);
CLOSE_FD(process->child_out);
CLOSE_FD(process->child_err);
CLOSE_FD(process->status);
return 0;
}
int git_process_wait(git_process_result *result, git_process *process)
{
int state;
if (result)
memset(result, 0, sizeof(git_process_result));
if (!process->pid) {
git_error_set(GIT_ERROR_INVALID, "process is stopped");
return -1;
}
if (waitpid(process->pid, &state, 0) < 0) {
git_error_set(GIT_ERROR_OS, "could not wait for child");
return -1;
}
process->pid = 0;
if (result) {
if (WIFEXITED(state)) {
result->status = GIT_PROCESS_STATUS_NORMAL;
result->exitcode = WEXITSTATUS(state);
} else if (WIFSIGNALED(state)) {
result->status = GIT_PROCESS_STATUS_ERROR;
result->signal = WTERMSIG(state);
} else {
result->status = GIT_PROCESS_STATUS_ERROR;
}
}
return 0;
}
int git_process_result_msg(git_str *out, git_process_result *result)
{
if (result->status == GIT_PROCESS_STATUS_NONE) {
return git_str_puts(out, "process not started");
} else if (result->status == GIT_PROCESS_STATUS_NORMAL) {
return git_str_printf(out, "process exited with code %d",
result->exitcode);
} else if (result->signal) {
return git_str_printf(out, "process exited on signal %d",
result->signal);
}
return git_str_puts(out, "unknown error");
}
void git_process_free(git_process *process)
{
if (!process)
return;
if (process->pid)
git_process_close(process);
git__free(process->cwd);
git_strlist_free_with_null(process->args);
git_strlist_free_with_null(process->env);
git__free(process);
}
/*
* 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.
*/
#include <stdio.h>
#include <git2.h>
#include "git2_util.h"
#include "process.h"
#include "strlist.h"
#ifndef DWORD_MAX
# define DWORD_MAX INT32_MAX
#endif
#define ENV_MAX 32767
struct git_process {
wchar_t *appname;
wchar_t *cmdline;
wchar_t *env;
wchar_t *cwd;
unsigned int capture_in : 1,
capture_out : 1,
capture_err : 1;
PROCESS_INFORMATION process_info;
HANDLE child_in;
HANDLE child_out;
HANDLE child_err;
git_process_result_status status;
};
/*
* Windows processes have a single command-line that is split by the
* invoked application into arguments (instead of an array of
* command-line arguments). This command-line is split by space or
* tab delimiters, unless that whitespace is within a double quote.
* Literal double-quotes themselves can be escaped by a backslash,
* but only when not within double quotes. Literal backslashes can
* be escaped by a backslash.
*
* Effectively, this means that instead of thinking about quoting
* individual strings, think about double quotes as an escaping
* mechanism for whitespace.
*
* In other words (using ` as a string boundary):
* [ `foo`, `bar` ] => `foo bar`
* [ `foo bar` ] => `foo" "bar`
* [ `foo bar`, `foo bar` ] => `foo" "bar foo" "bar`
* [ `foo "bar" foo` ] => `foo" "\"bar\"" "foo`
*/
int git_process__cmdline(
git_str *out,
const char **in,
size_t in_len)
{
bool quoted = false;
const char *c;
size_t i;
for (i = 0; i < in_len; i++) {
/* Arguments are delimited by an unquoted space */
if (i)
git_str_putc(out, ' ');
for (c = in[i]; *c; c++) {
/* Start or stop quoting spaces within an argument */
if ((*c == ' ' || *c == '\t') && !quoted) {
git_str_putc(out, '"');
quoted = true;
} else if (*c != ' ' && *c != '\t' && quoted) {
git_str_putc(out, '"');
quoted = false;
}
/* Escape double-quotes and backslashes */
if (*c == '"' || *c == '\\')
git_str_putc(out, '\\');
git_str_putc(out, *c);
}
}
return git_str_oom(out) ? -1 : 0;
}
GIT_INLINE(bool) is_delete_env(const char *env)
{
char *c = strchr(env, '=');
if (c == NULL)
return false;
return *(c+1) == '\0';
}
static int merge_env(wchar_t **out, const char **in, size_t in_len, bool exclude_env)
{
git_str merged = GIT_STR_INIT;
wchar_t *in16 = NULL, *env = NULL, *e;
char *e8 = NULL;
size_t e_len;
int ret = 0;
size_t i;
*out = NULL;
in16 = git__malloc(ENV_MAX * sizeof(wchar_t));
GIT_ERROR_CHECK_ALLOC(in16);
e8 = git__malloc(ENV_MAX);
GIT_ERROR_CHECK_ALLOC(e8);
for (i = 0; in && i < in_len; i++) {
if (is_delete_env(in[i]))
continue;
if ((ret = git_utf8_to_16(in16, ENV_MAX, in[i])) < 0)
goto done;
git_str_put(&merged, (const char *)in16, ret * 2);
git_str_put(&merged, "\0\0", 2);
}
if (!exclude_env) {
env = GetEnvironmentStringsW();
for (e = env; *e; e += (e_len + 1)) {
e_len = wcslen(e);
if ((ret = git_utf8_from_16(e8, ENV_MAX, e)) < 0)
goto done;
if (git_strlist_contains_key(in, in_len, e8, '='))
continue;
git_str_put(&merged, (const char *)e, e_len * 2);
git_str_put(&merged, "\0\0", 2);
}
}
git_str_put(&merged, "\0\0", 2);
*out = (wchar_t *)git_str_detach(&merged);
done:
if (env)
FreeEnvironmentStringsW(env);
git_str_dispose(&merged);
git__free(e8);
git__free(in16);
return ret < 0 ? -1 : 0;
}
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)
{
git_process *process;
git_str cmdline = GIT_STR_INIT;
int error;
GIT_ASSERT_ARG(out && args && args_len > 0);
*out = NULL;
process = git__calloc(1, sizeof(git_process));
GIT_ERROR_CHECK_ALLOC(process);
if ((error = git_process__cmdline(&cmdline, args, args_len)) < 0)
goto done;
if (git_utf8_to_16_alloc(&process->appname, args[0]) < 0 ||
git_utf8_to_16_alloc(&process->cmdline, cmdline.ptr) < 0) {
error = -1;
goto done;
}
if (opts && opts->cwd &&
git_utf8_to_16_alloc(&process->cwd, opts->cwd) < 0) {
error = -1;
goto done;
}
if (env && (error = merge_env(&process->env, env, env_len, opts && opts->exclude_env) < 0))
goto done;
if (opts) {
process->capture_in = opts->capture_in;
process->capture_out = opts->capture_out;
process->capture_err = opts->capture_err;
}
done:
if (error)
git_process_free(process);
else
*out = process;
git_str_dispose(&cmdline);
return error;
}
#define CLOSE_HANDLE(h) do { if ((h) != NULL) CloseHandle(h); } while(0)
int git_process_start(git_process *process)
{
STARTUPINFOW startup_info;
SECURITY_ATTRIBUTES security_attrs;
DWORD flags = CREATE_UNICODE_ENVIRONMENT;
HANDLE in[2] = { NULL, NULL },
out[2] = { NULL, NULL },
err[2] = { NULL, NULL };
memset(&security_attrs, 0, sizeof(SECURITY_ATTRIBUTES));
security_attrs.bInheritHandle = TRUE;
memset(&startup_info, 0, sizeof(STARTUPINFOW));
startup_info.cb = sizeof(STARTUPINFOW);
startup_info.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
startup_info.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
startup_info.hStdError = GetStdHandle(STD_ERROR_HANDLE);
if (process->capture_in) {
if (!CreatePipe(&in[0], &in[1], &security_attrs, 0) ||
!SetHandleInformation(in[1], HANDLE_FLAG_INHERIT, 0)) {
git_error_set(GIT_ERROR_OS, "could not create pipe");
goto on_error;
}
startup_info.hStdInput = in[0];
startup_info.dwFlags |= STARTF_USESTDHANDLES;
}
if (process->capture_out) {
if (!CreatePipe(&out[0], &out[1], &security_attrs, 0) ||
!SetHandleInformation(out[0], HANDLE_FLAG_INHERIT, 0)) {
git_error_set(GIT_ERROR_OS, "could not create pipe");
goto on_error;
}
startup_info.hStdOutput = out[1];
startup_info.dwFlags |= STARTF_USESTDHANDLES;
}
if (process->capture_err) {
if (!CreatePipe(&err[0], &err[1], &security_attrs, 0) ||
!SetHandleInformation(err[0], HANDLE_FLAG_INHERIT, 0)) {
git_error_set(GIT_ERROR_OS, "could not create pipe");
goto on_error;
}
startup_info.hStdError = err[1];
startup_info.dwFlags |= STARTF_USESTDHANDLES;
}
memset(&process->process_info, 0, sizeof(PROCESS_INFORMATION));
if (!CreateProcessW(process->appname, process->cmdline,
NULL, NULL, TRUE, flags, process->env,
process->cwd,
&startup_info,
&process->process_info)) {
git_error_set(GIT_ERROR_OS, "could not create process");
goto on_error;
}
CLOSE_HANDLE(in[0]); process->child_in = in[1];
CLOSE_HANDLE(out[1]); process->child_out = out[0];
CLOSE_HANDLE(err[1]); process->child_err = err[0];
return 0;
on_error:
CLOSE_HANDLE(in[0]); CLOSE_HANDLE(in[1]);
CLOSE_HANDLE(out[0]); CLOSE_HANDLE(out[1]);
CLOSE_HANDLE(err[0]); CLOSE_HANDLE(err[1]);
return -1;
}
ssize_t git_process_read(git_process *process, void *buf, size_t count)
{
DWORD ret;
if (count > DWORD_MAX)
count = DWORD_MAX;
if (count > SSIZE_MAX)
count = SSIZE_MAX;
if (!ReadFile(process->child_out, buf, (DWORD)count, &ret, NULL)) {
if (GetLastError() == ERROR_BROKEN_PIPE)
return 0;
git_error_set(GIT_ERROR_OS, "could not read");
return -1;
}
return ret;
}
ssize_t git_process_write(git_process *process, const void *buf, size_t count)
{
DWORD ret;
if (count > DWORD_MAX)
count = DWORD_MAX;
if (count > SSIZE_MAX)
count = SSIZE_MAX;
if (!WriteFile(process->child_in, buf, (DWORD)count, &ret, NULL)) {
git_error_set(GIT_ERROR_OS, "could not write");
return -1;
}
return ret;
}
int git_process_close_in(git_process *process)
{
if (!process->capture_in) {
git_error_set(GIT_ERROR_INVALID, "input is not open");
return -1;
}
if (process->child_in) {
CloseHandle(process->child_in);
process->child_in = NULL;
}
return 0;
}
int git_process_close_out(git_process *process)
{
if (!process->capture_out) {
git_error_set(GIT_ERROR_INVALID, "output is not open");
return -1;
}
if (process->child_out) {
CloseHandle(process->child_out);
process->child_out = NULL;
}
return 0;
}
int git_process_close_err(git_process *process)
{
if (!process->capture_err) {
git_error_set(GIT_ERROR_INVALID, "error is not open");
return -1;
}
if (process->child_err) {
CloseHandle(process->child_err);
process->child_err = NULL;
}
return 0;
}
int git_process_close(git_process *process)
{
if (process->child_in) {
CloseHandle(process->child_in);
process->child_in = NULL;
}
if (process->child_out) {
CloseHandle(process->child_out);
process->child_out = NULL;
}
if (process->child_err) {
CloseHandle(process->child_err);
process->child_err = NULL;
}
CloseHandle(process->process_info.hProcess);
process->process_info.hProcess = NULL;
CloseHandle(process->process_info.hThread);
process->process_info.hThread = NULL;
return 0;
}
int git_process_wait(git_process_result *result, git_process *process)
{
DWORD exitcode;
if (result)
memset(result, 0, sizeof(git_process_result));
if (!process->process_info.dwProcessId) {
git_error_set(GIT_ERROR_INVALID, "process is stopped");
return -1;
}
if (WaitForSingleObject(process->process_info.hProcess, INFINITE) == WAIT_FAILED) {
git_error_set(GIT_ERROR_OS, "could not wait for process");
return -1;
}
if (!GetExitCodeProcess(process->process_info.hProcess, &exitcode)) {
git_error_set(GIT_ERROR_OS, "could not get process exit code");
return -1;
}
result->status = GIT_PROCESS_STATUS_NORMAL;
result->exitcode = exitcode;
memset(&process->process_info, 0, sizeof(PROCESS_INFORMATION));
return 0;
}
int git_process_result_msg(git_str *out, git_process_result *result)
{
if (result->status == GIT_PROCESS_STATUS_NONE) {
return git_str_puts(out, "process not started");
} else if (result->status == GIT_PROCESS_STATUS_NORMAL) {
return git_str_printf(out, "process exited with code %d",
result->exitcode);
} else if (result->signal) {
return git_str_printf(out, "process exited on signal %d",
result->signal);
}
return git_str_puts(out, "unknown error");
}
void git_process_free(git_process *process)
{
if (!process)
return;
if (process->process_info.hProcess)
git_process_close(process);
git__free(process->env);
git__free(process->cwd);
git__free(process->cmdline);
git__free(process->appname);
git__free(process);
}
@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