From 3738ba745d0cd5aceed6cfc95da4f0124b6802ef Mon Sep 17 00:00:00 2001 From: Michael Tremer Date: Mon, 17 Mar 2025 11:17:20 +0000 Subject: [PATCH] python: Bring back a basic version of Pakfire.execute() Signed-off-by: Michael Tremer --- Makefile.am | 1 + src/python/pakfire.c | 145 ++++++++++++++++++++++++++++++++++++++++ tests/python/execute.py | 134 +++++++++++++++++++++++++++++++++++++ 3 files changed, 280 insertions(+) create mode 100755 tests/python/execute.py diff --git a/Makefile.am b/Makefile.am index 48319e9b..41455beb 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 \ diff --git a/src/python/pakfire.c b/src/python/pakfire.c index 48502a55..783ffdfa 100644 --- a/src/python/pakfire.c +++ b/src/python/pakfire.c @@ -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 index 00000000..f3ddad9f --- /dev/null +++ b/tests/python/execute.py @@ -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 . # +# # +############################################################################### + +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() -- 2.39.5