void SetOutputFileHandle(FILE *f, bool transfer_ownership);
void SetErrorFileHandle(FILE *f, bool transfer_ownership);
+
+ void FlushDebuggerOutputHandles();
FILE *GetInputFileHandle();
void SetOutputFileHandle(FILE *fh, bool tranfer_ownership);
void SetErrorFileHandle(FILE *fh, bool tranfer_ownership);
+
+ void FlushDebuggerOutputHandles();
void SaveInputTerminalState();
m_is_interactive(eLazyBoolCalculate),
m_is_real_terminal(eLazyBoolCalculate) {}
+ File(File &&rhs);
+
+ File& operator= (File &&rhs);
+
+ void Swap(File &other);
+
+ File(void *cookie,
+ int (*readfn)(void *, char *, int),
+ int (*writefn)(void *, const char *, int),
+ int (*closefn)(void *));
+
//------------------------------------------------------------------
/// Constructor with path.
///
LazyBool m_is_interactive;
LazyBool m_is_real_terminal;
LazyBool m_supports_colors;
-
-private:
- DISALLOW_COPY_AND_ASSIGN(File);
};
} // namespace lldb_private
--- /dev/null
+"""
+Test lldb Python API for setting output and error file handles
+"""
+
+from __future__ import print_function
+
+
+import contextlib
+import os
+import io
+import re
+import platform
+import unittest
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class StringIO(io.TextIOBase):
+
+ def __init__(self, buf=''):
+ self.buf = buf
+
+ def writable(self):
+ return True
+
+ def write(self, s):
+ self.buf += s
+ return len(s)
+
+
+class BadIO(io.TextIOBase):
+
+ def writable(self):
+ return True
+
+ def write(self, s):
+ raise Exception('OH NOE')
+
+
+@contextlib.contextmanager
+def replace_stdout(new):
+ old = sys.stdout
+ sys.stdout = new
+ try:
+ yield
+ finally:
+ sys.stdout = old
+
+
+def handle_command(debugger, cmd, raise_on_fail=True, collect_result=True):
+
+ ret = lldb.SBCommandReturnObject()
+
+ if collect_result:
+ interpreter = debugger.GetCommandInterpreter()
+ interpreter.HandleCommand(cmd, ret)
+ else:
+ debugger.HandleCommand(cmd)
+
+ if hasattr(debugger, 'FlushDebuggerOutputHandles'):
+ debugger.FlushDebuggerOutputHandles()
+
+ if collect_result and raise_on_fail and not ret.Succeeded():
+ raise Exception
+
+ return ret.GetOutput()
+
+
+
+class FileHandleTestCase(TestBase):
+
+ mydir = TestBase.compute_mydir(__file__)
+
+ def comment(self, *args):
+ if self.session is not None:
+ print(*args, file=self.session)
+
+ def skip_windows(self):
+ if platform.system() == 'Windows':
+ self.skipTest('windows')
+
+
+ @add_test_categories(['pyapi'])
+ @no_debug_info_test
+ def test_file_out(self):
+
+ debugger = lldb.SBDebugger.Create()
+ try:
+ with open('output', 'w') as f:
+ debugger.SetOutputFileHandle(f, False)
+ handle_command(debugger, 'script print("foobar")')
+
+ with open('output', 'r') as f:
+ self.assertEqual(f.read().strip(), "foobar")
+
+ finally:
+ self.RemoveTempFile('output')
+ lldb.SBDebugger.Destroy(debugger)
+
+
+ @add_test_categories(['pyapi'])
+ @no_debug_info_test
+ def test_file_error(self):
+
+ debugger = lldb.SBDebugger.Create()
+ try:
+ with open('output', 'w') as f:
+ debugger.SetErrorFileHandle(f, False)
+ handle_command(debugger, 'lolwut', raise_on_fail=False, collect_result=False)
+
+ with open('output', 'r') as f:
+ errors = f.read()
+ self.assertTrue(re.search(r'error:.*lolwut', errors))
+
+ finally:
+ self.RemoveTempFile('output')
+ lldb.SBDebugger.Destroy(debugger)
+
+
+ @add_test_categories(['pyapi'])
+ @no_debug_info_test
+ def test_string_out(self):
+
+ self.skip_windows()
+
+ io = StringIO()
+ debugger = lldb.SBDebugger.Create()
+ try:
+ debugger.SetOutputFileHandle(io, False)
+ handle_command(debugger, 'script print("foobar")')
+
+ self.assertEqual(io.buf.strip(), "foobar")
+
+ finally:
+ lldb.SBDebugger.Destroy(debugger)
+
+
+ @add_test_categories(['pyapi'])
+ @no_debug_info_test
+ def test_string_error(self):
+
+ self.skip_windows()
+
+ io = StringIO()
+ debugger = lldb.SBDebugger.Create()
+ try:
+ debugger.SetErrorFileHandle(io, False)
+ handle_command(debugger, 'lolwut', raise_on_fail=False, collect_result=False)
+
+ errors = io.buf
+ self.assertTrue(re.search(r'error:.*lolwut', errors))
+
+ finally:
+ lldb.SBDebugger.Destroy(debugger)
+
+ @add_test_categories(['pyapi'])
+ @no_debug_info_test
+ def test_replace_stdout(self):
+
+ self.skip_windows()
+
+ io = StringIO()
+ debugger = lldb.SBDebugger.Create()
+ try:
+
+ with replace_stdout(io):
+ handle_command(debugger, 'script print("lol, crash")', collect_result=False)
+
+ finally:
+ lldb.SBDebugger.Destroy(debugger)
+
+
+ @add_test_categories(['pyapi'])
+ @no_debug_info_test
+ def test_replace_stdout_with_nonfile(self):
+
+ self.skip_windows()
+
+ io = StringIO()
+
+ with replace_stdout(io):
+
+ class Nothing():
+ pass
+
+ debugger = lldb.SBDebugger.Create()
+ try:
+ with replace_stdout(Nothing):
+ self.assertEqual(sys.stdout, Nothing)
+ handle_command(debugger, 'script print("lol, crash")', collect_result=False)
+ self.assertEqual(sys.stdout, Nothing)
+ finally:
+ lldb.SBDebugger.Destroy(debugger)
+
+ sys.stdout.write("FOO")
+
+ self.assertEqual(io.buf, "FOO")
+
+
+ @add_test_categories(['pyapi'])
+ @no_debug_info_test
+ def test_stream_error(self):
+
+ self.skip_windows()
+
+ messages = list()
+
+ io = BadIO()
+ debugger = lldb.SBDebugger.Create()
+ try:
+ debugger.SetOutputFileHandle(io, False)
+ debugger.SetLoggingCallback(messages.append)
+ handle_command(debugger, 'log enable lldb script')
+ handle_command(debugger, 'script print "foobar"')
+
+ finally:
+ lldb.SBDebugger.Destroy(debugger)
+
+ for message in messages:
+ self.comment("GOT: " + message.strip())
+
+ self.assertTrue(any('OH NOE' in msg for msg in messages))
+ self.assertTrue(any('BadIO' in msg for msg in messages))
+
+
void
SetErrorFileHandle (FILE *f, bool transfer_ownership);
+ %feature("docstring",
+ "Flush the Debugger's Output/Error file handles.
+For instance, this is needed by a repl implementation on top of
+the SB API, where fine grained control of output timing was needed."
+ ) FlushDebuggerOutputHandles;
+ void
+ FlushDebuggerOutputHandles ();
+
FILE *
GetInputFileHandle ();
m_opaque_sp->SetInputFileHandle(fh, transfer_ownership);
}
+void SBDebugger::FlushDebuggerOutputHandles() {
+ Log *log(GetLogIfAllCategoriesSet(LIBLLDB_LOG_API));
+
+ if (log)
+ log->Printf(
+ "SBDebugger(%p)::FlushDebuggerOutputHandles ()",
+ static_cast<void *>(m_opaque_sp.get()));
+
+ if (m_opaque_sp)
+ m_opaque_sp->FlushDebuggerOutputHandles();
+}
+
void SBDebugger::SetOutputFileHandle(FILE *fh, bool transfer_ownership) {
Log *log(GetLogIfAllCategoriesSet(LIBLLDB_LOG_API));
if (!debugger_sp)
return;
+ /*
+ * FILE* get flushed on process exit. If those FILEs need to call into python
+ * to flush, we can't have them flushing after python is already torn down.
+ * That would result in a segfault. We are still relying on the python script
+ * to tear down the debugger before it exits.
+ */
+ debugger_sp->m_output_file_sp->Flush();
+ debugger_sp->m_error_file_sp->Flush();
+
debugger_sp->Clear();
if (g_debugger_list_ptr && g_debugger_list_mutex_ptr) {
err_file.SetStream(stderr, false);
}
+void Debugger::FlushDebuggerOutputHandles() {
+ m_output_file_sp->Flush();
+ m_error_file_sp->Flush();
+}
+
void Debugger::SaveInputTerminalState() {
if (m_input_file_sp) {
File &in_file = m_input_file_sp->GetFile();
#include <limits.h>
#include <stdarg.h>
#include <stdio.h>
+#include <assert.h>
#ifdef _WIN32
#include "lldb/Host/windows/windows.h"
int File::kInvalidDescriptor = -1;
FILE *File::kInvalidStream = NULL;
+File::File(File &&rhs)
+ : IOObject(eFDTypeFile, false), m_descriptor(kInvalidDescriptor),
+ m_stream(kInvalidStream), m_options(), m_own_stream(false),
+ m_is_interactive(eLazyBoolCalculate),
+ m_is_real_terminal(eLazyBoolCalculate),
+ m_supports_colors(eLazyBoolCalculate)
+{
+ Swap(rhs);
+}
+
+File& File::operator= (File &&rhs)
+{
+ Close();
+ Swap(rhs);
+ return *this;
+}
+
+void File::Swap(File &rhs)
+{
+ std::swap(m_descriptor, rhs.m_descriptor);
+ std::swap(m_stream, rhs.m_stream);
+ std::swap(m_own_stream, rhs.m_own_stream);
+ std::swap(m_options, rhs.m_options);
+ std::swap(m_is_interactive, rhs.m_is_interactive);
+ std::swap(m_is_real_terminal, rhs.m_is_real_terminal);
+ std::swap(m_supports_colors, rhs.m_supports_colors);
+}
+
+#if defined(__linux__)
+
+struct context {
+ void *cookie;
+ int (*readfn)(void *, char *, int);
+ int (*writefn)(void *, const char *, int);
+ int (*closefn)(void *);
+};
+
+static ssize_t
+write_wrapper(void *c, const char *buf, size_t size)
+{
+ auto ctx = (struct context *)c;
+ if (size > INT_MAX) {
+ size = INT_MAX;
+ }
+ ssize_t wrote = ctx->writefn(ctx->cookie, buf, (int)size);
+ assert(wrote < 0 || (size_t)wrote <= size);
+ if (wrote < 0) {
+ return -1;
+ } else {
+ return (int)wrote;
+ }
+}
+
+static ssize_t
+read_wrapper(void *c, char *buf, size_t size)
+{
+ auto ctx = (struct context *)c;
+ if (size > INT_MAX) {
+ size = INT_MAX;
+ }
+ ssize_t read = ctx->writefn(ctx->cookie, buf, (int)size);
+ assert(read < 0 || (size_t)read <= size);
+ if (read < 0) {
+ return -1;
+ } else {
+ return (int)read;
+ }
+}
+
+static int
+close_wrapper(void *c)
+{
+ auto ctx = (struct context *)c;
+ int ret = ctx->closefn(ctx->cookie);
+ delete ctx;
+ return ret;
+}
+
+#endif
+
+File::File(void *cookie,
+ int (*readfn)(void *, char *, int),
+ int (*writefn)(void *, const char *, int),
+ int (*closefn)(void *))
+ : IOObject(eFDTypeFile, false), m_descriptor(kInvalidDescriptor),
+ m_stream(kInvalidStream), m_options(), m_own_stream(false),
+ m_is_interactive(eLazyBoolCalculate),
+ m_is_real_terminal(eLazyBoolCalculate),
+ m_supports_colors(eLazyBoolCalculate)
+{
+#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__)
+ m_stream = funopen(cookie, readfn, writefn, NULL, closefn);
+#elif defined(__linux__)
+ cookie_io_functions_t io_funcs = {};
+ io_funcs.read = read_wrapper;
+ io_funcs.write = write_wrapper;
+ io_funcs.close = close_wrapper;
+ const char *mode = NULL;
+ if (readfn && writefn) {
+ mode = "r+";
+ } else if (readfn) {
+ mode = "r";
+ } else if (writefn) {
+ mode = "w";
+ }
+ if (mode) {
+ struct context *ctx = new context;
+ ctx->readfn = readfn;
+ ctx->writefn = writefn;
+ ctx->closefn = closefn;
+ ctx->cookie = cookie;
+ m_stream = fopencookie(ctx, mode, io_funcs);
+ if (!m_stream) {
+ delete ctx;
+ }
+ }
+#endif
+ if (m_stream) {
+ m_own_stream = true;
+ }
+}
+
+
File::File(const char *path, uint32_t options, uint32_t permissions)
: IOObject(eFDTypeFile, false), m_descriptor(kInvalidDescriptor),
m_stream(kInvalidStream), m_options(), m_own_stream(false),
#include "PythonDataObjects.h"
#include "ScriptInterpreterPython.h"
+#include "lldb/Utility/Log.h"
#include "lldb/Host/File.h"
#include "lldb/Host/FileSystem.h"
#include "lldb/Interpreter/ScriptInterpreter.h"
bool PythonFile::Check(PyObject *py_obj) {
#if PY_MAJOR_VERSION < 3
- return PyFile_Check(py_obj);
-#else
+ if (PyFile_Check(py_obj)) {
+ return true;
+ }
+#endif
// In Python 3, there is no `PyFile_Check`, and in fact PyFile is not even a
// first-class object type anymore. `PyFile_FromFd` is just a thin wrapper
// over `io.open()`, which returns some object derived from `io.IOBase`.
if (1 != PyObject_IsSubclass(object_type.get(), io_base_class.get()))
return false;
+
if (!object_type.HasAttribute("fileno"))
return false;
return true;
-#endif
}
void PythonFile::Reset(PyRefType type, PyObject *py_obj) {
// `py_obj` it still gets decremented if necessary.
PythonObject result(type, py_obj);
- if (!PythonFile::Check(py_obj)) {
+ if (py_obj == NULL || !PythonFile::Check(py_obj)) {
PythonObject::Reset();
return;
}
.Default(0);
}
+static const char *
+str(PyObject *o, const char *defaultt, PyObject **cleanup)
+{
+ *cleanup = NULL;
+ if (o == NULL) {
+ return defaultt;
+ }
+ PyObject *string = PyObject_Str(o);
+ *cleanup = string;
+ if (string == NULL) {
+ return defaultt;
+ }
+ if (PyUnicode_Check(string)) {
+ PyObject *bytes = PyUnicode_AsEncodedString(string, "utf-8", "Error ~");
+ if (bytes == NULL) {
+ return defaultt;
+ }
+ Py_XDECREF(string);
+ *cleanup = bytes;
+ return PyBytes_AS_STRING(bytes);
+ } else {
+ return PyBytes_AS_STRING(string);
+ }
+}
+
+
+static void
+log_exception(const char *fmt, PyObject *obj)
+{
+ Log *log = GetLogIfAllCategoriesSet(LIBLLDB_LOG_SCRIPT);
+ if (!log) {
+ return;
+ }
+ PyObject *pyclass = PyObject_Type(obj);
+ PyObject *pyclassname = NULL;
+ if (pyclass) {
+ pyclassname = PyObject_GetAttrString(pyclass, "__name__");
+ }
+ PyObject *exception = NULL, *v = NULL, *tb = NULL;
+ PyErr_Fetch(&exception, &v, &tb);
+ if (exception) {
+ PyErr_NormalizeException(&exception, &v, &tb);
+ }
+ PyObject *cleanup1 = NULL, *cleanup2 = NULL;
+ log->Printf(fmt,
+ str(pyclassname, "UknownClass", &cleanup1),
+ str(v, "unknown error", &cleanup2));
+ Py_XDECREF(cleanup1);
+ Py_XDECREF(cleanup2);
+ Py_XDECREF(pyclass);
+ Py_XDECREF(pyclassname);
+ Py_XDECREF(exception);
+ Py_XDECREF(v);
+ Py_XDECREF(tb);
+
+}
+
+class GIL
+{
+private:
+ PyGILState_STATE m_state;
+public:
+ GIL() {
+ m_state = PyGILState_Ensure();
+ }
+ ~GIL() {
+ PyGILState_Release(m_state);
+ }
+};
+
+#define callmethod(obj, name, fmt, ...) \
+ PythonObject(PyRefType::Owned, PyObject_CallMethod(obj, (char*)name, (char*)fmt, __VA_ARGS__))
+
+#define callmethod0(obj, name, fmt) \
+ PythonObject(PyRefType::Owned, PyObject_CallMethod(obj, (char*)name, (char*)fmt))
+
+static int readfn(void *ctx, char *buffer, int n)
+{
+ GIL gil;
+ auto *file = (PyObject *) ctx;
+
+ PythonObject pyresult = callmethod(file, "read", "(i)", n);
+ if (!pyresult.IsValid() || PyErr_Occurred()) {
+ log_exception("read from python %s failed: %s", file);
+ return -1;
+ }
+ if (!PyBytes_Check(pyresult)) {
+ PyErr_SetString(PyExc_TypeError, "read didn't return bytes");
+ log_exception("read from python %s failed: %s", file);
+ return -1;
+ }
+
+ int r = 0;
+ char *data = NULL;
+ ssize_t length = 0;
+ r = PyBytes_AsStringAndSize(pyresult, &data, &length);
+ if (r || length < 0 || PyErr_Occurred()) {
+ log_exception("read from python %s failed: %s", file);
+ return -1;
+ }
+
+ memcpy(buffer, data, length);
+ return length;
+}
+
+static int writefn(void *ctx, const char *buffer, int n)
+{
+ GIL gil;
+ auto *file = (PyObject *) ctx;
+
+ PythonObject pyresult = callmethod(file, "write", "(s#)", buffer, n);
+ if (!pyresult.IsValid() || PyErr_Occurred()) {
+ log_exception("write to python %s failed: %s", file);
+ return -1;
+ }
+
+ int result = PyLong_AsLong(pyresult);
+ if (PyErr_Occurred()) {
+ log_exception("read from python %s failed: %s", file);
+ return -1;
+ }
+
+ return result;
+}
+
+static int closefn(void *ctx) {
+ GIL gil;
+ auto *file = (PyObject *) ctx;
+ Py_XDECREF(file);
+ return 0;
+}
+
bool PythonFile::GetUnderlyingFile(File &file) const {
if (!IsValid())
return false;
+ if (!PythonFile::Check(m_py_obj))
+ return false;
+
file.Close();
- // We don't own the file descriptor returned by this function, make sure the
- // File object knows about that.
- file.SetDescriptor(PyObject_AsFileDescriptor(m_py_obj), false);
- PythonString py_mode = GetAttributeValue("mode").AsType<PythonString>();
- file.SetOptions(PythonFile::GetOptionsFromMode(py_mode.GetString()));
- return file.IsValid();
+
+ int fd = PyObject_AsFileDescriptor(m_py_obj);
+ if (fd >= 0) {
+ // We don't own the file descriptor returned by this function, make sure the
+ // File object knows about that.
+ file.SetDescriptor(PyObject_AsFileDescriptor(m_py_obj), false);
+ PythonString py_mode = GetAttributeValue("mode").AsType<PythonString>();
+ file.SetOptions(PythonFile::GetOptionsFromMode(py_mode.GetString()));
+ return file.IsValid();
+ }
+
+ bool readable = PyObject_IsTrue(callmethod0(m_py_obj, "readable", "()"));
+ bool writable = PyObject_IsTrue(callmethod0(m_py_obj, "writable", "()"));
+
+ Py_XINCREF(m_py_obj);
+ file = File(m_py_obj, readable ? readfn : NULL, writable ? writefn : NULL, closefn);
+ if (!file.IsValid()) {
+ closefn(m_py_obj);
+ return false;
+ } else {
+ return true;
+ }
}
#endif
Reset(PyRefType::Borrowed, rhs.m_py_obj);
}
+ operator PyObject*() const { return m_py_obj; }
+
// PythonObject is implicitly convertible to PyObject *, which will call the
// wrong overload. We want to explicitly disallow this, since a PyObject
// *always* owns its reference. Therefore the overload which takes a
}
bool ScriptInterpreterPython::SetStdHandle(File &file, const char *py_name,
- PythonFile &save_file,
+ PythonObject &save_file,
const char *mode) {
if (file.IsValid()) {
// Flush the file before giving it to python to avoid interleaved output.
PythonDictionary &sys_module_dict = GetSysModuleDictionary();
- save_file = sys_module_dict.GetItemForKey(PythonString(py_name))
- .AsType<PythonFile>();
+ save_file = sys_module_dict.GetItemForKey(PythonString(py_name));
PythonFile new_file(file, mode);
sys_module_dict.SetItemForKey(PythonString(py_name), new_file);
bool GetEmbeddedInterpreterModuleObjects();
- bool SetStdHandle(File &file, const char *py_name, PythonFile &save_file,
+ bool SetStdHandle(File &file, const char *py_name, PythonObject &save_file,
const char *mode);
- PythonFile m_saved_stdin;
- PythonFile m_saved_stdout;
- PythonFile m_saved_stderr;
+ PythonObject m_saved_stdin;
+ PythonObject m_saved_stdout;
+ PythonObject m_saved_stderr;
PythonObject m_main_module;
PythonObject m_lldb_module;
PythonDictionary m_session_dict;