forked from DataDog/datadog-agent
-
Notifications
You must be signed in to change notification settings - Fork 0
/
_util.c
310 lines (258 loc) · 10.4 KB
/
_util.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-present Datadog, Inc.
#include "_util.h"
#include "cgo_free.h"
#include "rtloader_mem.h"
#include "stringutils.h"
#include <stdio.h>
// must be set by the caller
static cb_get_subprocess_output_t cb_get_subprocess_output = NULL;
static PyObject *subprocess_output(PyObject *self, PyObject *args, PyObject *kw);
// Exceptions
/*! \fn void addSubprocessException(PyObject *m)
\brief Adds a custom SubprocessOutputEmptyError exception to the module passed as parameter.
\param m A PyObject* pointer to the module we wish to register the exception with.
*/
void addSubprocessException(PyObject *m)
{
PyObject *SubprocessOutputEmptyError = PyErr_NewException(_SUBPROCESS_OUTPUT_ERROR_NS_NAME, NULL, NULL);
PyModule_AddObject(m, _SUBPROCESS_OUTPUT_ERROR_NAME, SubprocessOutputEmptyError);
}
static PyMethodDef methods[] = {
{ "subprocess_output", (PyCFunction)subprocess_output, METH_VARARGS | METH_KEYWORDS,
"Exec a process and return the output." },
{ "get_subprocess_output", (PyCFunction)subprocess_output, METH_VARARGS | METH_KEYWORDS,
"Exec a process and return the output." },
{ NULL, NULL } // guards
};
#ifdef DATADOG_AGENT_THREE
static struct PyModuleDef module_def = { PyModuleDef_HEAD_INIT, _UTIL_MODULE_NAME, NULL, -1, methods };
PyMODINIT_FUNC PyInit__util(void)
{
PyObject *m = PyModule_Create(&module_def);
addSubprocessException(m);
return m;
}
#elif defined(DATADOG_AGENT_TWO)
// in Python2 keep the object alive for the program lifetime
static PyObject *module;
void Py2_init__util()
{
module = Py_InitModule(_UTIL_MODULE_NAME, methods);
addSubprocessException(module);
}
#endif
void _set_get_subprocess_output_cb(cb_get_subprocess_output_t cb)
{
cb_get_subprocess_output = cb;
}
/*! \fn void raiseEmptyOutputError()
\brief sets the SubprocessOutputEmptyError exception as the interpreter error.
If everything goes well the exception error will be set in the interpreter.
Otherwise, if the module or the exception class are not found, the relevant
error will be set in the interpreter instead.
*/
static void raiseEmptyOutputError()
{
PyObject *utilModule = PyImport_ImportModule(_UTIL_MODULE_NAME);
if (utilModule == NULL) {
PyErr_SetString(PyExc_TypeError, "error: no module '" _UTIL_MODULE_NAME "'");
return;
}
PyObject *excClass = PyObject_GetAttrString(utilModule, _SUBPROCESS_OUTPUT_ERROR_NAME);
if (excClass == NULL) {
Py_DecRef(utilModule);
PyErr_SetString(PyExc_TypeError, "no attribute '" _SUBPROCESS_OUTPUT_ERROR_NS_NAME "' found");
return;
}
PyErr_SetString(excClass, "get_subprocess_output expected output but had none.");
Py_DecRef(excClass);
Py_DecRef(utilModule);
}
/*! \fn PyObject *subprocess_output(PyObject *self, PyObject *args)
\brief This function implements the `_util.subprocess_output` _and_ `_util.get_subprocess_output`
python method, allowing to execute a subprocess and collect its output.
\param self A PyObject* pointer to the _util module.
\param args A PyObject* pointer to the args tuple with the desired subprocess commands, and
optionally a boolean raise_on_empty flag.
\param kw A PyObject* pointer to the kw dict with optionally an env dict.
\return a PyObject * pointer to a python tuple with the stdout, stderr output and the
command exit code.
This function is callable as the `_util.subprocess_output` or `_util.get_subprocess_output`
python methods. The command arguments list is fed to the CGO callback, where the command is
executed in go-land. The stdout, stderr and exit codes for the command are returned by the
callback; these are then converted into python strings and integer respectively and returned
in a tuple. If the optional `raise_on_empty` boolean flag is set, and the command output is
empty an exception will be raised: the error will be set in the interpreter and NULL will be
returned.
*/
PyObject *subprocess_output(PyObject *self, PyObject *args, PyObject *kw)
{
int i;
int raise = 0;
int ret_code = 0;
int subprocess_args_sz = 0;
int subprocess_env_sz = 0;
char **subprocess_args = NULL;
char **subprocess_env = NULL;
char *c_stdout = NULL;
char *c_stderr = NULL;
char *exception = NULL;
PyObject *cmd_args = NULL;
PyObject *cmd_raise_on_empty = NULL;
PyObject *cmd_env = NULL;
PyObject *pyResult = NULL;
if (!cb_get_subprocess_output) {
Py_RETURN_NONE;
}
PyGILState_STATE gstate = PyGILState_Ensure();
static char *keywords[] = { "command", "raise_on_empty", "env", NULL };
// `cmd_args` is mandatory and should be a list, `cmd_raise_on_empty` is an optional
// boolean. The string after the ':' is used as the function name in error messages.
if (!PyArg_ParseTupleAndKeywords(args, kw, "O|O" PY_ARG_PARSE_TUPLE_KEYWORD_ONLY "O:get_subprocess_output",
keywords, &cmd_args, &cmd_raise_on_empty, &cmd_env)) {
goto cleanup;
}
if (!PyList_Check(cmd_args)) {
PyErr_SetString(PyExc_TypeError, "command args is not a list");
goto cleanup;
}
// We already PyList_Check cmd_args, so PyList_Size won't fail and return -1
subprocess_args_sz = PyList_Size(cmd_args);
if (subprocess_args_sz == 0) {
PyErr_SetString(PyExc_TypeError, "invalid command: empty list");
goto cleanup;
}
if (!(subprocess_args = (char **)_malloc(sizeof(*subprocess_args) * (subprocess_args_sz + 1)))) {
PyErr_SetString(PyExc_MemoryError, "unable to allocate memory, bailing out");
goto cleanup;
}
// init to NULL for safety - could use memset, but this is safer.
for (i = 0; i <= subprocess_args_sz; i++) {
subprocess_args[i] = NULL;
}
for (i = 0; i < subprocess_args_sz; i++) {
char *subprocess_arg = as_string(PyList_GetItem(cmd_args, i));
if (subprocess_arg == NULL) {
PyErr_SetString(PyExc_TypeError, "command argument must be valid strings");
goto cleanup;
}
subprocess_args[i] = subprocess_arg;
}
if (cmd_env != NULL && cmd_env != Py_None) {
if (!PyDict_Check(cmd_env)) {
PyErr_SetString(PyExc_TypeError, "env is not a dict");
goto cleanup;
}
subprocess_env_sz = PyDict_Size(cmd_env);
if (subprocess_env_sz != 0) {
if (!(subprocess_env = (char **)_malloc(sizeof(*subprocess_env) * (subprocess_env_sz + 1)))) {
PyErr_SetString(PyExc_MemoryError, "unable to allocate memory, bailing out");
goto cleanup;
}
for (i = 0; i <= subprocess_env_sz; i++) {
subprocess_env[i] = NULL;
}
Py_ssize_t pos = 0;
PyObject *key = NULL, *value = NULL;
for (i = 0; i < subprocess_env_sz && PyDict_Next(cmd_env, &pos, &key, &value); i++) {
char *env_key = as_string(key);
if (env_key == NULL) {
PyErr_SetString(PyExc_TypeError, "env key is not a string");
goto cleanup;
}
char *env_value = as_string(value);
if (env_value == NULL) {
PyErr_SetString(PyExc_TypeError, "env value is not a string");
_free(env_key);
goto cleanup;
}
char *env = (char *)_malloc((strlen(env_key) + 1 + strlen(env_value) + 1) * sizeof(*env));
if (env == NULL) {
PyErr_SetString(PyExc_MemoryError, "unable to allocate memory, bailing out");
_free(env_key);
_free(env_value);
goto cleanup;
}
strcpy(env, env_key);
strcat(env, "=");
strcat(env, env_value);
_free(env_key);
_free(env_value);
subprocess_env[i] = env;
}
}
}
if (cmd_raise_on_empty != NULL && !PyBool_Check(cmd_raise_on_empty)) {
PyErr_SetString(PyExc_TypeError, "bad raise_on_empty argument: should be bool");
goto cleanup;
}
if (cmd_raise_on_empty == Py_True) {
raise = 1;
}
// Release the GIL so Python can execute other checks while Go runs the subprocess
PyGILState_Release(gstate);
PyThreadState *Tstate = PyEval_SaveThread();
cb_get_subprocess_output(subprocess_args, subprocess_env, &c_stdout, &c_stderr, &ret_code, &exception);
// Acquire the GIL now that Go is done
PyEval_RestoreThread(Tstate);
gstate = PyGILState_Ensure();
if (raise && strlen(c_stdout) == 0) {
raiseEmptyOutputError();
goto cleanup;
}
if (exception) {
PyErr_SetString(PyExc_Exception, exception);
goto cleanup;
}
PyObject *pyStdout = NULL;
if (c_stdout) {
pyStdout = PyStringFromCString(c_stdout);
} else {
Py_INCREF(Py_None);
pyStdout = Py_None;
}
PyObject *pyStderr = NULL;
if (c_stderr) {
pyStderr = PyStringFromCString(c_stderr);
} else {
Py_INCREF(Py_None);
pyStderr = Py_None;
}
pyResult = PyTuple_New(3);
PyTuple_SetItem(pyResult, 0, pyStdout);
PyTuple_SetItem(pyResult, 1, pyStderr);
#ifdef DATADOG_AGENT_THREE
PyTuple_SetItem(pyResult, 2, PyLong_FromLong(ret_code));
#else
PyTuple_SetItem(pyResult, 2, PyInt_FromLong(ret_code));
#endif
cleanup:
if (c_stdout) {
cgo_free(c_stdout);
}
if (c_stderr) {
cgo_free(c_stderr);
}
if (exception) {
cgo_free(exception);
}
if (subprocess_args) {
for (i = 0; i <= subprocess_args_sz && subprocess_args[i]; i++) {
_free(subprocess_args[i]);
}
_free(subprocess_args);
}
if (subprocess_env) {
for (i = 0; i <= subprocess_env_sz && subprocess_env[i]; i++) {
_free(subprocess_env[i]);
}
_free(subprocess_env);
}
// Please note that if we get here we have a matching PyGILState_Ensure above, so we're safe.
PyGILState_Release(gstate);
// pyResult will be NULL in the face of error to raise the exception set by PyErr_SetString
return pyResult;
}