Index: lldb/trunk/include/lldb/API/SBDebugger.h =================================================================== --- lldb/trunk/include/lldb/API/SBDebugger.h +++ lldb/trunk/include/lldb/API/SBDebugger.h @@ -78,6 +78,8 @@ void SetOutputFileHandle(FILE *f, bool transfer_ownership); void SetErrorFileHandle(FILE *f, bool transfer_ownership); + + void FlushDebuggerOutputHandles(); FILE *GetInputFileHandle(); Index: lldb/trunk/include/lldb/Core/Debugger.h =================================================================== --- lldb/trunk/include/lldb/Core/Debugger.h +++ lldb/trunk/include/lldb/Core/Debugger.h @@ -139,6 +139,8 @@ void SetOutputFileHandle(FILE *fh, bool tranfer_ownership); void SetErrorFileHandle(FILE *fh, bool tranfer_ownership); + + void FlushDebuggerOutputHandles(); void SaveInputTerminalState(); Index: lldb/trunk/include/lldb/Host/File.h =================================================================== --- lldb/trunk/include/lldb/Host/File.h +++ lldb/trunk/include/lldb/Host/File.h @@ -62,6 +62,17 @@ 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. /// @@ -479,9 +490,6 @@ LazyBool m_is_interactive; LazyBool m_is_real_terminal; LazyBool m_supports_colors; - -private: - DISALLOW_COPY_AND_ASSIGN(File); }; } // namespace lldb_private Index: lldb/trunk/packages/Python/lldbsuite/test/python_api/file_handle/TestFileHandle.py =================================================================== --- lldb/trunk/packages/Python/lldbsuite/test/python_api/file_handle/TestFileHandle.py +++ lldb/trunk/packages/Python/lldbsuite/test/python_api/file_handle/TestFileHandle.py @@ -0,0 +1,228 @@ +""" +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)) + + Index: lldb/trunk/scripts/interface/SBDebugger.i =================================================================== --- lldb/trunk/scripts/interface/SBDebugger.i +++ lldb/trunk/scripts/interface/SBDebugger.i @@ -171,6 +171,14 @@ 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 (); Index: lldb/trunk/source/API/SBDebugger.cpp =================================================================== --- lldb/trunk/source/API/SBDebugger.cpp +++ lldb/trunk/source/API/SBDebugger.cpp @@ -270,6 +270,18 @@ 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(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)); Index: lldb/trunk/source/Core/Debugger.cpp =================================================================== --- lldb/trunk/source/Core/Debugger.cpp +++ lldb/trunk/source/Core/Debugger.cpp @@ -680,6 +680,15 @@ 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) { @@ -896,6 +905,11 @@ 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(); Index: lldb/trunk/source/Host/common/File.cpp =================================================================== --- lldb/trunk/source/Host/common/File.cpp +++ lldb/trunk/source/Host/common/File.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #ifdef _WIN32 #include "lldb/Host/windows/windows.h" @@ -71,6 +72,129 @@ 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), Index: lldb/trunk/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.h =================================================================== --- lldb/trunk/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.h +++ lldb/trunk/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.h @@ -113,6 +113,8 @@ 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 Index: lldb/trunk/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.cpp =================================================================== --- lldb/trunk/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.cpp +++ lldb/trunk/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.cpp @@ -17,6 +17,7 @@ #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" @@ -959,8 +960,10 @@ 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`. @@ -977,11 +980,11 @@ 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) { @@ -989,7 +992,7 @@ // `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; } @@ -1034,17 +1037,168 @@ .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(); - 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(); + 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 Index: lldb/trunk/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.h =================================================================== --- lldb/trunk/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.h +++ lldb/trunk/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.h @@ -541,12 +541,12 @@ 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; Index: lldb/trunk/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp =================================================================== --- lldb/trunk/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp +++ lldb/trunk/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp @@ -504,7 +504,7 @@ } 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. @@ -512,8 +512,7 @@ PythonDictionary &sys_module_dict = GetSysModuleDictionary(); - save_file = sys_module_dict.GetItemForKey(PythonString(py_name)) - .AsType(); + save_file = sys_module_dict.GetItemForKey(PythonString(py_name)); PythonFile new_file(file, mode); sys_module_dict.SetItemForKey(PythonString(py_name), new_file);