diff --git a/lldb/include/lldb/API/SBFile.h b/lldb/include/lldb/API/SBFile.h --- a/lldb/include/lldb/API/SBFile.h +++ b/lldb/include/lldb/API/SBFile.h @@ -18,6 +18,7 @@ public: SBFile(); + SBFile(FileSP file_sp); SBFile(FILE *file, bool transfer_ownership); SBFile(int fd, const char *mode, bool transfer_ownership); ~SBFile(); @@ -33,7 +34,6 @@ private: FileSP m_opaque_sp; - SBFile(FileSP file_sp); }; } // namespace lldb diff --git a/lldb/packages/Python/lldbsuite/test/python_api/file_handle/TestFileHandle.py b/lldb/packages/Python/lldbsuite/test/python_api/file_handle/TestFileHandle.py --- a/lldb/packages/Python/lldbsuite/test/python_api/file_handle/TestFileHandle.py +++ b/lldb/packages/Python/lldbsuite/test/python_api/file_handle/TestFileHandle.py @@ -13,8 +13,53 @@ import lldb from lldbsuite.test import lldbtest from lldbsuite.test.decorators import ( - add_test_categories, no_debug_info_test, skipIf) - + add_test_categories, skipIf, skipIfWindows) + +class OhNoe(Exception): + pass + +class BadIO(io.TextIOBase): + @property + def closed(self): + return False + def writable(self): + return True + def readable(self): + return True + def write(self, s): + raise OhNoe('OH NOE') + def read(self, n): + raise OhNoe("OH NOE") + def flush(self): + raise OhNoe('OH NOE') + +# This class will raise an exception while it's being +# converted into a C++ object by swig +class ReallyBadIO(io.TextIOBase): + def fileno(self): + return 999 + def writable(self): + raise OhNoe("OH NOE!!!") + +class MutableBool(): + def __init__(self, value): + self.value = value + def set(self, value): + self.value = bool(value) + def __bool__(self): + return self.value + +class FlushTestIO(io.StringIO): + def __init__(self, mutable_flushed, mutable_closed): + super(FlushTestIO, self).__init__() + self.mut_flushed = mutable_flushed + self.mut_closed = mutable_closed + def close(self): + self.mut_closed.set(True) + return super(FlushTestIO, self).close() + def flush(self): + self.mut_flushed.set(True) + return super(FlushTestIO, self).flush() @contextmanager def replace_stdout(new): @@ -36,6 +81,7 @@ class FileHandleTestCase(lldbtest.TestBase): + NO_DEBUG_INFO_TESTCASE = True mydir = lldbtest.Base.compute_mydir(__file__) # The way this class interacts with the debugger is different @@ -84,7 +130,8 @@ @add_test_categories(['pyapi']) - @no_debug_info_test + @skipIfWindows # FIXME pre-existing bug, should be fixed + # when we delete the FILE* typemaps. def test_legacy_file_out_script(self): with open(self.out_filename, 'w') as f: self.debugger.SetOutputFileHandle(f, False) @@ -100,7 +147,6 @@ @add_test_categories(['pyapi']) - @no_debug_info_test def test_legacy_file_out(self): with open(self.out_filename, 'w') as f: self.debugger.SetOutputFileHandle(f, False) @@ -110,7 +156,8 @@ self.assertIn('deadbeef', f.read()) @add_test_categories(['pyapi']) - @no_debug_info_test + @skipIfWindows # FIXME pre-existing bug, should be fixed + # when we delete the FILE* typemaps. def test_legacy_file_err_with_get(self): with open(self.out_filename, 'w') as f: self.debugger.SetErrorFileHandle(f, False) @@ -124,7 +171,6 @@ @add_test_categories(['pyapi']) - @no_debug_info_test def test_legacy_file_err(self): with open(self.out_filename, 'w') as f: self.debugger.SetErrorFileHandle(f, False) @@ -135,7 +181,6 @@ @add_test_categories(['pyapi']) - @no_debug_info_test def test_sbfile_type_errors(self): sbf = lldb.SBFile() self.assertRaises(TypeError, sbf.Write, None) @@ -146,8 +191,7 @@ @add_test_categories(['pyapi']) - @no_debug_info_test - def test_sbfile_write(self): + def test_sbfile_write_fileno(self): with open(self.out_filename, 'w') as f: sbf = lldb.SBFile(f.fileno(), "w", False) self.assertTrue(sbf.IsValid()) @@ -161,8 +205,20 @@ @add_test_categories(['pyapi']) - @no_debug_info_test - def test_sbfile_read(self): + def test_sbfile_write(self): + with open(self.out_filename, 'w') as f: + sbf = lldb.SBFile(f) + e, n = sbf.Write(b'FOO\n') + self.assertTrue(e.Success()) + self.assertEqual(n, 4) + sbf.Close() + self.assertTrue(f.closed) + with open(self.out_filename, 'r') as f: + self.assertEqual(f.read().strip(), 'FOO') + + + @add_test_categories(['pyapi']) + def test_sbfile_read_fileno(self): with open(self.out_filename, 'w') as f: f.write('FOO') with open(self.out_filename, 'r') as f: @@ -175,7 +231,21 @@ @add_test_categories(['pyapi']) - @no_debug_info_test + def test_sbfile_read(self): + with open(self.out_filename, 'w') as f: + f.write('foo') + with open(self.out_filename, 'r') as f: + sbf = lldb.SBFile(f) + buf = bytearray(100) + e, n = sbf.Read(buf) + self.assertTrue(e.Success()) + self.assertEqual(n, 3) + self.assertEqual(buf[:n], b'foo') + sbf.Close() + self.assertTrue(f.closed) + + + @add_test_categories(['pyapi']) def test_fileno_out(self): with open(self.out_filename, 'w') as f: sbf = lldb.SBFile(f.fileno(), "w", False) @@ -189,7 +259,6 @@ @add_test_categories(['pyapi']) - @no_debug_info_test def test_fileno_help(self): with open(self.out_filename, 'w') as f: sbf = lldb.SBFile(f.fileno(), "w", False) @@ -201,7 +270,6 @@ @add_test_categories(['pyapi']) - @no_debug_info_test def test_immediate(self): with open(self.out_filename, 'w') as f: ret = lldb.SBCommandReturnObject() @@ -220,7 +288,6 @@ @add_test_categories(['pyapi']) - @no_debug_info_test def test_fileno_inout(self): with open(self.in_filename, 'w') as f: f.write("help help\n") @@ -244,7 +311,6 @@ @add_test_categories(['pyapi']) - @no_debug_info_test def test_fileno_error(self): with open(self.out_filename, 'w') as f: @@ -263,7 +329,6 @@ #FIXME This shouldn't fail for python2 either. @add_test_categories(['pyapi']) - @no_debug_info_test @skipIf(py_version=['<', (3,)]) def test_replace_stdout(self): f = io.StringIO() @@ -272,3 +337,222 @@ self.handleCmd('script sys.stdout.write("lol")', collect_result=False, check=False) self.assertEqual(sys.stdout, f) + + + @add_test_categories(['pyapi']) + def test_sbfile_write_borrowed(self): + with open(self.out_filename, 'w') as f: + sbf = lldb.SBFile.Create(f, borrow=True) + e, n = sbf.Write(b'FOO') + self.assertTrue(e.Success()) + self.assertEqual(n, 3) + sbf.Close() + self.assertFalse(f.closed) + f.write('BAR\n') + with open(self.out_filename, 'r') as f: + self.assertEqual(f.read().strip(), 'FOOBAR') + + + + @add_test_categories(['pyapi']) + @skipIf(py_version=['<', (3,)]) + def test_sbfile_write_forced(self): + with open(self.out_filename, 'w') as f: + written = MutableBool(False) + orig_write = f.write + def mywrite(x): + written.set(True) + return orig_write(x) + f.write = mywrite + sbf = lldb.SBFile.Create(f, force_io_methods=True) + e, n = sbf.Write(b'FOO') + self.assertTrue(written) + self.assertTrue(e.Success()) + self.assertEqual(n, 3) + sbf.Close() + self.assertTrue(f.closed) + with open(self.out_filename, 'r') as f: + self.assertEqual(f.read().strip(), 'FOO') + + + @add_test_categories(['pyapi']) + @skipIf(py_version=['<', (3,)]) + def test_sbfile_write_forced_borrowed(self): + with open(self.out_filename, 'w') as f: + written = MutableBool(False) + orig_write = f.write + def mywrite(x): + written.set(True) + return orig_write(x) + f.write = mywrite + sbf = lldb.SBFile.Create(f, borrow=True, force_io_methods=True) + e, n = sbf.Write(b'FOO') + self.assertTrue(written) + self.assertTrue(e.Success()) + self.assertEqual(n, 3) + sbf.Close() + self.assertFalse(f.closed) + with open(self.out_filename, 'r') as f: + self.assertEqual(f.read().strip(), 'FOO') + + + @add_test_categories(['pyapi']) + @skipIf(py_version=['<', (3,)]) + def test_sbfile_write_string(self): + f = io.StringIO() + sbf = lldb.SBFile(f) + e, n = sbf.Write(b'FOO') + self.assertEqual(f.getvalue().strip(), "FOO") + self.assertTrue(e.Success()) + self.assertEqual(n, 3) + sbf.Close() + self.assertTrue(f.closed) + + @add_test_categories(['pyapi']) + @skipIf(py_version=['<', (3,)]) + def test_sbfile_write_bytes(self): + f = io.BytesIO() + sbf = lldb.SBFile(f) + e, n = sbf.Write(b'FOO') + self.assertEqual(f.getvalue().strip(), b"FOO") + self.assertTrue(e.Success()) + self.assertEqual(n, 3) + sbf.Close() + self.assertTrue(f.closed) + + @add_test_categories(['pyapi']) + @skipIf(py_version=['<', (3,)]) + def test_sbfile_read_string(self): + f = io.StringIO('zork') + sbf = lldb.SBFile(f) + buf = bytearray(100) + e, n = sbf.Read(buf) + self.assertTrue(e.Success()) + self.assertEqual(buf[:n], b'zork') + + + @add_test_categories(['pyapi']) + @skipIf(py_version=['<', (3,)]) + def test_sbfile_read_string_one_byte(self): + f = io.StringIO('z') + sbf = lldb.SBFile(f) + buf = bytearray(1) + e, n = sbf.Read(buf) + self.assertTrue(e.Fail()) + self.assertEqual(n, 0) + self.assertEqual(e.GetCString(), "can't read less than 6 bytes from a utf8 text stream") + + + @add_test_categories(['pyapi']) + @skipIf(py_version=['<', (3,)]) + def test_sbfile_read_bytes(self): + f = io.BytesIO(b'zork') + sbf = lldb.SBFile(f) + buf = bytearray(100) + e, n = sbf.Read(buf) + self.assertTrue(e.Success()) + self.assertEqual(buf[:n], b'zork') + + + @add_test_categories(['pyapi']) + @skipIf(py_version=['<', (3,)]) + def test_sbfile_out(self): + with open(self.out_filename, 'w') as f: + sbf = lldb.SBFile(f) + status = self.debugger.SetOutputFile(sbf) + self.assertTrue(status.Success()) + self.handleCmd('script 2+2') + with open(self.out_filename, 'r') as f: + self.assertEqual(f.read().strip(), '4') + + + @add_test_categories(['pyapi']) + def test_sbfile_error(self): + with open(self.out_filename, 'w') as f: + sbf = lldb.SBFile(f) + status = self.debugger.SetErrorFile(sbf) + self.assertTrue(status.Success()) + self.handleCmd('lolwut', check=False, collect_result=False) + with open(self.out_filename, 'r') as f: + errors = f.read() + self.assertTrue(re.search(r'error:.*lolwut', errors)) + + + @add_test_categories(['pyapi']) + def test_exceptions(self): + self.assertRaises(TypeError, lldb.SBFile, None) + self.assertRaises(TypeError, lldb.SBFile, "ham sandwich") + if sys.version_info[0] < 3: + self.assertRaises(TypeError, lldb.SBFile, ReallyBadIO()) + else: + self.assertRaises(OhNoe, lldb.SBFile, ReallyBadIO()) + error, n = lldb.SBFile(BadIO()).Write(b"FOO") + self.assertEqual(n, 0) + self.assertTrue(error.Fail()) + self.assertEqual(error.GetCString(), "OhNoe('OH NOE')") + error, n = lldb.SBFile(BadIO()).Read(bytearray(100)) + self.assertEqual(n, 0) + self.assertTrue(error.Fail()) + self.assertEqual(error.GetCString(), "OhNoe('OH NOE')") + + + @add_test_categories(['pyapi']) + @skipIf(py_version=['<', (3,)]) + def test_exceptions_logged(self): + messages = list() + self.debugger.SetLoggingCallback(messages.append) + self.handleCmd('log enable lldb script') + self.debugger.SetOutputFile(lldb.SBFile(BadIO())) + self.handleCmd('script 1+1') + self.assertTrue(any('OH NOE' in msg for msg in messages)) + + + @add_test_categories(['pyapi']) + @skipIf(py_version=['<', (3,)]) + def test_flush(self): + flushed = MutableBool(False) + closed = MutableBool(False) + f = FlushTestIO(flushed, closed) + self.assertFalse(flushed) + self.assertFalse(closed) + sbf = lldb.SBFile(f) + self.assertFalse(flushed) + self.assertFalse(closed) + sbf = None + self.assertFalse(flushed) + self.assertTrue(closed) + self.assertTrue(f.closed) + + flushed = MutableBool(False) + closed = MutableBool(False) + f = FlushTestIO(flushed, closed) + self.assertFalse(flushed) + self.assertFalse(closed) + sbf = lldb.SBFile.Create(f, borrow=True) + self.assertFalse(flushed) + self.assertFalse(closed) + sbf = None + self.assertTrue(flushed) + self.assertFalse(closed) + self.assertFalse(f.closed) + + + @add_test_categories(['pyapi']) + def test_fileno_flush(self): + with open(self.out_filename, 'w') as f: + f.write("foo") + sbf = lldb.SBFile(f) + sbf.Write(b'bar') + sbf = None + self.assertTrue(f.closed) + with open(self.out_filename, 'r') as f: + self.assertEqual(f.read(), 'foobar') + + with open(self.out_filename, 'w+') as f: + f.write("foo") + sbf = lldb.SBFile.Create(f, borrow=True) + sbf.Write(b'bar') + sbf = None + self.assertFalse(f.closed) + f.seek(0) + self.assertEqual(f.read(), 'foobar') diff --git a/lldb/scripts/Python/python-typemaps.swig b/lldb/scripts/Python/python-typemaps.swig --- a/lldb/scripts/Python/python-typemaps.swig +++ b/lldb/scripts/Python/python-typemaps.swig @@ -372,6 +372,65 @@ $1 = $1 || PyCallable_Check(reinterpret_cast($input)); } + +%typemap(in) lldb::FileSP { + using namespace lldb_private; + if (PythonFile::Check($input)) { + PythonFile py_file(PyRefType::Borrowed, $input); + auto sp = unwrapOrSetPythonException(py_file.ConvertToFile()); + if (sp) + $1 = sp; + else + return nullptr; + } +} + +%typemap(in) lldb::FileSP FORCE_IO_METHODS { + using namespace lldb_private; + if (PythonFile::Check($input)) { + PythonFile py_file(PyRefType::Borrowed, $input); + auto sp = unwrapOrSetPythonException(py_file.ConvertToFileForcingUseOfScriptingIOMethods()); + if (sp) + $1 = sp; + else + return nullptr; + } +} + +%typemap(in) lldb::FileSP BORROWED { + using namespace lldb_private; + if (PythonFile::Check($input)) { + PythonFile py_file(PyRefType::Borrowed, $input); + auto sp = unwrapOrSetPythonException(py_file.ConvertToFile(/*borrowed=*/true)); + if (sp) + $1 = sp; + else + return nullptr; + } +} + +%typemap(in) lldb::FileSP BORROWED_FORCE_IO_METHODS { + using namespace lldb_private; + if (PythonFile::Check($input)) { + PythonFile py_file(PyRefType::Borrowed, $input); + auto sp = unwrapOrSetPythonException(py_file.ConvertToFileForcingUseOfScriptingIOMethods(/*borrowed=*/true)); + if (sp) + $1 = sp; + else + return nullptr; + } +} + +%typecheck(SWIG_TYPECHECK_POINTER) lldb::FileSP { + if (lldb_private::PythonFile::Check($input)) { + $1 = 1; + } else { + PyErr_Clear(); + $1 = 0; + } +} + + // FIXME both of these paths wind up calling fdopen() with no provision for ever calling // fclose() on the result. SB interfaces that use FILE* should be deprecated for scripting // use and this typemap should eventually be removed. diff --git a/lldb/scripts/interface/SBFile.i b/lldb/scripts/interface/SBFile.i --- a/lldb/scripts/interface/SBFile.i +++ b/lldb/scripts/interface/SBFile.i @@ -15,9 +15,53 @@ class SBFile { public: + SBFile(); + + %feature("docstring", " + Initialize a SBFile from a file descriptor. mode is + 'r', 'r+', or 'w', like fdopen."); SBFile(int fd, const char *mode, bool transfer_ownership); + %feature("docstring", "initialize a SBFile from a python file object"); + SBFile(FileSP file); + + %extend { + static lldb::SBFile MakeBorrowed(lldb::FileSP BORROWED) { + return lldb::SBFile(BORROWED); + } + static lldb::SBFile MakeForcingIOMethods(lldb::FileSP FORCE_IO_METHODS) { + return lldb::SBFile(FORCE_IO_METHODS); + } + static lldb::SBFile MakeBorrowedForcingIOMethods(lldb::FileSP BORROWED_FORCE_IO_METHODS) { + return lldb::SBFile(BORROWED_FORCE_IO_METHODS); + } + } + + %pythoncode { + @classmethod + def Create(cls, file, borrow=False, force_io_methods=False): + """ + Create a SBFile from a python file object, with options. + + If borrow is set then the underlying file will + not be closed when the SBFile is closed or destroyed. + + If force_scripting_io is set then the python read/write + methods will be called even if a file descriptor is available. + """ + if borrow: + if force_io_methods: + return cls.MakeBorrowedForcingIOMethods(file) + else: + return cls.MakeBorrowed(file) + else: + if force_io_methods: + return cls.MakeForcingIOMethods(file) + else: + return cls(file) + } + ~SBFile (); %feature("autodoc", "Read(buffer) -> SBError, bytes_read") Read; diff --git a/lldb/source/API/SBFile.cpp b/lldb/source/API/SBFile.cpp --- a/lldb/source/API/SBFile.cpp +++ b/lldb/source/API/SBFile.cpp @@ -16,7 +16,9 @@ SBFile::~SBFile() {} -SBFile::SBFile(FileSP file_sp) : m_opaque_sp(file_sp) {} +SBFile::SBFile(FileSP file_sp) : m_opaque_sp(file_sp) { + LLDB_RECORD_DUMMY(void, SBfile, SBFile, (FileSP), file_sp); +} SBFile::SBFile() { LLDB_RECORD_CONSTRUCTOR_NO_ARGS(SBFile); } diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.h b/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.h --- a/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.h +++ b/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.h @@ -84,14 +84,19 @@ PythonObject(const PythonObject &rhs) : m_py_obj(nullptr) { Reset(rhs); } + PythonObject(PythonObject &&rhs) { + m_py_obj = rhs.m_py_obj; + rhs.m_py_obj = nullptr; + } + virtual ~PythonObject() { Reset(); } void Reset() { // Avoid calling the virtual method since it's not necessary // to actually validate the type of the PyObject if we're // just setting to null. - if (Py_IsInitialized()) - Py_XDECREF(m_py_obj); + if (m_py_obj && Py_IsInitialized()) + Py_DECREF(m_py_obj); m_py_obj = nullptr; } @@ -123,7 +128,7 @@ // an owned reference by incrementing it. If it is an owned // reference (for example the caller allocated it with PyDict_New() // then we must *not* increment it. - if (Py_IsInitialized() && type == PyRefType::Borrowed) + if (m_py_obj && Py_IsInitialized() && type == PyRefType::Borrowed) Py_XINCREF(m_py_obj); } @@ -467,8 +472,38 @@ void Reset(File &file, const char *mode); lldb::FileUP GetUnderlyingFile() const; + + llvm::Expected ConvertToFile(bool borrowed = false); + llvm::Expected + ConvertToFileForcingUseOfScriptingIOMethods(bool borrowed = false); }; +class PythonException : public llvm::ErrorInfo { +private: + PyObject *m_exception_type, *m_exception, *m_traceback; + PyObject *m_repr_bytes; + +public: + static char ID; + const char *toCString() const; + PythonException(const char *caller); + void Restore(); + ~PythonException(); + void log(llvm::raw_ostream &OS) const override; + std::error_code convertToErrorCode() const override; +}; + +template T unwrapOrSetPythonException(llvm::Expected expected) { + if (expected) + return expected.get(); + llvm::handleAllErrors( + expected.takeError(), [](PythonException &E) { E.Restore(); }, + [](const llvm::ErrorInfoBase &E) { + PyErr_SetString(PyExc_Exception, E.message().c_str()); + }); + return T(); +} + } // namespace lldb_private #endif diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.cpp --- a/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.cpp +++ b/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.cpp @@ -18,6 +18,7 @@ #include "lldb/Host/File.h" #include "lldb/Host/FileSystem.h" #include "lldb/Interpreter/ScriptInterpreter.h" +#include "lldb/Utility/Log.h" #include "lldb/Utility/Stream.h" #include "llvm/ADT/StringSwitch.h" @@ -33,6 +34,14 @@ s.value(llvm::formatv("Python Obj: {0:X}", GetValue()).str()); } +template static T Take(PyObject * obj) { + return T(PyRefType::Owned, obj); +} + +template static T Retain(PyObject * obj) { + return T(PyRefType::Borrowed, obj); +} + // PythonObject void PythonObject::Dump(Stream &strm) const { @@ -963,20 +972,19 @@ // first-class object type anymore. `PyFile_FromFd` is just a thin wrapper // over `io.open()`, which returns some object derived from `io.IOBase`. As a // result, the only way to detect a file in Python 3 is to check whether it - // inherits from `io.IOBase`. Since it is possible for non-files to also - // inherit from `io.IOBase`, we additionally verify that it has the `fileno` - // attribute, which should guarantee that it is backed by the file system. + // inherits from `io.IOBase`. PythonObject io_module(PyRefType::Owned, PyImport_ImportModule("io")); PythonDictionary io_dict(PyRefType::Borrowed, PyModule_GetDict(io_module.get())); PythonObject io_base_class = io_dict.GetItemForKey(PythonString("IOBase")); + assert(!PyErr_Occurred()); PythonObject object_type(PyRefType::Owned, PyObject_Type(py_obj)); - if (1 != PyObject_IsSubclass(object_type.get(), io_base_class.get())) - return false; - if (!object_type.HasAttribute("fileno")) + if (!PyObject_IsSubclass(object_type.get(), io_base_class.get())) { + PyErr_Clear(); return false; + } return true; #endif @@ -1031,4 +1039,445 @@ return file; } +namespace { +class GIL { +public: + GIL() { + m_state = PyGILState_Ensure(); + assert(!PyErr_Occurred()); + } + ~GIL() { PyGILState_Release(m_state); } + +protected: + PyGILState_STATE m_state; +}; +} + +const char *PythonException::toCString() const { + if (m_repr_bytes) { + return PyBytes_AS_STRING(m_repr_bytes); + } else { + return "unknown exception"; + } +} + +PythonException::PythonException(const char *caller) { + assert(PyErr_Occurred()); + m_exception_type = m_exception = m_traceback = m_repr_bytes = NULL; + PyErr_Fetch(&m_exception_type, &m_exception, &m_traceback); + PyErr_NormalizeException(&m_exception_type, &m_exception, &m_traceback); + if (m_exception) { + PyObject *repr = PyObject_Repr(m_exception); + if (repr) { + m_repr_bytes = PyUnicode_AsEncodedString(repr, "utf-8", nullptr); + Py_XDECREF(repr); + } + } + + Log *log = GetLogIfAllCategoriesSet(LIBLLDB_LOG_SCRIPT); + LLDB_LOGF(log, "%s failed with exception: %s", caller, toCString()); + PyErr_Clear(); +} +void PythonException::Restore() { + if (m_exception_type && m_exception) { + PyErr_Restore(m_exception_type, m_exception, m_traceback); + } else { + PyErr_SetString(PyExc_Exception, toCString()); + } + m_exception_type = m_exception = m_traceback = NULL; +} + +PythonException::~PythonException() { + Py_XDECREF(m_exception_type); + Py_XDECREF(m_exception); + Py_XDECREF(m_traceback); + Py_XDECREF(m_repr_bytes); +} + +void PythonException::log(llvm::raw_ostream & OS) const { OS << toCString(); } + +std::error_code PythonException::convertToErrorCode() const { + return llvm::inconvertibleErrorCode(); +} + +char PythonException::ID = 0; + +llvm::Expected GetOptionsForPyObject(PythonObject & obj) { + uint32_t options = 0; +#if PY_MAJOR_VERSION >= 3 + auto readable = + Take(PyObject_CallMethod(obj.get(), "readable", "()")); + if (PyErr_Occurred()) + return llvm::make_error("ConvertToFile"); + auto writable = + Take(PyObject_CallMethod(obj.get(), "writable", "()")); + if (PyErr_Occurred()) + return llvm::make_error("ConvertToFile"); + if (PyObject_IsTrue(readable.get())) + options |= File::eOpenOptionRead; + if (PyObject_IsTrue(writable.get())) + options |= File::eOpenOptionWrite; +#else + PythonString py_mode = obj.GetAttributeValue("mode").AsType(); + options = File::GetOptionsFromMode(py_mode.GetString()); +#endif + return options; +} + +// Base class template for python files. All it knows how to do +// is hold a reference to the python object and close or flush it +// when the File is closed. +namespace{ +template class OwnedPythonFile : public Base { +public: + template + OwnedPythonFile(const PythonFile &file, bool borrowed, Args... args) + : Base(args...), m_py_obj(file.get()), m_borrowed(borrowed) { + assert(m_py_obj); + Py_INCREF(m_py_obj); + } + + ~OwnedPythonFile() override { + assert(m_py_obj); + GIL takeGIL; + Close(); + Py_DECREF(m_py_obj); + m_py_obj = nullptr; + } + + bool IsValid() const override { + GIL takeGIL; + auto closed = + Take(PyObject_GetAttrString(m_py_obj, "closed")); + if (!closed.IsValid() || PyErr_Occurred()) { + PyErr_Clear(); + return false; + } + if (PyObject_IsTrue(closed.get())) + return false; + if (PyErr_Occurred()) { + PyErr_Clear(); + return false; + } + return Base::IsValid(); + } + + Status Close() override { + assert(m_py_obj); + Status py_error, base_error; + GIL takeGIL; + if (!m_borrowed) { + Take(PyObject_CallMethod(m_py_obj, "close", "()")); + if (PyErr_Occurred()) + py_error = llvm::make_error("Close"); + } + base_error = Base::Close(); + if (py_error.Fail()) + return py_error; + return base_error; + }; + +protected: + PyObject *m_py_obj; + bool m_borrowed; +}; +} + +// A SimplePythonFile is a OwnedPythonFile that just does all I/O as +// a NativeFile +namespace { +class SimplePythonFile : public OwnedPythonFile { +public: + SimplePythonFile(const PythonFile &file, bool borrowed, int fd, + uint32_t options) + : OwnedPythonFile(file, borrowed, fd, options, false){}; +}; +} + + +#if PY_MAJOR_VERSION >= 3 + +namespace { +class PythonBuffer { +public: + // you must check PyErr_Occurred() after calling this constructor. + PythonBuffer(PythonObject &obj, int flags = PyBUF_SIMPLE) : m_buffer({}) { + PyObject_GetBuffer(obj.get(), &m_buffer, flags); + } + ~PythonBuffer() { + if (m_buffer.obj) { + PyBuffer_Release(&m_buffer); + } + } + Py_buffer &get() { return m_buffer; } + +protected: + Py_buffer m_buffer; +}; +} + +// OwnedPythonFile::IsValid() chains into Base::IsValid() +// File::IsValid() is false by default, but for the following classes +// we want the file to be considered valid as long as the python bits +// are valid. +namespace { +class PresumptivelyValidFile : public File { +public: + bool IsValid() const override { return true; }; +}; +} + +// Shared methods between TextPythonFile and BinaryPythonFile +namespace { +class PythonIOFile : public OwnedPythonFile { +public: + PythonIOFile(const PythonFile &file, bool borrowed) + : OwnedPythonFile(file, borrowed){}; + + ~PythonIOFile() override { Close(); } + + Status Close() override { + assert(m_py_obj); + GIL takeGIL; + if (m_borrowed) + return Flush(); + Take(PyObject_CallMethod(m_py_obj, "close", "()")); + if (PyErr_Occurred()) + return Status(llvm::make_error("Close")); + return Status(); + }; + + Status Flush() override { + GIL takeGIL; + Take(PyObject_CallMethod(m_py_obj, "flush", "()")); + Status error; + if (PyErr_Occurred()) + error = llvm::make_error("Flush"); + return error; + } +}; +} + +namespace { +class BinaryPythonFile : public PythonIOFile { + friend class PythonFile; + +protected: + int m_descriptor; + +public: + BinaryPythonFile(int fd, const PythonFile &file, bool borrowed) + : PythonIOFile(file, borrowed), + m_descriptor(File::DescriptorIsValid(fd) ? fd + : File::kInvalidDescriptor) {} + + int GetDescriptor() const override { return m_descriptor; } + + Status Write(const void *buf, size_t &num_bytes) override { + GIL takeGIL; + auto pybuffer = Take(PyMemoryView_FromMemory( + const_cast((const char *)buf), num_bytes, PyBUF_READ)); + num_bytes = 0; + auto bytes_written = Take( + PyObject_CallMethod(m_py_obj, "write", "(O)", pybuffer.get())); + if (PyErr_Occurred()) + return Status(llvm::make_error("Write")); + long l_bytes_written = PyLong_AsLong(bytes_written.get()); + if (PyErr_Occurred()) + return Status(llvm::make_error("Write")); + num_bytes = l_bytes_written; + if (l_bytes_written < 0 || (unsigned long)l_bytes_written != num_bytes) { + return Status("overflow"); + } + return Status(); + } + + Status Read(void *buf, size_t &num_bytes) override { + GIL takeGIL; + auto pybuffer_obj = Take(PyObject_CallMethod( + m_py_obj, "read", "(L)", (unsigned long long)num_bytes)); + num_bytes = 0; + if (PyErr_Occurred()) + return Status(llvm::make_error("Read")); + if (pybuffer_obj.IsNone()) { + // EOF + num_bytes = 0; + return Status(); + } + PythonBuffer pybuffer(pybuffer_obj); + if (PyErr_Occurred()) + return Status(llvm::make_error("Read")); + memcpy(buf, pybuffer.get().buf, pybuffer.get().len); + num_bytes = pybuffer.get().len; + return Status(); + } +}; +} + +namespace { +class TextPythonFile : public PythonIOFile { + friend class PythonFile; + +protected: + int m_descriptor; + +public: + TextPythonFile(int fd, const PythonFile &file, bool borrowed) + : PythonIOFile(file, borrowed), + m_descriptor(File::DescriptorIsValid(fd) ? fd + : File::kInvalidDescriptor) {} + + int GetDescriptor() const override { return m_descriptor; } + + Status Write(const void *buf, size_t &num_bytes) override { + GIL takeGIL; + auto pystring = Take( + PyUnicode_FromStringAndSize((const char *)buf, num_bytes)); + num_bytes = 0; + if (PyErr_Occurred()) + return Status(llvm::make_error("Write")); + auto bytes_written = Take( + PyObject_CallMethod(m_py_obj, "write", "(O)", pystring.get())); + if (PyErr_Occurred()) + return Status(llvm::make_error("Write")); + long l_bytes_written = PyLong_AsLong(bytes_written.get()); + if (PyErr_Occurred()) + return Status(llvm::make_error("Write")); + num_bytes = l_bytes_written; + if (l_bytes_written < 0 || (unsigned long)l_bytes_written != num_bytes) { + return Status("overflow"); + } + return Status(); + } + + Status Read(void *buf, size_t &num_bytes) override { + GIL takeGIL; + size_t num_chars = num_bytes / 6; + size_t orig_num_bytes = num_bytes; + num_bytes = 0; + if (orig_num_bytes < 6) { + return Status("can't read less than 6 bytes from a utf8 text stream"); + } + auto pystring = Take(PyObject_CallMethod( + m_py_obj, "read", "(L)", (unsigned long long)num_chars)); + if (pystring.IsNone()) { + // EOF + return Status(); + } + if (PyErr_Occurred()) + return Status(llvm::make_error("Read")); + if (!PyUnicode_Check(pystring.get())) + return Status("read() didn't return a str"); + if (PyErr_Occurred()) + return Status(llvm::make_error("Read")); + Py_ssize_t size; + const char *utf8 = PyUnicode_AsUTF8AndSize(pystring.get(), &size); + if (!utf8 || PyErr_Occurred()) + return Status(llvm::make_error("Read")); + assert(size >= 0 && (size_t)size <= orig_num_bytes); + memcpy(buf, utf8, size); + num_bytes = size; + return Status(); + } +}; +} + +#endif + +llvm::Expected PythonFile::ConvertToFile(bool borrowed) { + if (!IsValid()) + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "invalid PythonFile"); + + int fd = PyObject_AsFileDescriptor(m_py_obj); + if (fd < 0) { + PyErr_Clear(); + return ConvertToFileForcingUseOfScriptingIOMethods(borrowed); + } + auto options = GetOptionsForPyObject(*this); + if (!options) + return options.takeError(); + + // LLDB and python will not share I/O buffers. We should probably + // flush the python buffers now. + Take(PyObject_CallMethod(m_py_obj, "flush", "()")); + if (PyErr_Occurred()) + return llvm::make_error("Flush"); + + FileSP file_sp; + if (borrowed) { + // In this case we we don't need to retain the python + // object at all. + file_sp = std::make_shared(fd, options.get(), false); + } else { + file_sp = std::static_pointer_cast( + std::make_shared(*this, borrowed, fd, options.get())); + } + if (!file_sp->IsValid()) + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "invalid File"); + + return file_sp; +} + +llvm::Expected PythonFile::ConvertToFileForcingUseOfScriptingIOMethods( + bool borrowed) { + + assert(!PyErr_Occurred()); + + if (!IsValid()) + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "invalid PythonFile"); + +#if PY_MAJOR_VERSION < 3 + + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "not supported on python 2"); + +#else + + int fd = PyObject_AsFileDescriptor(m_py_obj); + if (fd < 0) { + PyErr_Clear(); + fd = File::kInvalidDescriptor; + } + + auto io_module = Take(PyImport_ImportModule("io")); + auto io_dict = Retain(PyModule_GetDict(io_module.get())); + auto textIOBase = io_dict.GetItemForKey(PythonString("TextIOBase")); + auto rawIOBase = io_dict.GetItemForKey(PythonString("BufferedIOBase")); + auto bufferedIOBase = io_dict.GetItemForKey(PythonString("RawIOBase")); + + // python interpreter badly janked if we can't get those. + assert(!PyErr_Occurred()); + + FileSP file_sp; + if (PyObject_IsInstance(m_py_obj, textIOBase.get())) { + file_sp = std::static_pointer_cast( + std::make_shared(fd, *this, borrowed)); + } + if (PyErr_Occurred()) + return llvm::make_error("Convert"); + + if (!file_sp && (PyObject_IsInstance(m_py_obj, rawIOBase.get()) || + PyObject_IsInstance(m_py_obj, bufferedIOBase.get()))) { + file_sp = std::static_pointer_cast( + std::make_shared(fd, *this, borrowed)); + } + if (PyErr_Occurred()) + return llvm::make_error("Convert"); + + if (!file_sp) + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "python file is neither text nor binary"); + + if (!file_sp->IsValid()) + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "invalid File"); + + return file_sp; + +#endif +} + #endif