summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorYaniv Bronhaim <ybronhei@redhat.com>2013-01-21 15:28:02 +0200
committerYaniv Bronhaim <ybronhei@redhat.com>2013-01-21 15:28:02 +0200
commit7bd79cee2b5fd8555e0aa256f8ca5531bd52ada7 (patch)
treebafc188b374aea7b0736b018082bde01085081f2
downloadcpopen-7bd79cee2b5fd8555e0aa256f8ca5531bd52ada7.tar.gz
cpopen-7bd79cee2b5fd8555e0aa256f8ca5531bd52ada7.tar.xz
cpopen-7bd79cee2b5fd8555e0aa256f8ca5531bd52ada7.zip
Initial commit
Signed-off-by: Yaniv Bronhaim <ybronhei@redhat.com>
-rw-r--r--cpopen.py73
-rw-r--r--cpopen.spec35
-rw-r--r--createprocess.c329
-rw-r--r--setup.py13
4 files changed, 450 insertions, 0 deletions
diff --git a/cpopen.py b/cpopen.py
new file mode 100644
index 0000000..2e5fbd6
--- /dev/null
+++ b/cpopen.py
@@ -0,0 +1,73 @@
+#
+# Copyright 2012 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Refer to the README and COPYING files for full details of the license
+#
+
+"""
+Python's implementation of Popen forks back to python before execing.
+Forking a python proc is a very complex and volatile process.
+
+This is a simpler method of execing that doesn't go back to python after
+forking. This allows for faster safer exec.
+"""
+
+import os
+from subprocess import Popen, PIPE
+
+from createprocess import createProcess
+
+
+class cpopen(Popen):
+ def __init__(self, args, close_fds=False, cwd=None, env=None):
+ if not isinstance(args, list):
+ args = list(args)
+
+ if env is not None and not isinstance(env, list):
+ env = list(("=".join(item) for item in env.iteritems()))
+
+ Popen.__init__(self, args,
+ close_fds=close_fds, cwd=cwd, env=env,
+ stdin=PIPE, stdout=PIPE,
+ stderr=PIPE)
+
+ def _execute_child(self, args, executable, preexec_fn, close_fds,
+ cwd, env, universal_newlines,
+ startupinfo, creationflags, shell,
+ p2cread, p2cwrite,
+ c2pread, c2pwrite,
+ errread, errwrite):
+
+ try:
+ pid, stdin, stdout, stderr = createProcess(args, close_fds,
+ p2cread, p2cwrite,
+ c2pread, c2pwrite,
+ errread, errwrite,
+ cwd, env)
+
+ self.pid = pid
+ self._closed = False
+ self._returncode = None
+ except:
+ os.close(p2cwrite)
+ os.close(errread)
+ os.close(c2pread)
+ raise
+ finally:
+ os.close(p2cread)
+ os.close(errwrite)
+ os.close(c2pwrite)
diff --git a/cpopen.spec b/cpopen.spec
new file mode 100644
index 0000000..83fe09d
--- /dev/null
+++ b/cpopen.spec
@@ -0,0 +1,35 @@
+%{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")}
+%{!?python_ver: %global python_ver %(%{__python} -c "import sys ; print sys.version[:3]")}
+
+Name: python-cpopen
+Version: 1.0
+Release: 1%{?dist}
+Summary: Creates a subprocess in simpler safer manner
+
+License: GPLv2+
+Group: System Environment/Libraries
+URL: http://pypi.python.org/pypi/cpopen
+Source0: http://pypi.python.org/packages/source/c/cpopen/cpopen-%{version}.tar.gz
+
+BuildRequires: python2
+
+%description
+Python package for creating subprocess in simpler and safer manner by using C code.
+
+%prep
+%setup -q -n cpopen-%{version}
+
+%build
+
+
+%install
+%{__python} setup.py install --root $RPM_BUILD_ROOT --install-lib %{python_sitelib}
+
+%files
+%{python_sitelib}/createprocess.so*
+%{python_sitelib}/cpopen.py*
+%{python_sitelib}/cpopen-%{version}-py*.egg-info
+
+%changelog
+* Sun Jan 20 2013 Yaniv Bronhaim <ybronhei@redhat.com> 1.0
+- Initial take
diff --git a/createprocess.c b/createprocess.c
new file mode 100644
index 0000000..499bed6
--- /dev/null
+++ b/createprocess.c
@@ -0,0 +1,329 @@
+/*
+* Copyright 2012 Red Hat, Inc.
+*
+* This program is free software; you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation; either version 2 of the License, or
+* (at your option) any later version.
+*
+* 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 General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program; if not, write to the Free Software
+* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*
+* Refer to the README and COPYING files for full details of the license
+*/
+
+#include <Python.h>
+
+#include <dirent.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+static PyObject *createProcess(PyObject *self, PyObject *args);
+static PyMethodDef CreateProcessMethods[];
+static void closeFDs(int errnofd);
+
+/* Python boilerplate */
+static PyMethodDef
+CreateProcessMethods[] = {
+ {"createProcess", createProcess, METH_VARARGS,
+ "Execute a command."},
+ {NULL, NULL, 0, NULL} /* Sentinel */
+};
+
+PyMODINIT_FUNC
+initcreateprocess(void)
+{
+ PyObject *m;
+
+ m = Py_InitModule("createprocess", CreateProcessMethods);
+
+ // In the future put other init code after this condition.
+ if (m == NULL)
+ return;
+}
+
+/* Just like close() but retries on interrupt */
+static int
+safeClose(int fd) {
+ int rv;
+
+retry:
+ rv = close(fd);
+ if ((rv < 0) && (errno == EINTR)) {
+ goto retry;
+ }
+
+ return rv;
+}
+
+static int
+setCloseOnExec(int fd) {
+ int flags;
+
+ flags = fcntl(fd, F_GETFD);
+ if (flags == -1) {
+ return -1;
+ }
+
+ if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1) {
+ return -1;
+ }
+
+ return 0;
+}
+
+/* Closes all open FDs except for stdin, stdout and stderr */
+static void
+closeFDs(int errnofd) {
+ DIR *dp;
+ int dfd;
+ struct dirent *ep;
+ int fdNum = -1;
+
+ dfd = open("/proc/self/fd/", O_RDONLY);
+ dp = fdopendir(dfd);
+ while ((ep = readdir(dp))) {
+ if(sscanf(ep->d_name, "%d", &fdNum) < 1) {
+ continue;
+ }
+
+ if (fdNum < 3) {
+ continue;
+ }
+
+ if (fdNum == dfd) {
+ continue;
+ }
+
+ if (fdNum == errnofd) {
+ continue;
+ }
+
+ safeClose(fdNum);
+ }
+
+ closedir(dp);
+ safeClose(dfd);
+}
+
+static void
+freeStringArray(char** arr) {
+ char** item;
+ for (item = arr; *item != NULL; item++) {
+ PyMem_Free(*item);
+ }
+
+ free(arr);
+}
+
+/* Copies the strings from a python list to a null terminated array.
+ * The strings are shallow copied and are owned by python.
+ * Don't keep this array after the call.
+ *
+ * Returns a NULL terminated array of null strings. On error returns NULL and
+ * sets the python error accordingly
+ */
+static char**
+pyListToArray(PyObject* list, int checkIfEmpty) {
+ int argn;
+ int i;
+ char** argv;
+
+ if (!PyList_Check(list)) {
+ PyErr_SetString(PyExc_TypeError, "Argument must be a python list");
+ return NULL;
+ }
+
+ argn = PyList_Size(list);
+ if ((checkIfEmpty) && (argn < 1)) {
+ PyErr_SetString(PyExc_ValueError, "List must not be empty");
+ return NULL;
+ }
+
+ argv = calloc(argn + 1, sizeof(char*));
+ if (!argv) {
+ PyErr_SetFromErrno(PyExc_OSError);
+ return NULL;
+ }
+
+ for (i = 0; i < argn; i++) {
+ if (!PyArg_Parse(PyList_GetItem(list, i),
+ "et;",
+ Py_FileSystemDefaultEncoding,
+ &argv[i])) {
+ PyErr_SetString(PyExc_TypeError,
+ "createProcess() arg 2 must contain only strings");
+ goto fail;
+ }
+ }
+
+ return argv;
+
+fail:
+ freeStringArray(argv);
+ return NULL;
+}
+
+/* Python's implementation of Popen forks back to python before execing.
+ * Forking a python proc is a very complex and volatile process.
+ *
+ * This is a simpler method of execing that doesn't go back to python after
+ * forking. This allows for faster safer exec.
+ *
+ * return NULL on error and sets the python error accordingly.
+ */
+static PyObject *
+createProcess(PyObject *self, PyObject *args)
+{
+ int cpid;
+
+ int outfd[2] = {-1, -1};
+ int in1fd[2] = {-1, -1};
+ int in2fd[2] = {-1, -1};
+
+ int errnofd[2] = {-1, -1};
+ int childErrno = 0;
+
+ PyObject* pyArgList;
+ PyObject* pyEnvList;
+ const char* cwd;
+ int close_fds = 0;
+
+ char** argv = NULL;
+ char** envp = NULL;
+
+ if (!PyArg_ParseTuple(args, "O!iiiiiiizO:createProcess;",
+ &PyList_Type, &pyArgList, &close_fds,
+ &outfd[0], &outfd[1],
+ &in1fd[0], &in1fd[1],
+ &in2fd[0], &in2fd[1],
+ &cwd, &pyEnvList)) {
+ return NULL;
+ }
+
+ argv = pyListToArray(pyArgList, 1);
+ if (!argv) {
+ goto fail;
+ }
+
+ if (PyList_Check(pyEnvList)) {
+ envp = pyListToArray(pyEnvList, 0);
+ if (!envp) {
+ goto fail;
+ }
+ }
+
+ if(pipe(errnofd) < 0) {
+ PyErr_SetFromErrno(PyExc_OSError);
+ goto fail;
+ }
+
+try_fork:
+ cpid = fork();
+ if (cpid < 0) {
+ if (errno == EAGAIN ||
+ errno == EINTR ) {
+ goto try_fork;
+ }
+
+ PyErr_SetFromErrno(PyExc_OSError);
+ goto fail;
+ }
+
+ if (!cpid) {
+ safeClose(0);
+ safeClose(1);
+ safeClose(2);
+
+ dup2(outfd[0], 0);
+ dup2(in1fd[1], 1);
+ dup2(in2fd[1], 2);
+
+ safeClose(outfd[0]);
+ safeClose(outfd[1]);
+ safeClose(in1fd[0]);
+ safeClose(in1fd[1]);
+ safeClose(in2fd[0]);
+ safeClose(in2fd[1]);
+ safeClose(errnofd[0]);
+ if (setCloseOnExec(errnofd[1]) < 0) {
+ goto sendErrno;
+ }
+
+ if (close_fds) {
+ closeFDs(errnofd[1]);
+ }
+
+ if (cwd) {
+ if (chdir(cwd) < 0) {
+ goto sendErrno;
+ }
+ setenv("PWD", cwd, 1);
+ }
+exec:
+ if (envp) {
+ execvpe(argv[0], argv, envp);
+ } else {
+ execvp(argv[0], argv);
+ }
+
+ if (errno == EINTR ||
+ errno == EAGAIN )
+ {
+ goto exec;
+ }
+sendErrno:
+ if (write(errnofd[1], &errno, sizeof(int)) < 0) {
+ exit(errno);
+ }
+ exit(-1);
+ }
+
+ safeClose(errnofd[1]);
+ errnofd[1] = -1;
+ if (read(errnofd[0], &childErrno, sizeof(int)) == sizeof(int)) {
+ errno = childErrno;
+ PyErr_SetFromErrno(PyExc_OSError);
+ goto fail;
+ }
+
+ safeClose(errnofd[0]);
+ errnofd[0] = -1;
+
+ /* From this point errors shouldn't occur, if they do something is very
+ * very very wrong */
+
+ freeStringArray(argv);
+
+ if (envp) {
+ freeStringArray(envp);
+ }
+
+ return Py_BuildValue("(iiii)", cpid, outfd[1], in1fd[0], in2fd[0]);
+
+fail:
+ if (argv) {
+ freeStringArray(argv);
+ }
+
+ if (envp) {
+ freeStringArray(envp);
+ }
+
+ if (errnofd[0] >= 0) {
+ safeClose(errnofd[0]);
+ }
+
+ if (errnofd[1] >= 0) {
+ safeClose(errnofd[1]);
+ }
+
+ return NULL;
+}
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..ee8c094
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,13 @@
+from distutils.core import setup, Extension
+
+module1 = Extension('createprocess',
+ sources=['createprocess.c'])
+
+setup(name='cpopen',
+ version='1.0',
+ description='Creates a subprocess in simpler safer manner',
+ py_modules=['cpopen'],
+ author='Yaniv Bronhaim',
+ author_email='ybronhei@redhat.com',
+ url='redhat.com',
+ ext_modules=[module1])