Skip to content

Commit fbcb0ab

Browse files
authored
[subprocess][go] run the subprocesses from go-land (#843)
* [python] use subprocess32 whenever possible * [python] collect interpreter usage statistics. [py] install pympler too [py] fix multiple issues with python memory collector [py] initialize entries * [api] be REST friendly * [subprocess] fork from go-land add C-binding * [subprocess] run from go-land - WIP * [py] wrap new call in legacy call for BC * [subprocess][go] almost ready - pending exception import from go * [subprocess][go] compiles * [subprocess][go] return None if returning null * [subprocess][go] adding GIL to GetSubprocessOutput go func * [subprocess] adding test, removing obsolete py code + typo fix * [subprocess] no need for subprocess32 now that we run from goland. * [subprocess][go] fix race condition to collect cmd output/stderr * [subprocess][c] make C90 compliant. * [circleci] updating image to go1.9.1 and newer ruby. * [circleci] updating builder to address openssl issue with image * [subprocess][py] naturally make the exception available to python scripts - BC * [subprocess][go] lock thread + indentation issues * [subprocess][go] dont decrement references
1 parent 8f56d96 commit fbcb0ab

File tree

8 files changed

+258
-74
lines changed

8 files changed

+258
-74
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ experimental:
1313
templates:
1414
job_template: &job_template
1515
docker:
16-
- image: jfullaondo/datadog-agent-builder-deb:0.0.4
16+
- image: jfullaondo/datadog-agent-builder-deb:0.1.1
1717
environment:
1818
USE_SYSTEM_LIBS: "1"
1919
working_directory: /go/src/github.com/DataDog/datadog-agent

.circleci/images/builder/Dockerfile

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
FROM golang:1.8.3
1+
FROM golang:1.9.1
22

33
RUN sed -i 's/^#\s*\(deb.*universe\)$/\1/g' /etc/apt/sources.list \
44
&& sed -i 's/^#\s*\(deb.*multiverse\)$/\1/g' /etc/apt/sources.list \
55
&& sed -i 's/main/main contrib non-free/' /etc/apt/sources.list
66

7-
RUN apt-get update && apt-get install -y python2.7-dev autoconf autogen intltool
7+
RUN apt-get update && apt-get install -y python2.7-dev autoconf autogen intltool libssl1.0-dev
88
RUN apt-get install -y libsnmp-base libsnmp-dev libpq-dev snmp-mibs-downloader
99

1010
# Ruby,,,
@@ -14,9 +14,9 @@ RUN mkdir -p /usr/local/etc \
1414
echo 'update: --no-document'; \
1515
} >> /usr/local/etc/gemrc
1616

17-
ENV RUBY_MAJOR 2.3
18-
ENV RUBY_VERSION 2.3.4
19-
ENV RUBY_DOWNLOAD_SHA256 341cd9032e9fd17c452ed8562a8d43f7e45bfe05e411d0d7d627751dd82c578c
17+
ENV RUBY_MAJOR 2.4
18+
ENV RUBY_VERSION 2.4.2
19+
ENV RUBY_DOWNLOAD_SHA256 748a8980d30141bd1a4124e11745bb105b436fb1890826e0d2b9ea31af27f735
2020
ENV RUBYGEMS_VERSION 2.6.12
2121

2222
# some of ruby's build scripts are written in ruby
@@ -57,14 +57,14 @@ RUN set -ex \
5757
--build="$gnuArch" \
5858
--disable-install-doc \
5959
--enable-shared \
60+
--with-openssl=/usr/lib/ssl \
6061
&& make -j "$(nproc)" \
6162
&& make install \
6263
\
6364
&& apt-get purge -y --auto-remove $buildDeps \
6465
&& cd / \
65-
&& rm -r /usr/src/ruby \
66-
\
67-
&& gem update --system "$RUBYGEMS_VERSION"
66+
&& gem update --system "$RUBYGEMS_VERSION" \
67+
&& rm -r /usr/src/ruby
6868

6969
ENV BUNDLER_VERSION 1.15.3
7070

cmd/agent/dist/utils/subprocess_output.py

Lines changed: 3 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -6,73 +6,12 @@
66
# (C) Datadog, Inc. 2010-2017
77
# All rights reserved
88

