blob: 44ead18528552d131d3d3123a749dc4f4c1e9b0b [file] [log] [blame] [edit]
/*
* Copyright (C) Tildeslash Ltd. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give
* permission to link the code of portions of this program with the
* OpenSSL library under certain conditions as described in each
* individual source file, and distribute linked combinations
* including the two.
*
* You must obey the GNU Affero General Public License in all respects
* for all of the code used other than OpenSSL.
*/
#include "Config.h"
#include <stdio.h>
#include <ctype.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include "Str.h"
#include "Dir.h"
#include "File.h"
#include "List.h"
#include "system/Net.h"
#include "system/System.h"
#include "system/Command.h"
/**
* Implementation of the Command and Process interfaces.
*
* @see http://www.mmonit.com/
* @file
*/
/* ----------------------------------------------------------- Definitions */
#define T Command_T
struct T {
uid_t uid;
gid_t gid;
List_T env;
List_T args;
char *working_directory;
};
struct Process_T {
pid_t pid;
uid_t uid;
gid_t gid;
char **env;
char **args;
int status;
int stdin_pipe[2];
int stdout_pipe[2];
int stderr_pipe[2];
InputStream_T in;
InputStream_T err;
OutputStream_T out;
char *working_directory;
};
const char *Command_Path = "PATH=/bin:/usr/bin:/usr/local/bin:/opt/csw/bin:/usr/sfw/bin";
/* --------------------------------------------------------------- Private */
/* Search the env list and return the pointer to the name (in the list)
if found, otherwise NULL */
static inline char *findEnv(T C, const char *name) {
for (list_t p = C->env->head; p; p = p->next)
if (Str_startsWith(p->e, name))
return p->e;
return NULL;
}
/* Remove env variable and value identified by name */
static inline void removeEnv(T C, const char *name) {
char *e = findEnv(C, name);
if (e) {
List_remove(C->env, e);
FREE(e);
}
}
/* Free each string in a list of strings */
static void freeStrings(List_T l) {
while (List_length(l) > 0) {
char *s = List_pop(l);
FREE(s);
}
}
/* Build the Command args list. The list represent the array sent
to execv and the List contains the following entries: args[0]
is the path to the program, the rest are arguments to the program */
static void buildArgs(T C, const char *path, const char *x, va_list ap) {
freeStrings(C->args);
List_append(C->args, Str_dup(path));
va_list ap_copy;
va_copy(ap_copy, ap);
for (; x; x = va_arg(ap_copy, char *))
List_append(C->args, Str_dup(x));
va_end(ap_copy);
}
/* Create stdio pipes for communication between parent and child process */
static void createPipes(Process_T P) {
if (pipe(P->stdin_pipe) < 0
|| pipe(P->stdout_pipe) < 0
|| pipe(P->stderr_pipe) < 0) {
ERROR("Command pipe(2): Bad file descriptors -- %s", System_getLastError());
}
}
/* Setup stdio pipes in subprocess */
static void setupChildPipes(Process_T P) {
close(P->stdin_pipe[1]); // close write end
if (P->stdin_pipe[0] != STDIN_FILENO) {
if (dup2(P->stdin_pipe[0], STDIN_FILENO) != STDIN_FILENO)
ERROR("Command: dup2(stdin) -- %s\n", System_getLastError());
close(P->stdin_pipe[0]);
}
close(P->stdout_pipe[0]); // close read end
if (P->stdout_pipe[1] != STDOUT_FILENO) {
if (dup2(P->stdout_pipe[1], STDOUT_FILENO) != STDOUT_FILENO)
ERROR("Command: dup2(stdout) -- %s\n", System_getLastError());
close(P->stdout_pipe[1]);
}
close(P->stderr_pipe[0]); // close read end
if (P->stderr_pipe[1] != STDERR_FILENO) {
if (dup2(P->stderr_pipe[1], STDERR_FILENO) != STDERR_FILENO)
ERROR("Command: dup2(stderr) -- %s\n", System_getLastError());
close(P->stderr_pipe[1]);
}
}
/* Setup stdio pipes in parent process for communication with the subprocess */
static void setupParentPipes(Process_T P) {
close(P->stdin_pipe[0]); // close read end
Net_setNonBlocking(P->stdin_pipe[1]);
close(P->stdout_pipe[1]); // close write end
Net_setNonBlocking(P->stdout_pipe[0]);
close(P->stderr_pipe[1]); // close write end
Net_setNonBlocking(P->stderr_pipe[0]);
}
/* Close stdio pipes in parent process */
static void closeParentPipes(Process_T P) {
close(P->stdin_pipe[1]); // close write end
close(P->stdout_pipe[0]); // close read end
close(P->stderr_pipe[0]); // close read end
}
/* Close and destroy opened stdio streams */
static void closeStreams(Process_T P) {
if (P->in) InputStream_free(&P->in);
if (P->err) InputStream_free(&P->err);
if (P->out) OutputStream_free(&P->out);
}
/* -------------------------------------------------------------- Process_T */
static inline void setstatus(Process_T P) {
if (WIFEXITED(P->status))
P->status = WEXITSTATUS(P->status);
else if (WIFSIGNALED(P->status))
P->status = WTERMSIG(P->status);
else if (WIFSTOPPED(P->status))
P->status = WSTOPSIG(P->status);
}
static Process_T Process_new(void) {
Process_T P;
NEW(P);
P->status = -1;
return P;
}
void Process_free(Process_T *P) {
assert(P && *P);
FREE((*P)->args);
FREE((*P)->env);
FREE((*P)->working_directory);
if (Process_isRunning(*P))
Process_kill(*P);
closeParentPipes(*P);
closeStreams(*P);
FREE(*P);
}
uid_t Process_getUid(Process_T P) {
assert(P);
return P->uid;
}
gid_t Process_getGid(Process_T P) {
assert(P);
return P->gid;
}
const char *Process_getDir(Process_T P) {
assert(P);
if (! P->working_directory) {
char t[STRLEN];
P->working_directory = Str_dup(Dir_cwd(t, STRLEN));
}
return P->working_directory;
}
pid_t Process_getPid(Process_T P) {
assert(P);
return P->pid;
}
int Process_waitFor(Process_T P) {
assert(P);
if (P->status < 0) {
int r;
do
r = waitpid(P->pid, &P->status, 0); // Wait blocking
while (r == -1 && errno == EINTR);
if (r != P->pid)
P->status = -1;
else
setstatus(P);
}
return P->status;
}
int Process_exitStatus(Process_T P) {
assert(P);
if (P->status < 0) {
int r;
do
r = waitpid(P->pid, &P->status, WNOHANG); // Wait non-blocking
while (r == -1 && errno == EINTR);
if (r == 0) // Process is still running
P->status = -1;
else
setstatus(P);
}
return P->status;
}
int Process_isRunning(Process_T P) {
assert(P);
errno = 0;
return ((getpgid(P->pid) > -1) || (errno == EPERM));
}
OutputStream_T Process_getOutputStream(Process_T P) {
assert(P);
if (! P->out)
P->out = OutputStream_new(P->stdin_pipe[1]);
return P->out;
}
InputStream_T Process_getInputStream(Process_T P) {
assert(P);
if (! P->in)
P->in = InputStream_new(P->stdout_pipe[0]);
return P->in;
}
InputStream_T Process_getErrorStream(Process_T P) {
assert(P);
if (! P->err)
P->err = InputStream_new(P->stderr_pipe[0]);
return P->err;
}
void Process_terminate(Process_T P) {
assert(P);
kill(P->pid, SIGTERM);
}
void Process_kill(Process_T P) {
assert(P);
kill(P->pid, SIGKILL);
}
/* ---------------------------------------------------------------- Public */
T Command_new(const char *path, const char *arg0, ...) {
T C;
if (! File_exist(path))
THROW(AssertException, "File '%s' does not exist", path ? path : "null");
NEW(C);
C->env = List_new();
C->args = List_new();
List_append(C->env, Str_dup(Command_Path));
va_list ap;
va_start(ap, arg0);
buildArgs(C, path, arg0, ap);
va_end(ap);
return C;
}
void Command_free(T *C) {
assert(C && *C);
freeStrings((*C)->args);
List_free(&(*C)->args);
freeStrings((*C)->env);
List_free(&(*C)->env);
FREE((*C)->working_directory);
FREE(*C);
}
void Command_setUid(T C, uid_t uid) {
assert(C);
C->uid = uid;
}
uid_t Command_getUid(T C) {
assert(C);
return C->uid;
}
void Command_setGid(T C, gid_t gid) {
assert(C);
C->gid = gid;
}
gid_t Command_getGid(T C) {
assert(C);
return C->gid;
}
void Command_setDir(T C, const char *dir) {
assert(C);
if (dir) { // Allow to set a NULL directory, meaning the calling process's current directory
if (! File_isDirectory(dir))
THROW(AssertException, "The working directory '%s' is not a directory", dir);
if (! File_isExecutable(dir))
THROW(AssertException, "The working directory '%s' is not accessible", dir);
}
FREE(C->working_directory);
C->working_directory = Str_dup(dir);
File_removeTrailingSeparator(C->working_directory);
}
const char *Command_getDir(Command_T C) {
assert(C);
return C->working_directory;
}
/* Env variables are stored in the environment list as "name=value" strings */
void Command_setEnv(Command_T C, const char *name, const char *value) {
assert(C);
assert(name);
removeEnv(C, name);
List_append(C->env, Str_cat("%s=%s", name, value ? value : ""));
}
void Command_vSetEnv(T C, const char *env, ...) {
assert(C);
if (STR_DEF(env)) {
va_list ap;
char *s, *n, *v, *t;
va_start(ap, env);
n = s = Str_vcat(env, ap);
va_end(ap);
while ((v = strchr(n, '='))) {
*(v++) = 0;
t = strchr(v, ';');
if (t)
*(t++) = 0;
Command_setEnv(C, Str_trim(n), Str_trim(v));
if (t)
n = t;
else
break;
}
FREE(s);
}
}
/* Returns the value part from a "name=value" environment string */
const char *Command_getEnv(Command_T C, const char *name) {
assert(C);
assert(name);
char *e = findEnv(C, name);
if (e) {
char *v = strchr(e, '=');
if (v)
return ++v;
}
return NULL;
}
List_T Command_getCommand(T C) {
assert(C);
return C->args;
}
/* The Execute function. Note that we use vfork() rather than fork. Vfork has
a special semantic in that the child process runs in the parent address space
until exec is called in the child. The child also run first and suspend the
parent process until exec or exit is called */
Process_T Command_execute(T C) {
assert(C);
volatile int exec_error = 0;
Process_T P = Process_new();
P->env = (char**)List_toArray(C->env);
P->args = (char**)List_toArray(C->args);
createPipes(P);
if ((P->pid = vfork()) < 0) {
ERROR("Command: fork failed -- %s\n", System_getLastError());
Process_free(&P);
return NULL;
}
// Child
else if (P->pid == 0) {
if (C->working_directory) {
if (! Dir_chdir(C->working_directory)) {
exec_error = errno;
ERROR("Command: sub-process cannot change working directory to '%s' -- %s\n", C->working_directory, System_getLastError());
_exit(errno);
}
}
if (C->uid)
P->uid = (setuid(C->uid) != 0) ? ERROR("Command: Cannot change process uid to '%d' -- %s\n", C->uid, System_getLastError()), getuid() : C->uid;
else
P->uid = getuid();
if (C->gid)
P->gid = (setgid(C->gid) != 0) ? ERROR("Command: Cannot change process gid to '%d' -- %s\n", C->gid, System_getLastError()), getgid() : C->gid;
else
P->gid = getgid();
setsid(); // Loose controlling terminal
setupChildPipes(P);
// Close all descriptors except stdio
int descriptors = getdtablesize();
for (int i = 3; i < descriptors; i++)
close(i);
// Unblock any signals and reset signal handlers
sigset_t mask;
sigemptyset(&mask);
pthread_sigmask(SIG_SETMASK, &mask, NULL);
signal(SIGINT, SIG_DFL);
signal(SIGQUIT, SIG_DFL);
signal(SIGABRT, SIG_DFL);
signal(SIGTERM, SIG_DFL);
signal(SIGPIPE, SIG_DFL);
signal(SIGCHLD, SIG_DFL);
signal(SIGHUP, SIG_IGN); // Ensure future opens won't allocate controlling TTYs
// Execute the program
execve(P->args[0], P->args, P->env);
exec_error = errno;
_exit(errno);
}
// Parent
if (exec_error != 0)
Process_free(&P);
else
setupParentPipes(P);
errno = exec_error;
return P;
}