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 @@ -13,11 +13,23 @@ namespace lldb { +/* These tags make no difference at the c++ level, but + * when the constructors are called from python they control + * how python files are converted by SWIG into FileSP */ +struct FileBorrow {}; +struct FileForceScriptingIO {}; +struct FileBorrowAndForceScriptingIO {}; + class LLDB_API SBFile { friend class SBDebugger; public: SBFile(); + SBFile(FileSP file_sp) : m_opaque_sp(file_sp){}; + SBFile(FileBorrow, FileSP file_sp) : m_opaque_sp(file_sp){}; + SBFile(FileForceScriptingIO, FileSP file_sp) : m_opaque_sp(file_sp){}; + SBFile(FileBorrowAndForceScriptingIO, FileSP file_sp) + : m_opaque_sp(file_sp){}; SBFile(FILE *file, bool transfer_ownership); SBFile(int fd, const char *mode, bool transfer_ownership); ~SBFile(); @@ -33,7 +45,6 @@ private: FileSP m_opaque_sp; - SBFile(FileSP file_sp) : m_opaque_sp(file_sp) {} }; } // namespace lldb diff --git a/lldb/include/lldb/Host/File.h b/lldb/include/lldb/Host/File.h --- a/lldb/include/lldb/Host/File.h +++ b/lldb/include/lldb/Host/File.h @@ -26,6 +26,10 @@ /// /// A file class that divides abstracts the LLDB core from host file /// functionality. +/// +/// Subclasses that override Read() or Write() must also override +/// OverridesIO() -- to return true -- and also Flush(). +/// Other virtual methods are optional. class File : public IOObject { public: static int kInvalidDescriptor; @@ -77,7 +81,7 @@ ~File() override; bool IsValid() const override { - return DescriptorIsValid() || StreamIsValid(); + return DescriptorIsValid() || StreamIsValid() || OverridesIO(); } /// Convert to pointer operator. @@ -94,7 +98,7 @@ /// \return /// A pointer to this object if either the directory or filename /// is valid, nullptr otherwise. - operator bool() const { return DescriptorIsValid() || StreamIsValid(); } + operator bool() const { return IsValid(); } /// Logical NOT operator. /// @@ -110,7 +114,7 @@ /// \return /// Returns \b true if the object has an empty directory and /// filename, \b false otherwise. - bool operator!() const { return !DescriptorIsValid() && !StreamIsValid(); } + bool operator!() const { return !IsValid(); } /// Get the file spec for this file. /// @@ -134,7 +138,7 @@ /// nullptr otherwise. FILE *TakeStreamAndClear(); - int GetDescriptor() const; + virtual int GetDescriptor() const; static uint32_t GetOptionsFromMode(llvm::StringRef mode); @@ -197,7 +201,7 @@ /// /// \return /// The resulting seek offset, or -1 on error. - off_t SeekFromStart(off_t offset, Status *error_ptr = nullptr); + virtual off_t SeekFromStart(off_t offset, Status *error_ptr = nullptr); /// Seek to an offset relative to the current file position. /// @@ -217,7 +221,7 @@ /// /// \return /// The resulting seek offset, or -1 on error. - off_t SeekFromCurrent(off_t offset, Status *error_ptr = nullptr); + virtual off_t SeekFromCurrent(off_t offset, Status *error_ptr = nullptr); /// Seek to an offset relative to the end of the file. /// @@ -238,7 +242,7 @@ /// /// \return /// The resulting seek offset, or -1 on error. - off_t SeekFromEnd(off_t offset, Status *error_ptr = nullptr); + virtual off_t SeekFromEnd(off_t offset, Status *error_ptr = nullptr); /// Read bytes from a file from the specified file offset. /// @@ -261,7 +265,7 @@ /// \return /// An error object that indicates success or the reason for /// failure. - Status Read(void *dst, size_t &num_bytes, off_t &offset); + virtual Status Read(void *dst, size_t &num_bytes, off_t &offset); /// Write bytes to a file at the specified file offset. /// @@ -286,14 +290,14 @@ /// \return /// An error object that indicates success or the reason for /// failure. - Status Write(const void *src, size_t &num_bytes, off_t &offset); + virtual Status Write(const void *src, size_t &num_bytes, off_t &offset); /// Flush the current stream /// /// \return /// An error object that indicates success or the reason for /// failure. - Status Flush(); + virtual Status Flush(); /// Sync to disk. /// @@ -350,6 +354,8 @@ bool StreamIsValid() const { return m_stream != kInvalidStream; } + virtual bool OverridesIO() const { return false; } + void CalculateInteractiveAndTerminal(); // Member variables 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 @@ -15,6 +15,30 @@ from lldbsuite.test.lldbtest import * from lldbsuite.test import lldbutil +class OhNoe(Exception): + pass + +class BadIO(io.TextIOBase): + 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") + +class ReallyBadIO(io.TextIOBase): + 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 @contextlib.contextmanager def replace_stdout(new): @@ -65,6 +89,7 @@ debugger.SetOutputFileHandle(f, False) handle_command(debugger, 'script 1+1') debugger.GetOutputFileHandle().write('FOO\n') + lldb.SBDebugger.Destroy(debugger) with open('output', 'r') as f: self.assertEqual(readStrippedLines(f), ['2', 'FOO']) finally: @@ -98,7 +123,7 @@ debugger.SetErrorFileHandle(f, False) handle_command(debugger, 'lolwut', raise_on_fail=False, collect_result=False) debugger.GetErrorFileHandle().write('FOOBAR\n') - + lldb.SBDebugger.Destroy(debugger) with open('output', 'r') as f: errors = f.read() self.assertTrue(re.search(r'error:.*lolwut', errors)) @@ -291,6 +316,7 @@ @add_test_categories(['pyapi']) @no_debug_info_test + @skipIf(py_version=['<', (3,)]) def test_replace_stdout(self): f = io.StringIO() debugger = lldb.SBDebugger.Create() @@ -303,3 +329,231 @@ finally: lldb.SBDebugger.Destroy(debugger) + + @add_test_categories(['pyapi']) + @no_debug_info_test + def test_sbfile_write(self): + try: + with open('output', '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('output', 'r') as f: + self.assertEqual(f.read().strip(), 'FOO') + finally: + self.RemoveTempFile('output') + + + @add_test_categories(['pyapi']) + @no_debug_info_test + def test_sbfile_read(self): + try: + with open('output', 'w') as f: + f.write('foo') + with open('output', '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) + finally: + self.RemoveTempFile('output') + + + @add_test_categories(['pyapi']) + @no_debug_info_test + def test_sbfile_write_borrowed(self): + try: + with open('output', 'w') as f: + sbf = lldb.SBFile(lldb.FileBorrow(), f) + 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('output', 'r') as f: + self.assertEqual(f.read().strip(), 'FOOBAR') + finally: + self.RemoveTempFile('output') + + + @add_test_categories(['pyapi']) + @no_debug_info_test + @skipIf(py_version=['<', (3,)]) + def test_sbfile_write_forced(self): + try: + with open('output', '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(lldb.FileForceScriptingIO(), f) + 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('output', 'r') as f: + self.assertEqual(f.read().strip(), 'FOO') + finally: + self.RemoveTempFile('output') + + + @add_test_categories(['pyapi']) + @no_debug_info_test + @skipIf(py_version=['<', (3,)]) + def test_sbfile_write_forced_borrowed(self): + try: + with open('output', '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(lldb.FileBorrowAndForceScriptingIO(), f) + 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('output', 'r') as f: + self.assertEqual(f.read().strip(), 'FOO') + finally: + self.RemoveTempFile('output') + + + + @add_test_categories(['pyapi']) + @no_debug_info_test + @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) + + @add_test_categories(['pyapi']) + @no_debug_info_test + @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) + + @add_test_categories(['pyapi']) + @no_debug_info_test + @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']) + @no_debug_info_test + @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']) + @no_debug_info_test + @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']) + @no_debug_info_test + @skipIf(py_version=['<', (3,)]) + def test_sbfile_out(self): + + debugger = lldb.SBDebugger.Create() + try: + with open('output', 'w') as f: + sbf = lldb.SBFile(f) + status = debugger.SetOutputFile(sbf) + if status.Fail(): + raise Exception(status) + handle_command(debugger, 'script 2+2') + + with open('output', 'r') as f: + self.assertEqual(f.read().strip(), '4') + + finally: + self.RemoveTempFile('output') + lldb.SBDebugger.Destroy(debugger) + + + @add_test_categories(['pyapi']) + @no_debug_info_test + def test_sbfile_error(self): + + debugger = lldb.SBDebugger.Create() + try: + with open('output', 'w') as f: + sbf = lldb.SBFile(f) + status = debugger.SetErrorFile(sbf) + if status.Fail(): + raise Exception(status) + + 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_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')") + 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 @@ -17,12 +17,41 @@ "Represents a file." ) SBFile; +struct FileBorrow {}; +struct FileForceScriptingIO {}; +struct FileBorrowAndForceScriptingIO {}; + 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); + + %feature("docstring", " + Like SBFile(f), but the underlying file will + not be closed when the SBFile is closed or destroyed."); + SBFile(FileBorrow, FileSP BORROWED); + + %feature("docstring" " + like SetFile(f), but the python read/write methods will be called even if + a file descriptor is available."); + SBFile(FileForceScriptingIO, FileSP FORCE_IO_METHODS); + + %feature("docstring" " + like SetFile(f), but the python read/write methods will be called even + if a file descriptor is available -- and the underlying file will not + be closed when the SBFile is closed or destroyed."); + SBFile(FileBorrowAndForceScriptingIO, FileSP BORROWED_FORCE_IO_METHODS); + ~SBFile (); %feature("autodoc", "Read(buffer) -> SBError, bytes_read") Read; diff --git a/lldb/source/Host/common/File.cpp b/lldb/source/Host/common/File.cpp --- a/lldb/source/Host/common/File.cpp +++ b/lldb/source/Host/common/File.cpp @@ -111,7 +111,7 @@ FILE *File::GetStream() { if (!StreamIsValid()) { - if (DescriptorIsValid()) { + if (DescriptorIsValid() && !OverridesIO()) { const char *mode = GetStreamOpenModeFromOptions(m_options); if (mode) { if (!m_own_descriptor) { @@ -232,8 +232,11 @@ } off_t File::SeekFromStart(off_t offset, Status *error_ptr) { - off_t result = 0; - if (DescriptorIsValid()) { + off_t result = -1; + if (OverridesIO()) { + if (error_ptr) + error_ptr->SetErrorString("unsupported operation"); + } else if (DescriptorIsValid()) { result = ::lseek(m_descriptor, offset, SEEK_SET); if (error_ptr) { @@ -259,7 +262,10 @@ off_t File::SeekFromCurrent(off_t offset, Status *error_ptr) { off_t result = -1; - if (DescriptorIsValid()) { + if (OverridesIO()) { + if (error_ptr) + error_ptr->SetErrorString("unsupported operation"); + } else if (DescriptorIsValid()) { result = ::lseek(m_descriptor, offset, SEEK_CUR); if (error_ptr) { @@ -285,7 +291,10 @@ off_t File::SeekFromEnd(off_t offset, Status *error_ptr) { off_t result = -1; - if (DescriptorIsValid()) { + if (OverridesIO()) { + if (error_ptr) + error_ptr->SetErrorString("unsupported operation"); + } else if (DescriptorIsValid()) { result = ::lseek(m_descriptor, offset, SEEK_END); if (error_ptr) { @@ -322,7 +331,8 @@ Status File::Sync() { Status error; - if (DescriptorIsValid()) { + int fd = GetDescriptor(); + if (fd != kInvalidDescriptor) { #ifdef _WIN32 int err = FlushFileBuffers((HANDLE)_get_osfhandle(m_descriptor)); if (err == 0) @@ -467,6 +477,11 @@ Status File::Read(void *buf, size_t &num_bytes, off_t &offset) { Status error; + if (OverridesIO()) { + error.SetErrorString("unsupported operation"); + return error; + } + #if defined(MAX_READ_SIZE) if (num_bytes > MAX_READ_SIZE) { uint8_t *p = (uint8_t *)buf; @@ -527,6 +542,11 @@ Status File::Write(const void *buf, size_t &num_bytes, off_t &offset) { Status error; + if (OverridesIO()) { + error.SetErrorString("unsupported operation"); + return error; + } + #if defined(MAX_WRITE_SIZE) if (num_bytes > MAX_WRITE_SIZE) { const uint8_t *p = (const uint8_t *)buf; @@ -600,7 +620,7 @@ // Print some formatted output to the stream. size_t File::PrintfVarArg(const char *format, va_list args) { size_t result = 0; - if (DescriptorIsValid()) { + if (DescriptorIsValid() || OverridesIO()) { char *s = nullptr; result = vasprintf(&s, format, args); if (s != nullptr) { 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; } @@ -467,8 +472,41 @@ void Reset(File &file, const char *mode); lldb::FileUP GetUnderlyingFile() const; + + llvm::Expected ConvertToFile(bool borrowed = false); + llvm::Expected + ConvertToFileForcingUseOfScriptingIOMethods(bool borrowed = false, + uint32_t options = 0); +}; + +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(); + } else { + 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" @@ -29,6 +30,14 @@ using namespace lldb_private; using namespace lldb; +template static T Owned(PyObject *obj) { + return T(PyRefType::Owned, obj); +} + +template static T Borrowed(PyObject *obj) { + return T(PyRefType::Borrowed, obj); +} + void StructuredPythonObject::Dump(Stream &s, bool pretty_print) const { s << "Python Obj: 0x" << GetValue(); } @@ -954,6 +963,8 @@ PythonFile::~PythonFile() {} bool PythonFile::Check(PyObject *py_obj) { + if (!py_obj) + return false; #if PY_MAJOR_VERSION < 3 return PyFile_Check(py_obj); #else @@ -961,9 +972,7 @@ // 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())); @@ -973,8 +982,6 @@ if (1 != PyObject_IsSubclass(object_type.get(), io_base_class.get())) return false; - if (!object_type.HasAttribute("fileno")) - return false; return true; #endif @@ -1029,4 +1036,339 @@ return file; } +class GIL { +public: + GIL() { m_state = PyGILState_Ensure(); } + ~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); + if (log) { + log->Printf("%s failed with exception: %s", caller, toCString()); + } +} +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 = + Owned(PyObject_CallMethod(obj.get(), "readable", "()")); + auto writable = + Owned(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; +} + +class SimplePythonFile : public File { +public: + SimplePythonFile(int fd, uint32_t options, PyObject *py_obj, bool borrowed) + : File(fd, options, false), m_py_obj(py_obj), m_borrowed(borrowed) { + Py_INCREF(m_py_obj); + } + + Status Close() override { + File::Close(); + GIL takeGIL; + if (!m_borrowed) + PyObject_CallMethod(m_py_obj, "close", "()"); + Py_XDECREF(m_py_obj); + Status error; + if (PyErr_Occurred()) + error = llvm::make_error("Close"); + return error; + }; + +protected: + PyObject *m_py_obj; + bool m_borrowed; +}; + +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) { + return ConvertToFileForcingUseOfScriptingIOMethods(borrowed); + } + auto options = GetOptionsForPyObject(*this); + if (!options) + return options.takeError(); + + 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::make_shared(fd, options.get(), m_py_obj, + borrowed); + } + if (!file_sp->IsValid()) + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "invalid File"); + + return file_sp; +} + +#if PY_MAJOR_VERSION >= 3 + +class PythonBuffer { +public: + 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; +}; + +class BinaryPythonFile : public SimplePythonFile { + friend class PythonFile; + +public: + BinaryPythonFile(int fd, uint32_t options, PyObject *py_obj, bool borrowed) + : SimplePythonFile(fd, options, py_obj, borrowed) {} + + bool OverridesIO() const override { return true; } + + Status Write(const void *buf, size_t &num_bytes) override { + GIL takeGIL; + auto pybuffer = Owned(PyMemoryView_FromMemory( + const_cast((const char *)buf), num_bytes, PyBUF_READ)); + num_bytes = 0; + auto bytes_written = Owned( + 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 = Owned(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(); + } + + Status Flush() override { + GIL takeGIL; + PyErr_Clear(); + PyObject_CallMethod(m_py_obj, "flush", "()"); + Status error; + if (PyErr_Occurred()) + error = llvm::make_error("Flush"); + return error; + } +}; + +class TextPythonFile : public SimplePythonFile { + friend class PythonFile; + +public: + TextPythonFile(int fd, uint32_t options, PyObject *py_obj, bool borrowed) + : SimplePythonFile(fd, options, py_obj, borrowed) {} + + bool OverridesIO() const override { return true; } + + Status Write(const void *buf, size_t &num_bytes) override { + GIL takeGIL; + auto pystring = Owned( + PyUnicode_FromStringAndSize((const char *)buf, num_bytes)); + num_bytes = 0; + if (PyErr_Occurred()) + return Status(llvm::make_error("Write")); + auto bytes_written = Owned( + 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 = Owned(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"); + 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(); + } + + Status Flush() override { + GIL takeGIL; + PyErr_Clear(); + PyObject_CallMethod(m_py_obj, "flush", "()"); + Status error; + if (PyErr_Occurred()) + error = llvm::make_error("Flush"); + return error; + } +}; + +#endif + +llvm::Expected +PythonFile::ConvertToFileForcingUseOfScriptingIOMethods(bool borrowed, + uint32_t options) { + 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 = -1; + } + + if (options == 0) { + auto calculated_options = GetOptionsForPyObject(*this); + if (calculated_options) { + options = calculated_options.get(); + } else { + return calculated_options.takeError(); + } + } + + auto io_module = Owned(PyImport_ImportModule("io")); + auto io_dict = Borrowed(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")); + + FileSP file_sp; + if (PyObject_IsInstance(m_py_obj, textIOBase.get())) { + file_sp = std::make_shared(fd, options, m_py_obj, borrowed); + } else if (PyObject_IsInstance(m_py_obj, rawIOBase.get()) || + PyObject_IsInstance(m_py_obj, bufferedIOBase.get())) { + file_sp = + std::make_shared(fd, options, m_py_obj, borrowed); + } else + 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