9-
# stdlib
10-
from functools import wraps
11-
import logging
12-
import tempfile
13-
import os
14-
import sys
15-
16-
if os.name == 'posix' and sys.version_info[0] < 3:
17-
try:
18-
import subprocess32 as subprocess
19-
except ImportError:
20-
import subprocess
21-
else:
22-
import subprocess
23-
24-
25-
log = logging.getLogger(__name__)
26-
27-
28-
class SubprocessOutputEmptyError(Exception):
29-
pass
30-
9+
from util import get_subprocess_output as subprocess_output
10+
from util import SubprocessOutputEmptyError # noqa
3111

3212
def get_subprocess_output(command, log, raise_on_empty_output=True):
3313
"""
3414
Run the given subprocess command and return its output. Raise an Exception
3515
if an error occurs.
3616
"""
37-
38-
# Use tempfile, allowing a larger amount of memory. The subprocess.Popen
39-
# docs warn that the data read is buffered in memory. They suggest not to
40-
# use subprocess.PIPE if the data size is large or unlimited.
41-
with tempfile.TemporaryFile() as stdout_f, tempfile.TemporaryFile() as stderr_f:
42-
proc = subprocess.Popen(command, stdout=stdout_f, stderr=stderr_f)
43-
pid = proc.pid
44-
log.debug("running process: {0} with pid: {1}".format(" ".join(command), pid))
45-
46-
retcode = proc.wait()
47-
if retcode != 0:
48-
log.debug("Error while running {0} with pid: {1}. It returned with code {2}".format(" ".join(command), pid, retcode))
49-
50-
stderr_f.seek(0)
51-
err = stderr_f.read()
52-
if err:
53-
log.debug("Error while running {0} : {1}".format(" ".join(command), err))
54-
55-
stdout_f.seek(0)
56-
output = stdout_f.read()
57-
58-
if not output and raise_on_empty_output:
59-
raise SubprocessOutputEmptyError("get_subprocess_output expected output but had none.")
60-
61-
return (output, err, proc.returncode)
62-
63-
64-
def log_subprocess(func):
65-
"""
66-
Wrapper around subprocess to log.debug commands.
67-
"""
68-
@wraps(func)
69-
def wrapper(*params, **kwargs):
70-
fc = "%s(%s)" % (func.__name__, ', '.join(
71-
[a.__repr__() for a in params] +
72-
["%s = %s" % (a, b) for a, b in kwargs.items()]
73-
))
74-
log.debug("%s called" % fc)
75-
return func(*params, **kwargs)
76-
return wrapper
77-
78-
subprocess.Popen = log_subprocess(subprocess.Popen)
17+
return subprocess_output(command, raise_on_empty_output)

omnibus/config/software/datadog-agent-integrations.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@
9696
# Manually add "core" dependencies that are not listed in the checks requirements
9797
all_reqs_file.puts "requests==2.11.1"
9898
all_reqs_file.puts "pympler==0.5"
99-
all_reqs_file.puts "subprocess32==3.2.7" if os != 'windows'
10099

101100
all_reqs_file.close
102101
end

pkg/collector/py/datadog_agent.c

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
#include "datadog_agent.h"
22

3+
// Functions
34
PyObject* GetVersion(PyObject *self, PyObject *args);
45
PyObject* Headers(PyObject *self, PyObject *args);
56
PyObject* GetHostname(PyObject *self, PyObject *args);
67
PyObject* LogMessage(char *message, int logLevel);
78
PyObject* GetConfig(char *key);
9+
PyObject* GetSubprocessOutput(char **args, int argc, int raise);
10+
11+
// Exceptions
12+
PyObject* SubprocessOutputEmptyError;
813

