782 lines
26 KiB
C++
782 lines
26 KiB
C++
/*
|
|
* Copyright (C) 2011 Intel Corporation
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) version 3.
|
|
*
|
|
* This library 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
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
* 02110-1301 USA
|
|
*/
|
|
|
|
#include "ForkExec.h"
|
|
#include <syncevo/LogRedirect.h>
|
|
#include <syncevo/ThreadSupport.h>
|
|
|
|
#if defined(HAVE_GLIB)
|
|
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
|
|
#include <pcrecpp.h>
|
|
#include <ctype.h>
|
|
#include "test.h"
|
|
|
|
SE_BEGIN_CXX
|
|
|
|
static const std::string ForkExecEnvVar("SYNCEVOLUTION_FORK_EXEC=");
|
|
static const std::string ForkExecInstanceEnvVar("SYNCEVOLUTION_FORK_EXEC_INSTANCE=");
|
|
|
|
#ifndef GDBUS_CXX_HAVE_DISCONNECT
|
|
// internal D-Bus API: only used to monitor parent by having one method call pending
|
|
static const std::string FORKEXEC_PARENT_PATH("/org/syncevolution/forkexec/parent");
|
|
static const std::string FORKEXEC_PARENT_IFACE("org.syncevolution.forkexec.parent");
|
|
static const std::string FORKEXEC_PARENT_DESTINATION = "direct.peer"; // doesn't matter, routing is off
|
|
|
|
/**
|
|
* The only purpose is to accept method calls and never reply.
|
|
* When the parent destructs or gets killed, the caller (= child)
|
|
* will notice because the method call fails, which ForkExecChild
|
|
* translates into a "parent died" signal.
|
|
*/
|
|
class ForkExecParentDBusAPI : public GDBusCXX::DBusObjectHelper
|
|
{
|
|
public:
|
|
/**
|
|
* @param instance a unique string to distinguish multiple different ForkExecParent
|
|
* instances; necessary because otherwise GIO GDBus may route messages from
|
|
* one connection to older instances on other connections
|
|
*/
|
|
ForkExecParentDBusAPI(const GDBusCXX::DBusConnectionPtr &conn, const std::string &instance) :
|
|
GDBusCXX::DBusObjectHelper(conn,
|
|
FORKEXEC_PARENT_PATH + "/" + instance,
|
|
FORKEXEC_PARENT_IFACE)
|
|
{
|
|
add(this, &ForkExecParentDBusAPI::watch, "Watch");
|
|
activate();
|
|
}
|
|
|
|
~ForkExecParentDBusAPI()
|
|
{
|
|
SE_LOG_DEBUG(NULL, "ForkExecParentDBusAPI %s: destroying with %ld active watches",
|
|
getPath(),
|
|
(long)m_watches.size());
|
|
}
|
|
|
|
bool hasWatches() const { return !m_watches.empty(); }
|
|
|
|
private:
|
|
void watch(const boost::shared_ptr< GDBusCXX::Result0> &result)
|
|
{
|
|
SE_LOG_DEBUG(NULL, "ForkExecParentDBusAPI %s: received 'Watch' method call from child",
|
|
getPath());
|
|
m_watches.push_back(result);
|
|
}
|
|
std::list< boost::shared_ptr< GDBusCXX::Result0> > m_watches;
|
|
};
|
|
#endif // GDBUS_CXX_HAVE_DISCONNECT
|
|
|
|
ForkExec::ForkExec()
|
|
{
|
|
}
|
|
|
|
static Mutex ForkExecMutex;
|
|
static unsigned int ForkExecCount;
|
|
|
|
ForkExecParent::ForkExecParent(const std::string &helper, const std::vector<std::string> &args) :
|
|
m_helper(helper),
|
|
m_args(args),
|
|
m_childPid(0),
|
|
m_hasConnected(false),
|
|
m_hasQuit(false),
|
|
m_status(0),
|
|
m_sigIntSent(false),
|
|
m_sigTermSent(false),
|
|
m_mergedStdoutStderr(false),
|
|
m_out(NULL),
|
|
m_err(NULL),
|
|
m_outID(0),
|
|
m_errID(0),
|
|
m_watchChild(NULL)
|
|
{
|
|
Mutex::Guard guard = ForkExecMutex.lock();
|
|
ForkExecCount++;
|
|
m_instance = StringPrintf("forkexec%u", ForkExecCount);
|
|
}
|
|
|
|
boost::shared_ptr<ForkExecParent> ForkExecParent::create(const std::string &helper,
|
|
const std::vector<std::string> &args)
|
|
{
|
|
boost::shared_ptr<ForkExecParent> forkexec(new ForkExecParent(helper, args));
|
|
return forkexec;
|
|
}
|
|
|
|
ForkExecParent::~ForkExecParent()
|
|
{
|
|
if (m_outID) {
|
|
g_source_remove(m_outID);
|
|
}
|
|
if (m_errID) {
|
|
g_source_remove(m_errID);
|
|
}
|
|
if (m_out) {
|
|
g_io_channel_unref(m_out);
|
|
}
|
|
if (m_err) {
|
|
g_io_channel_unref(m_err);
|
|
}
|
|
if (m_watchChild) {
|
|
// stop watching
|
|
g_source_destroy(m_watchChild);
|
|
g_source_unref(m_watchChild);
|
|
}
|
|
if (m_childPid) {
|
|
g_spawn_close_pid(m_childPid);
|
|
}
|
|
#ifndef GDBUS_CXX_HAVE_DISCONNECT
|
|
if (m_api) {
|
|
SE_LOG_DEBUG(NULL, "ForkExecParent: shutting down, telling %s %ld that it lost the connection, it %s",
|
|
m_helper.c_str(),
|
|
(long)m_childPid,
|
|
m_api->hasWatches() ? "is watching" : "is not watching");
|
|
m_api.reset();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* Redirect stdout to stderr.
|
|
*
|
|
* Child setup function, called insided forked process before exec().
|
|
* only async-signal-safe functions allowed according to http://developer.gnome.org/glib/2.30/glib-Spawning-Processes.html#GSpawnChildSetupFunc
|
|
*/
|
|
void ForkExecParent::forked(gpointer data) throw()
|
|
{
|
|
ForkExecParent *me = static_cast<ForkExecParent *>(data);
|
|
|
|
// When debugging, undo the LogRedirect output redirection that
|
|
// we inherited from the parent process. That ensures that
|
|
// any output is printed directly, instead of going through
|
|
// the parent's output processing in LogRedirect.
|
|
if (getenv("SYNCEVOLUTION_DEBUG")) {
|
|
LogRedirect::removeRedirect();
|
|
}
|
|
|
|
if (me->m_mergedStdoutStderr) {
|
|
dup2(STDERR_FILENO, STDOUT_FILENO);
|
|
}
|
|
}
|
|
|
|
void ForkExecParent::start()
|
|
{
|
|
if (m_watchChild) {
|
|
SE_THROW("child already started");
|
|
}
|
|
|
|
// boost::shared_ptr<ForkExecParent> me = ...;
|
|
GDBusCXX::DBusErrorCXX dbusError;
|
|
|
|
SE_LOG_DEBUG(NULL, "ForkExecParent: preparing for child process %s", m_helper.c_str());
|
|
m_server = GDBusCXX::DBusServerCXX::listen(boost::bind(&ForkExecParent::newClientConnection, this, _2), &dbusError);
|
|
if (!m_server) {
|
|
dbusError.throwFailure("starting server");
|
|
}
|
|
|
|
// look for helper binary
|
|
std::string helper;
|
|
GSpawnFlags flags = G_SPAWN_DO_NOT_REAP_CHILD;
|
|
if (m_helper.find('/') == m_helper.npos) {
|
|
helper = getEnv("SYNCEVOLUTION_LIBEXEC_DIR", "");
|
|
if (helper.empty()) {
|
|
// env variable not set, look in libexec dir
|
|
helper = SYNCEVO_LIBEXEC;
|
|
helper += "/";
|
|
helper += m_helper;
|
|
if (access(helper.c_str(), R_OK)) {
|
|
// some error, try PATH
|
|
flags = (GSpawnFlags)(flags | G_SPAWN_SEARCH_PATH);
|
|
helper = m_helper;
|
|
}
|
|
} else {
|
|
// use env variable without further checks, must work
|
|
helper += "/";
|
|
helper += m_helper;
|
|
}
|
|
} else {
|
|
// absolute path, use it
|
|
helper = m_helper;
|
|
}
|
|
|
|
m_argvStrings.push_back(helper);
|
|
m_argvStrings.insert(m_argvStrings.end(),
|
|
m_args.begin(),
|
|
m_args.end());
|
|
m_argv.reset(AllocStringArray(m_argvStrings));
|
|
for (char **env = environ;
|
|
*env;
|
|
env++) {
|
|
if (!boost::starts_with(*env, ForkExecEnvVar) &&
|
|
!boost::starts_with(*env, ForkExecInstanceEnvVar)) {
|
|
m_envStrings.push_back(*env);
|
|
}
|
|
}
|
|
|
|
// pass D-Bus address via env variable
|
|
m_envStrings.push_back(ForkExecEnvVar + m_server->getAddress());
|
|
m_envStrings.push_back(ForkExecInstanceEnvVar + getInstance());
|
|
m_env.reset(AllocStringArray(m_envStrings));
|
|
|
|
SE_LOG_DEBUG(NULL, "ForkExecParent: running %s with D-Bus address %s",
|
|
helper.c_str(), m_server->getAddress().c_str());
|
|
|
|
// Check which kind of output redirection is wanted.
|
|
m_mergedStdoutStderr = !m_onOutput.empty();
|
|
if (!m_onOutput.empty()) {
|
|
m_mergedStdoutStderr = true;
|
|
}
|
|
|
|
GErrorCXX gerror;
|
|
int err = -1, out = -1;
|
|
if (!g_spawn_async_with_pipes(NULL, // working directory
|
|
static_cast<gchar **>(m_argv.get()),
|
|
static_cast<gchar **>(m_env.get()),
|
|
(GSpawnFlags)(flags | G_SPAWN_LEAVE_DESCRIPTORS_OPEN),
|
|
// child setup function: redirect stdout to stderr, undo LogRedirect
|
|
forked, this,
|
|
&m_childPid,
|
|
NULL, // set stdin to /dev/null
|
|
(m_mergedStdoutStderr || m_onStdout.empty()) ? NULL : &out,
|
|
(m_mergedStdoutStderr || !m_onStderr.empty()) ? &err : NULL,
|
|
gerror)) {
|
|
m_childPid = 0;
|
|
gerror.throwError(SE_HERE, "spawning child");
|
|
}
|
|
// set up output redirection, ignoring failures
|
|
setupPipe(m_err, m_errID, err);
|
|
setupPipe(m_out, m_outID, out);
|
|
|
|
SE_LOG_DEBUG(NULL, "ForkExecParent: child process for %s has pid %ld",
|
|
helper.c_str(), (long)m_childPid);
|
|
|
|
// TODO: introduce C++ wrapper around GSource
|
|
m_watchChild = g_child_watch_source_new(m_childPid);
|
|
g_source_set_callback(m_watchChild, (GSourceFunc)watchChildCallback, this, NULL);
|
|
g_source_attach(m_watchChild, NULL);
|
|
}
|
|
|
|
void ForkExecParent::setupPipe(GIOChannel *&channel, guint &sourceID, int fd)
|
|
{
|
|
if (fd == -1) {
|
|
// nop
|
|
return;
|
|
}
|
|
|
|
// Other program executed by us shall not inherit a copy of this
|
|
// file descriptor.
|
|
fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC);
|
|
|
|
channel = g_io_channel_unix_new(fd);
|
|
if (!channel) {
|
|
// failure
|
|
SE_LOG_DEBUG(NULL, "g_io_channel_unix_new() returned NULL");
|
|
close(fd);
|
|
return;
|
|
}
|
|
// Close fd when freeing the channel (done by caller).
|
|
g_io_channel_set_close_on_unref(channel, true);
|
|
// Don't block in outputReady().
|
|
GErrorCXX error;
|
|
g_io_channel_set_flags(channel, G_IO_FLAG_NONBLOCK, error);
|
|
// We assume that the helper is writing data in the same encoding
|
|
// and thus avoid any kind of conversion. Necessary to avoid
|
|
// buffering.
|
|
error.clear();
|
|
g_io_channel_set_encoding(channel, NULL, error);
|
|
g_io_channel_set_buffered(channel, true);
|
|
sourceID = g_io_add_watch(channel, (GIOCondition)(G_IO_IN|G_IO_ERR|G_IO_HUP), outputReady, this);
|
|
}
|
|
|
|
gboolean ForkExecParent::outputReady(GIOChannel *source,
|
|
GIOCondition condition,
|
|
gpointer data) throw ()
|
|
{
|
|
bool cont = true;
|
|
|
|
try {
|
|
ForkExecParent *me = static_cast<ForkExecParent *>(data);
|
|
gchar *buffer = NULL;
|
|
gsize length = 0;
|
|
GErrorCXX error;
|
|
// Try reading, even if the condition wasn't G_IO_IN.
|
|
GIOStatus status = g_io_channel_read_to_end(source, &buffer, &length, error);
|
|
if (buffer && length) {
|
|
if (source == me->m_out) {
|
|
me->m_onStdout(buffer, length);
|
|
} else if (me->m_mergedStdoutStderr) {
|
|
me->m_onOutput(buffer, length);
|
|
} else {
|
|
me->m_onStderr(buffer, length);
|
|
}
|
|
}
|
|
if (status == G_IO_STATUS_EOF ||
|
|
(condition & (G_IO_HUP|G_IO_ERR)) ||
|
|
error) {
|
|
SE_LOG_DEBUG(NULL, "reading helper %s %ld done: %s",
|
|
source == me->m_out ? "stdout" :
|
|
me->m_mergedStdoutStderr ? "combined stdout/stderr" :
|
|
"stderr",
|
|
(long)me->m_childPid,
|
|
(const char *)error);
|
|
|
|
// Will remove event source from main loop.
|
|
cont = false;
|
|
|
|
// Free channel and forget source tag (source will be freed
|
|
// by caller when we return false).
|
|
if (source == me->m_out) {
|
|
me->m_out = NULL;
|
|
me->m_outID = 0;
|
|
} else {
|
|
me->m_err = NULL;
|
|
me->m_errID = 0;
|
|
}
|
|
g_io_channel_unref(source);
|
|
|
|
// Send delayed OnQuit signal now?
|
|
me->checkCompletion();
|
|
}
|
|
// If an exception skips this, we are going to die, in
|
|
// which case we don't care about the leak.
|
|
g_free(buffer);
|
|
} catch (...) {
|
|
Exception::handle(HANDLE_EXCEPTION_FATAL);
|
|
}
|
|
|
|
return cont;
|
|
}
|
|
|
|
void ForkExecParent::watchChildCallback(GPid pid,
|
|
gint status,
|
|
gpointer data) throw()
|
|
{
|
|
ForkExecParent *me = static_cast<ForkExecParent *>(data);
|
|
me->m_hasQuit = true;
|
|
me->m_status = status;
|
|
me->checkCompletion();
|
|
}
|
|
|
|
void ForkExecParent::checkCompletion() throw ()
|
|
{
|
|
if (m_hasQuit &&
|
|
!m_out &&
|
|
!m_err) {
|
|
try {
|
|
m_onQuit(m_status);
|
|
if (!m_hasConnected ||
|
|
m_status != 0) {
|
|
SE_LOG_DEBUG(NULL, "ForkExecParent: child %ld was signaled %s, signal %d (SIGINT=%d, SIGTERM=%d), int sent %s, term sent %s",
|
|
(long)m_childPid,
|
|
WIFSIGNALED(m_status) ? "yes" : "no",
|
|
WTERMSIG(m_status), SIGINT, SIGTERM,
|
|
m_sigIntSent ? "yes" : "no",
|
|
m_sigTermSent ? "yes" : "no");
|
|
if (WIFSIGNALED(m_status) &&
|
|
((WTERMSIG(m_status) == SIGINT && m_sigIntSent) ||
|
|
(WTERMSIG(m_status) == SIGTERM && m_sigTermSent))) {
|
|
// not an error when the child dies because we killed it
|
|
return;
|
|
}
|
|
if (WIFSIGNALED(m_status) &&
|
|
WTERMSIG(m_status) == SIGKILL &&
|
|
m_sigTermSent) {
|
|
// This started to happen on Debian Testing after the Wheezy release:
|
|
// everything seems to shut down normally, and yet the exit status
|
|
// of the helper shows SIGKILL instead of SIGTERM as the reason for
|
|
// quitting. valgrind is involved, too. Not sure where this new (?)
|
|
// behavior comes from. It seems to be harmless, so accept that
|
|
// additional exit code without complaining (which would break unit
|
|
// testing).
|
|
SE_LOG_DEBUG(NULL, "ForkExecParent: ignoring unexpected exit signal SIGKILL of child %ld", (long)m_childPid);
|
|
return;
|
|
}
|
|
std::string error = "child process quit";
|
|
if (!m_hasConnected) {
|
|
error += " unexpectedly";
|
|
}
|
|
if (WIFEXITED(m_status)) {
|
|
error += StringPrintf(" with return code %d", WEXITSTATUS(m_status));
|
|
} else if (WIFSIGNALED(m_status)) {
|
|
error += StringPrintf(" because of signal %d", WTERMSIG(m_status));
|
|
} else {
|
|
error += " for unknown reasons";
|
|
}
|
|
SE_LOG_ERROR(NULL, "%s", error.c_str());
|
|
m_onFailure(STATUS_FATAL, error);
|
|
}
|
|
} catch (...) {
|
|
std::string explanation;
|
|
SyncMLStatus status = Exception::handle(explanation);
|
|
try {
|
|
m_onFailure(status, explanation);
|
|
} catch (...) {
|
|
Exception::handle();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ForkExecParent::newClientConnection(GDBusCXX::DBusConnectionPtr &conn) throw()
|
|
{
|
|
try {
|
|
SE_LOG_DEBUG(NULL, "ForkExecParent: child %s %ld has connected",
|
|
m_helper.c_str(),
|
|
(long)m_childPid);
|
|
m_hasConnected = true;
|
|
#ifndef GDBUS_CXX_HAVE_DISCONNECT
|
|
m_api.reset(new ForkExecParentDBusAPI(conn, getInstance()));
|
|
#endif
|
|
m_onConnect(conn);
|
|
dbus_bus_connection_undelay(conn);
|
|
} catch (...) {
|
|
std::string explanation;
|
|
SyncMLStatus status = Exception::handle(explanation);
|
|
try {
|
|
m_onFailure(status, explanation);
|
|
} catch (...) {
|
|
Exception::handle();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ForkExecParent::addEnvVar(const std::string &name, const std::string &value)
|
|
{
|
|
if(!name.empty()) {
|
|
m_envStrings.push_back(name + "=" + value);
|
|
}
|
|
}
|
|
|
|
void ForkExecParent::stop(int signal)
|
|
{
|
|
if (!m_childPid || m_hasQuit) {
|
|
// not running, nop
|
|
return;
|
|
}
|
|
|
|
SE_LOG_DEBUG(NULL, "ForkExecParent: killing %s %ld with signal %d (%s %s)",
|
|
m_helper.c_str(),
|
|
(long)m_childPid,
|
|
signal,
|
|
(!signal || signal == SIGINT) ? "SIGINT" : "",
|
|
(!signal || signal == SIGTERM) ? "SIGTERM" : "");
|
|
if (!signal || signal == SIGINT) {
|
|
::kill(m_childPid, SIGINT);
|
|
m_sigIntSent = true;
|
|
}
|
|
if (!signal || signal == SIGTERM) {
|
|
::kill(m_childPid, SIGTERM);
|
|
m_sigTermSent = true;
|
|
}
|
|
if (signal && signal != SIGINT && signal != SIGTERM) {
|
|
::kill(m_childPid, signal);
|
|
}
|
|
}
|
|
|
|
void ForkExecParent::kill()
|
|
{
|
|
if (!m_childPid || m_hasQuit) {
|
|
// not running, nop
|
|
return;
|
|
}
|
|
|
|
SE_LOG_DEBUG(NULL, "ForkExecParent: killing %s %ld with SIGKILL",
|
|
m_helper.c_str(),
|
|
(long)m_childPid);
|
|
::kill(m_childPid, SIGKILL);
|
|
#ifndef GDBUS_CXX_HAVE_DISCONNECT
|
|
// Cancel the pending method call from the child to us. This will
|
|
// send an error reply to the child, which it'll treat as
|
|
// "connection lost".
|
|
if (m_api) {
|
|
SE_LOG_DEBUG(NULL, "ForkExecParent: telling %s %ld that it lost the connection, it %s",
|
|
m_helper.c_str(),
|
|
(long)m_childPid,
|
|
m_api->hasWatches() ? "is watching" : "is not watching");
|
|
m_api.reset();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
ForkExecChild::ForkExecChild() :
|
|
m_state(IDLE)
|
|
{
|
|
m_instance = getEnv(ForkExecInstanceEnvVar.substr(0, ForkExecInstanceEnvVar.size() - 1).c_str(), "");
|
|
}
|
|
|
|
boost::shared_ptr<ForkExecChild> ForkExecChild::create()
|
|
{
|
|
boost::shared_ptr<ForkExecChild> forkexec(new ForkExecChild);
|
|
return forkexec;
|
|
}
|
|
|
|
void ForkExecChild::connect()
|
|
{
|
|
// set error state, clear it later
|
|
m_state = DISCONNECTED;
|
|
|
|
const char *address = getParentDBusAddress();
|
|
if (!address) {
|
|
SE_THROW("cannot connect to parent, was not forked");
|
|
}
|
|
|
|
SE_LOG_DEBUG(NULL, "ForkExecChild: connecting to parent with D-Bus address %s",
|
|
address);
|
|
GDBusCXX::DBusErrorCXX dbusError;
|
|
GDBusCXX::DBusConnectionPtr conn = dbus_get_bus_connection(address,
|
|
&dbusError);
|
|
if (!conn) {
|
|
dbusError.throwFailure("connecting to server");
|
|
}
|
|
|
|
m_state = CONNECTED;
|
|
|
|
// start watching connection
|
|
#ifdef GDBUS_CXX_HAVE_DISCONNECT
|
|
conn.setDisconnect(boost::bind(&ForkExecChild::connectionLost, this));
|
|
#else
|
|
// emulate disconnect with a pending method call
|
|
class Parent : public GDBusCXX::DBusRemoteObject
|
|
{
|
|
public:
|
|
Parent(const GDBusCXX::DBusConnectionPtr &conn, const std::string &instance) :
|
|
GDBusCXX::DBusRemoteObject(conn,
|
|
FORKEXEC_PARENT_PATH + "/" + instance,
|
|
FORKEXEC_PARENT_IFACE,
|
|
FORKEXEC_PARENT_DESTINATION),
|
|
m_watch(*this, "Watch")
|
|
{}
|
|
|
|
GDBusCXX::DBusClientCall0 m_watch;
|
|
} parent(conn, getInstance());
|
|
parent.m_watch.start(boost::bind(&ForkExecChild::connectionLost, this));
|
|
#endif
|
|
|
|
m_onConnect(conn);
|
|
dbus_bus_connection_undelay(conn);
|
|
}
|
|
|
|
void ForkExecChild::connectionLost()
|
|
{
|
|
SE_LOG_DEBUG(NULL, "lost connection to parent");
|
|
m_state = DISCONNECTED;
|
|
m_onQuit();
|
|
}
|
|
|
|
bool ForkExecChild::wasForked()
|
|
{
|
|
return getParentDBusAddress() != NULL;
|
|
}
|
|
|
|
const char *ForkExecChild::getParentDBusAddress()
|
|
{
|
|
return getenv(ForkExecEnvVar.substr(0, ForkExecEnvVar.size() - 1).c_str());
|
|
}
|
|
|
|
#ifdef ENABLE_UNIT_TESTS
|
|
/**
|
|
* Assumes that /bin/[false/true/echo] exist and that "env" is in the
|
|
* path. Currently this does not cover actual D-Bus connection
|
|
* handling and usage.
|
|
*/
|
|
class ForkExecTest : public CppUnit::TestFixture
|
|
{
|
|
public:
|
|
void setUp()
|
|
{
|
|
m_statusValid = false;
|
|
m_status = 0;
|
|
}
|
|
|
|
private:
|
|
CPPUNIT_TEST_SUITE(ForkExecTest);
|
|
CPPUNIT_TEST(testTrue);
|
|
CPPUNIT_TEST(testFalse);
|
|
CPPUNIT_TEST(testPath);
|
|
CPPUNIT_TEST(testNotFound);
|
|
CPPUNIT_TEST(testEnv1);
|
|
CPPUNIT_TEST(testEnv2);
|
|
CPPUNIT_TEST(testOutErr);
|
|
CPPUNIT_TEST(testMerged);
|
|
CPPUNIT_TEST_SUITE_END();
|
|
|
|
bool m_statusValid;
|
|
int m_status;
|
|
|
|
void hasQuit(int status)
|
|
{
|
|
m_status = status;
|
|
m_statusValid = true;
|
|
}
|
|
|
|
static void append(const char *buffer, size_t length, std::string &all)
|
|
{
|
|
all.append(buffer, length);
|
|
}
|
|
|
|
boost::shared_ptr<ForkExecParent> create(const std::string &helper)
|
|
{
|
|
boost::shared_ptr<ForkExecParent> parent(ForkExecParent::create(helper));
|
|
parent->m_onQuit.connect(boost::bind(&ForkExecTest::hasQuit, this, _1));
|
|
return parent;
|
|
}
|
|
|
|
void testTrue()
|
|
{
|
|
boost::shared_ptr<ForkExecParent> parent(create("/bin/true"));
|
|
parent->start();
|
|
while (!m_statusValid) {
|
|
g_main_context_iteration(NULL, true);
|
|
}
|
|
CPPUNIT_ASSERT(WIFEXITED(m_status));
|
|
CPPUNIT_ASSERT_EQUAL(0, WEXITSTATUS(m_status));
|
|
}
|
|
|
|
void testFalse()
|
|
{
|
|
boost::shared_ptr<ForkExecParent> parent(create("/bin/false"));
|
|
parent->start();
|
|
while (!m_statusValid) {
|
|
g_main_context_iteration(NULL, true);
|
|
}
|
|
CPPUNIT_ASSERT(WIFEXITED(m_status));
|
|
CPPUNIT_ASSERT_EQUAL(1, WEXITSTATUS(m_status));
|
|
}
|
|
|
|
void testPath()
|
|
{
|
|
boost::shared_ptr<ForkExecParent> parent(create("true"));
|
|
parent->start();
|
|
while (!m_statusValid) {
|
|
g_main_context_iteration(NULL, true);
|
|
}
|
|
CPPUNIT_ASSERT(WIFEXITED(m_status));
|
|
CPPUNIT_ASSERT_EQUAL(0, WEXITSTATUS(m_status));
|
|
}
|
|
|
|
void testNotFound()
|
|
{
|
|
boost::shared_ptr<ForkExecParent> parent(create("no-such-binary"));
|
|
std::string out;
|
|
std::string err;
|
|
parent->m_onStdout.connect(boost::bind(append, _1, _2, boost::ref(out)));
|
|
parent->m_onStderr.connect(boost::bind(append, _1, _2, boost::ref(err)));
|
|
try {
|
|
parent->start();
|
|
} catch (const SyncEvo::Exception &ex) {
|
|
if (strstr(ex.what(), "spawning child: ")) {
|
|
// glib itself detected that binary wasn't found. This
|
|
// is what normally happens, but there's no guarantee,
|
|
// thus the code below...
|
|
return;
|
|
}
|
|
throw;
|
|
}
|
|
while (!m_statusValid) {
|
|
g_main_context_iteration(NULL, true);
|
|
}
|
|
CPPUNIT_ASSERT(WIFEXITED(m_status));
|
|
CPPUNIT_ASSERT_EQUAL(1, WEXITSTATUS(m_status));
|
|
CPPUNIT_ASSERT_EQUAL(std::string(""), out);
|
|
CPPUNIT_ASSERT_MESSAGE(err, err.find("no-such-binary") != err.npos);
|
|
}
|
|
|
|
void testEnv1()
|
|
{
|
|
boost::shared_ptr<ForkExecParent> parent(create("env"));
|
|
parent->addEnvVar("FORK_EXEC_TEST_ENV", "foobar");
|
|
std::string out;
|
|
parent->m_onStdout.connect(boost::bind(append, _1, _2, boost::ref(out)));
|
|
parent->start();
|
|
while (!m_statusValid) {
|
|
g_main_context_iteration(NULL, true);
|
|
}
|
|
CPPUNIT_ASSERT(WIFEXITED(m_status));
|
|
CPPUNIT_ASSERT_EQUAL(0, WEXITSTATUS(m_status));
|
|
CPPUNIT_ASSERT_MESSAGE(out, out.find("FORK_EXEC_TEST_ENV=foobar\n") != out.npos);
|
|
}
|
|
|
|
void testEnv2()
|
|
{
|
|
boost::shared_ptr<ForkExecParent> parent(create("env"));
|
|
parent->addEnvVar("FORK_EXEC_TEST_ENV1", "foo");
|
|
parent->addEnvVar("FORK_EXEC_TEST_ENV2", "bar");
|
|
std::string out;
|
|
parent->m_onStdout.connect(boost::bind(append, _1, _2, boost::ref(out)));
|
|
parent->start();
|
|
while (!m_statusValid) {
|
|
g_main_context_iteration(NULL, true);
|
|
}
|
|
CPPUNIT_ASSERT(WIFEXITED(m_status));
|
|
CPPUNIT_ASSERT_EQUAL(0, WEXITSTATUS(m_status));
|
|
CPPUNIT_ASSERT_MESSAGE(out, out.find("FORK_EXEC_TEST_ENV1=foo\n") != out.npos);
|
|
CPPUNIT_ASSERT_MESSAGE(out, out.find("FORK_EXEC_TEST_ENV2=bar\n") != out.npos);
|
|
}
|
|
|
|
void testOutErr()
|
|
{
|
|
// This tests uses a trick to get output via stdout (normal
|
|
// env output) and stderr (from ld.so).
|
|
boost::shared_ptr<ForkExecParent> parent(create("env"));
|
|
parent->addEnvVar("FORK_EXEC_TEST_ENV", "foobar");
|
|
parent->addEnvVar("LD_DEBUG", "files");
|
|
|
|
std::string out;
|
|
std::string err;
|
|
parent->m_onStdout.connect(boost::bind(append, _1, _2, boost::ref(out)));
|
|
parent->m_onStderr.connect(boost::bind(append, _1, _2, boost::ref(err)));
|
|
parent->start();
|
|
while (!m_statusValid) {
|
|
g_main_context_iteration(NULL, true);
|
|
}
|
|
CPPUNIT_ASSERT(WIFEXITED(m_status));
|
|
CPPUNIT_ASSERT_EQUAL(0, WEXITSTATUS(m_status));
|
|
CPPUNIT_ASSERT_MESSAGE(out, out.find("FORK_EXEC_TEST_ENV=foobar\n") != out.npos);
|
|
CPPUNIT_ASSERT_MESSAGE(err, err.find("transferring control: ") != err.npos);
|
|
}
|
|
|
|
void testMerged()
|
|
{
|
|
// This tests uses a trick to get output via stdout (normal
|
|
// env output) and stderr (from ld.so).
|
|
boost::shared_ptr<ForkExecParent> parent(create("env"));
|
|
parent->addEnvVar("FORK_EXEC_TEST_ENV", "foobar");
|
|
parent->addEnvVar("LD_DEBUG", "files");
|
|
|
|
std::string output;
|
|
parent->m_onOutput.connect(boost::bind(append, _1, _2, boost::ref(output)));
|
|
parent->start();
|
|
while (!m_statusValid) {
|
|
g_main_context_iteration(NULL, true);
|
|
}
|
|
CPPUNIT_ASSERT(WIFEXITED(m_status));
|
|
CPPUNIT_ASSERT_EQUAL(0, WEXITSTATUS(m_status));
|
|
// output from ld.so directly followed by env output
|
|
CPPUNIT_ASSERT_MESSAGE(output,
|
|
pcrecpp::RE("transferring control:.*\\n(\\s+\\d+:.*\\n)*[A-Za-z0-9_]+=.*\\n").PartialMatch(output));
|
|
}
|
|
};
|
|
SYNCEVOLUTION_TEST_SUITE_REGISTRATION(ForkExecTest);
|
|
|
|
#endif
|
|
|
|
SE_END_CXX
|
|
|
|
#endif // HAVE_GLIB
|