]> git.ipfire.org Git - pakfire.git/commitdiff
python: Bring back a basic version of Pakfire.execute()
authorMichael Tremer <michael.tremer@ipfire.org>
Mon, 17 Mar 2025 11:17:20 +0000 (11:17 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Mon, 17 Mar 2025 11:17:20 +0000 (11:17 +0000)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
Makefile.am
src/python/pakfire.c
tests/python/execute.py [new file with mode: 0755]

index 48319e9b86ce1b13f9f5d2064244cbe5e84b9da9..41455beb656eef13e45e6521fd3ff7333f4c364e 100644 (file)
@@ -1268,6 +1268,7 @@ dist_check_SCRIPTS = \
        tests/python/arch.py \
        tests/python/archive.py \
        tests/python/ctx.py \
+       tests/python/execute.py \
        tests/python/keys.py \
        tests/python/main.py \
        tests/python/package.py \
index 48502a5500b2412c22082aa4404da58c431fa0c6..783ffdfa9dda6c26ef3a47aa1d3a5a6ecbae0297 100644 (file)
@@ -528,6 +528,145 @@ ERROR:
        return ret;
 }
 
+/*
+ * Execute
+ */
+
+static PyObject* Pakfire_execute(PakfireObject* self, PyObject* args, PyObject* kwargs) {
+       struct pakfire_jail* jail = NULL;
+       struct pakfire_env* env = NULL;
+       const char** argv = NULL;
+       PyObject* ret = NULL;
+       PyObject* k = NULL;
+       PyObject* v = NULL;
+       Py_ssize_t p = 0;
+       int r;
+
+       PyObject* command = NULL;
+       PyObject* environ = NULL;
+
+       const char* kwlist[] = {
+               "command",
+               "environ",
+               NULL,
+       };
+
+       // Parse arguments
+       if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|O", (char**)kwlist, &command, &environ))
+               goto ERROR;
+
+       // Check if command is a list
+       if (!PyList_Check(command)) {
+               PyErr_SetString(PyExc_TypeError, "command must be a list");
+               goto ERROR;
+       }
+
+       // Fetch the length of the command
+       const ssize_t command_length = PyList_Size(command);
+
+       // Check if command is not empty
+       if (!command_length) {
+               PyErr_SetString(PyExc_ValueError, "command is empty");
+               goto ERROR;
+       }
+
+       // Allocate argv
+       argv = calloc(command_length + 1, sizeof(*argv));
+       if (!argv)
+               goto ERROR;
+
+       // All arguments in command must be strings
+       for (unsigned int i = 0; i < command_length; i++) {
+               PyObject* item = PyList_GET_ITEM(command, i);
+
+               if (!PyUnicode_Check(item)) {
+                       PyErr_Format(PyExc_TypeError, "Item %u in command is not a string", i);
+                       goto ERROR;
+               }
+
+               // Copy to argv
+               argv[i] = PyUnicode_AsUTF8(item);
+       }
+
+       // Parse the environment
+       if (environ) {
+               // Create a new environment
+               r = pakfire_env_create(&env, self->ctx);
+               if (r < 0) {
+                       errno = -r;
+                       PyErr_SetFromErrno(PyExc_OSError);
+                       goto ERROR;
+               }
+
+               // Check if environ is a dictionary
+               if (!PyDict_Check(environ)) {
+                       PyErr_SetString(PyExc_TypeError, "environ must be a dictionary");
+                       goto ERROR;
+               }
+
+               // All keys and values must be strings
+               while (PyDict_Next(environ, &p, &k, &v)) {
+                       if (!PyUnicode_Check(k) || !PyUnicode_Check(v)) {
+                               PyErr_SetString(PyExc_TypeError, "Environment contains a non-string object");
+                               goto ERROR;
+                       }
+
+                       // Set environment value
+                       r = pakfire_env_set(env, PyUnicode_AsUTF8(k), "%s", PyUnicode_AsUTF8(v));
+                       if (r < 0) {
+                               errno = -r;
+                               PyErr_SetFromErrno(PyExc_OSError);
+                               goto ERROR;
+                       }
+               }
+       }
+
+       // Create a new jail
+       r = pakfire_jail_create(&jail, self->pakfire);
+       if (r < 0) {
+               errno = -r;
+               PyErr_SetFromErrno(PyExc_OSError);
+               goto ERROR;
+       }
+
+       Py_BEGIN_ALLOW_THREADS
+
+       // Execute command
+       r = pakfire_jail_communicate(jail, argv, env, 0, NULL, NULL, NULL, NULL);
+
+       Py_END_ALLOW_THREADS
+
+       // If the return code was negative, we had some internal error
+       if (r < 0) {
+               errno = -r;
+               PyErr_SetFromErrno(PyExc_OSError);
+               goto ERROR;
+
+       // Otherwise the executed command returned some error code
+       } else if (r > 0) {
+               PyObject* code = PyLong_FromLong(r);
+
+               // Raise CommandExecutionError
+               PyErr_SetObject(PyExc_CommandExecutionError, code);
+               Py_DECREF(code);
+
+               goto ERROR;
+       }
+
+       // Otherwise return None
+       ret = Py_NewRef(Py_None);
+
+ERROR:
+       if (jail)
+               pakfire_jail_unref(jail);
+       if (env)
+               pakfire_env_unref(env);
+       if (argv)
+               free(argv);
+
+       return ret;
+}
+
 static struct PyMethodDef Pakfire_methods[] = {
        {
                "clean",
@@ -541,6 +680,12 @@ static struct PyMethodDef Pakfire_methods[] = {
                METH_VARARGS,
                NULL
        },
+       {
+               "execute",
+               (PyCFunction)Pakfire_execute,
+               METH_VARARGS|METH_KEYWORDS,
+               NULL,
+       },
        {
                "generate_key",
                (PyCFunction)Pakfire_generate_key,
diff --git a/tests/python/execute.py b/tests/python/execute.py
new file mode 100755 (executable)
index 0000000..f3ddad9
--- /dev/null
@@ -0,0 +1,134 @@
+#!/usr/bin/python3
+###############################################################################
+#                                                                             #
+# Pakfire - The IPFire package management system                              #
+# Copyright (C) 2024 Pakfire development team                                 #
+#                                                                             #
+# 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 3 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, see <http://www.gnu.org/licenses/>.       #
+#                                                                             #
+###############################################################################
+
+import logging
+import pakfire
+
+import tests
+
+class ExecuteTests(tests.TestCase):
+       """
+               This tests the execute command
+       """
+       def setUp(self):
+               self.pakfire = self.setup_pakfire()
+
+       # XXX Temporarily disabled, because the jail messes up the console
+       def test_execute(self):
+               r = self.pakfire.execute(["/command", "exit-with-code", "0"])
+
+               self.assertIsNone(r)
+
+       def test_return_value(self):
+               with self.assertRaises(pakfire.CommandExecutionError) as e:
+                       self.pakfire.execute(["/command", "exit-with-code", "123"])
+
+               # Extract return code
+               code, = e.exception.args
+
+               self.assertTrue(code == 123)
+
+       def test_environ(self):
+               r = self.pakfire.execute(["/command", "echo-environ", "VAR1"],
+                       environ={"VAR1" : "VAL1"})
+
+               self.assertIsNone(r)
+
+       def test_invalid_inputs(self):
+               # Arguments
+               with self.assertRaises(TypeError):
+                       self.pakfire.execute("/command")
+
+               with self.assertRaises(TypeError):
+                       self.pakfire.execute(["/command", 1])
+
+               with self.assertRaises(TypeError):
+                       self.pakfire.execute(("/command", "--help"))
+
+               # Environment
+               with self.assertRaises(TypeError):
+                       self.pakfire.execute(["/command", "--help"], environ={"VAR1" : 1})
+
+               with self.assertRaises(TypeError):
+                       self.pakfire.execute(["/command", "--help"], environ={1 : "VAL1"})
+
+               with self.assertRaises(TypeError):
+                       self.pakfire.execute(["/command", "--help"], environ="VAR1=VAL1")
+
+       def test_execute_non_existant_command(self):
+               """
+                       Executing non-existant commands should raise an error
+               """
+               with self.assertRaises(pakfire.CommandExecutionError):
+                       self.pakfire.execute(["/command-does-not-exist"])
+
+       def test_execute_output(self):
+               self.pakfire.execute(["/command", "echo", "123"])
+
+               # Multiple newlines in one read
+               self.pakfire.execute(["/command", "echo", "1\n2\n3"])
+
+               # Run a command with a lot of output which exceeds the buffer size
+               self.pakfire.execute(["/command", "lines", "1", "65536"])
+
+               # Run a command that generates lots of lines
+               self.pakfire.execute(["/command", "lines", "100", "40"])
+
+       #def test_nice(self):
+       #       self.pakfire.execute(["/command", "print-nice"], nice=5)
+
+       #def test_nice_invalid_input(self):
+       #       """
+       #               Tries using an invalid nice value
+       #       """
+       #       with self.assertRaises(OSError):
+       #               self.pakfire.execute(["/command", "print-nice"], nice=100)
+
+       #def test_check_open_file_descriptors(self):
+       #       """
+       #               Since we are spawning child processes, it might happen that we leak file
+       #               descriptors to the child process.
+       #       """
+       #       self.pakfire.execute(["/command", "check-open-file-descriptors"])
+
+       # Signals
+
+       def test_send_signal_DEFAULT(self):
+               """
+                       Sends a stupid signal which doesn't do anything
+               """
+               self.pakfire.execute(["/command", "send-signal", "0"])
+
+       def test_send_signal_KILL(self):
+               """
+                       Test the process killing itself
+               """
+               self.pakfire.execute(["/command", "send-signal", "9"])
+
+       def test_send_signal_TERM(self):
+               """
+                       Test the process terminating itself
+               """
+               self.pakfire.execute(["/command", "send-signal", "15"])
+
+
+if __name__ == "__main__":
+       tests.main()