914
static PyObject *get_config(PyObject *self, PyObject *args) {
1015
char *key;
@@ -38,6 +43,65 @@ static PyObject *log_message(PyObject *self, PyObject *args) {
3843
return LogMessage(message, log_level);
3944
}
4045

46+
static PyObject *get_subprocess_output(PyObject *self, PyObject *args) {
47+
PyObject *cmd_args, *cmd_raise_on_empty;
48+
int raise = 1, i=0;
49+
int subprocess_args_sz;
50+
char ** subprocess_args, * subprocess_arg;
51+
PyObject * py_result = Py_None;
52+
53+
PyGILState_STATE gstate = PyGILState_Ensure();
54+
55+
cmd_raise_on_empty = NULL;
56+
if (!PyArg_ParseTuple(args, "O|O:get_subprocess_output", &cmd_args, &cmd_raise_on_empty)) {
57+
PyGILState_Release(gstate);
58+
PyErr_SetString(PyExc_TypeError, "unable to parse arguments");
59+
Py_RETURN_NONE;
60+
}
61+
62+
if (!PyList_Check(cmd_args)) {
63+
PyGILState_Release(gstate);
64+
PyErr_SetString(PyExc_TypeError, "command args not a list");
65+
Py_RETURN_NONE;
66+
}
67+
68+
if (cmd_raise_on_empty != NULL && !PyBool_Check(cmd_raise_on_empty)) {
69+
PyGILState_Release(gstate);
70+
PyErr_SetString(PyExc_TypeError, "bad raise on empty argument - should be bool");
71+
Py_RETURN_NONE;
72+
}
73+
74+
if (cmd_raise_on_empty != NULL) {
75+
raise = (int)(cmd_raise_on_empty == Py_True);
76+
}
77+
78+
subprocess_args_sz = PyList_Size(cmd_args);
79+
if(!(subprocess_args = (char **)malloc(sizeof(char *)*subprocess_args_sz))) {
80+
PyGILState_Release(gstate);
81+
PyErr_SetString(PyExc_MemoryError, "unable to allocate memory, bailing out");
82+
Py_RETURN_NONE;
83+
}
84+
85+
for (i = 0; i < subprocess_args_sz; i++) {
86+
subprocess_arg = PyString_AsString(PyList_GetItem(cmd_args, i));
87+
if (subprocess_arg == NULL) {
88+
PyErr_SetString(PyExc_Exception, "unable to parse arguments to cgo/go-land");
89+
free(subprocess_args);
90+
Py_RETURN_NONE;
91+
}
92+
subprocess_args[i] = subprocess_arg;
93+
}
94+
95+
PyGILState_Release(gstate);
96+
py_result = GetSubprocessOutput(subprocess_args, subprocess_args_sz, raise);
97+
free(subprocess_args);
98+
99+
if (py_result == NULL) {
100+
Py_RETURN_NONE;
101+
}
102+
return py_result;
103+
}
104+
41105
static PyMethodDef datadogAgentMethods[] = {
42106
{"get_version", GetVersion, METH_VARARGS, "Get the Agent version."},
43107
{"get_config", get_config, METH_VARARGS, "Get value from the agent configuration."},
@@ -53,6 +117,7 @@ static PyMethodDef datadogAgentMethods[] = {
53117
*/
54118
static PyMethodDef utilMethods[] = {
55119
{"headers", (PyCFunction)Headers, METH_VARARGS, "Get basic HTTP headers with the right UserAgent."},
120+
{"get_subprocess_output", (PyCFunction)get_subprocess_output, METH_VARARGS, "Run subprocess and return its output."},
56121
{NULL, NULL}
57122
};
58123

@@ -64,5 +129,9 @@ void initdatadogagent()
64129
PyObject *da = Py_InitModule("datadog_agent", datadogAgentMethods);
65130
PyObject *util = Py_InitModule("util", utilMethods);
66131

132+
SubprocessOutputEmptyError = PyErr_NewException("util.SubprocessOutputEmptyError", NULL, NULL);
133+
Py_INCREF(SubprocessOutputEmptyError);
134+
PyModule_AddObject(util, "SubprocessOutputEmptyError", SubprocessOutputEmptyError);
135+
67136
PyGILState_Release(gstate);
68137
}

pkg/collector/py/datadog_agent.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
package py
77

88
import (
9+
"fmt"
10+
"io/ioutil"
11+
"os/exec"
12+
"runtime"
13+
"sync"
14+
"syscall"
915
"unsafe"
1016

1117
log "github.com/cihub/seelog"
@@ -111,6 +117,115 @@ func LogMessage(message *C.char, logLevel C.int) *C.PyObject {
111117
return C._none()
112118
}
113119

120+
// GetSubprocessOutput runs the subprocess and returns the output
121+
//export GetSubprocessOutput
122+
func GetSubprocessOutput(argv **C.char, argc, raise int) *C.PyObject {
123+
124+
// https://github.com/golang/go/wiki/cgo#turning-c-arrays-into-go-slices
125+
length := int(argc)
126+
subprocessArgs := make([]string, length-1)
127+
cmdSlice := (*[1 << 30]*C.char)(unsafe.Pointer(argv))[:length:length]
128+
subprocessCmd := C.GoString(cmdSlice[0])
129+
for i := 1; i < length; i++ {
130+
subprocessArgs[i-1] = C.GoString(cmdSlice[i])
131+
}
132+
cmd := exec.Command(subprocessCmd, subprocessArgs...)
133+
134+
runtime.LockOSThread()
135+
defer runtime.UnlockOSThread()
136+
glock := C.PyGILState_Ensure()
137+
defer C.PyGILState_Release(glock)
138+
139+
stdout, err := cmd.StdoutPipe()
140+
if err != nil {
141+
cErr := C.CString(fmt.Sprintf("internal error creating stdout pipe: %v", err))
142+
C.PyErr_SetString(C.PyExc_Exception, cErr)
143+
C.free(unsafe.Pointer(cErr))
144+
return C._none()
145+
}
146+
147+
var wg sync.WaitGroup
148+
var output []byte
149+
wg.Add(1)
150+
go func() {
151+
defer wg.Done()
152+
output, _ = ioutil.ReadAll(stdout)
153+
}()
154+
155+
stderr, err := cmd.StderrPipe()
156+
if err != nil {
157+
cErr := C.CString(fmt.Sprintf("internal error creating stderr pipe: %v", err))
158+
C.PyErr_SetString(C.PyExc_Exception, cErr)
159+
C.free(unsafe.Pointer(cErr))
160+
return C._none()
161+
}
162+
163+
var outputErr []byte
164+
wg.Add(1)
165+
go func() {
166+
defer wg.Done()
167+
outputErr, _ = ioutil.ReadAll(stderr)
168+
}()
169+
170+
cmd.Start()
171+
172+
retCode := 0
173+
err = cmd.Wait()
174+
if exiterr, ok := err.(*exec.ExitError); ok {
175+
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
176+
retCode = status.ExitStatus()
177+
}
178+
}
179+
wg.Wait()
180+
181+
if raise > 0 {
182+
// raise on error
183+
if len(output) == 0 {
184+
cModuleName := C.CString("util")
185+
utilModule := C.PyImport_ImportModule(cModuleName)
186+
C.free(unsafe.Pointer(cModuleName))
187+
if utilModule == nil {
188+
cErr := C.CString("unable to import subprocess empty output exception")
189+
C.PyErr_SetString(C.PyExc_Exception, cErr)
190+
C.free(unsafe.Pointer(cErr))
191+
return C._none()
192+
}
193+
defer C.Py_DecRef(utilModule)
194+
195+
cExcName := C.CString("SubprocessOutputEmptyError")
196+
excClass := C.PyObject_GetAttrString(utilModule, cExcName)
197+
C.free(unsafe.Pointer(cExcName))
198+
if excClass == nil {
199+
cErr := C.CString("unable to import subprocess empty output exception")
200+
C.PyErr_SetString(C.PyExc_Exception, cErr)
201+
C.free(unsafe.Pointer(cErr))
202+
return C._none()
203+
}
204+
defer C.Py_DecRef(excClass)
205+
206+
cErr := C.CString("get_subprocess_output expected output but had none.")
207+
C.PyErr_SetString((*C.PyObject)(unsafe.Pointer(excClass)), cErr)
208+
C.free(unsafe.Pointer(cErr))
209+
return C._none()
210+
}
211+
}
212+
213+
cOutput := C.CString(string(output[:]))
214+
pyOutput := C.PyString_FromString(cOutput)
215+
C.free(unsafe.Pointer(cOutput))
216+
cOutputErr := C.CString(string(outputErr[:]))
217+
pyOutputErr := C.PyString_FromString(cOutputErr)
218+
C.free(unsafe.Pointer(cOutputErr))
219+
pyRetCode := C.PyInt_FromLong(C.long(retCode))
220+
221+
pyResult := C.PyTuple_New(3)
222+
C.PyTuple_SetItem(pyResult, 0, pyOutput)
223+
C.PyTuple_SetItem(pyResult, 1, pyOutputErr)
224+
C.PyTuple_SetItem(pyResult, 2, pyRetCode)
225+
226+
return pyResult
227+
}
228+
114229
func initDatadogAgent() {
115230
C.initdatadogagent()
116231
}

pkg/collector/py/datadog_agent.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include <Python.h>
55

6+
67
void initdatadogagent();
78

89
#endif /* DATADOG_HEADER */

0 commit comments

Comments
 (0)