diff --git a/lldb/include/lldb/API/LLDB.h b/lldb/include/lldb/API/LLDB.h --- a/lldb/include/lldb/API/LLDB.h +++ b/lldb/include/lldb/API/LLDB.h @@ -13,8 +13,8 @@ #include "lldb/API/SBAttachInfo.h" #include "lldb/API/SBBlock.h" #include "lldb/API/SBBreakpoint.h" -#include "lldb/API/SBBreakpointName.h" #include "lldb/API/SBBreakpointLocation.h" +#include "lldb/API/SBBreakpointName.h" #include "lldb/API/SBBroadcaster.h" #include "lldb/API/SBCommandInterpreter.h" #include "lldb/API/SBCommandReturnObject.h" @@ -28,6 +28,7 @@ #include "lldb/API/SBEvent.h" #include "lldb/API/SBExecutionContext.h" #include "lldb/API/SBExpressionOptions.h" +#include "lldb/API/SBFile.h" #include "lldb/API/SBFileSpec.h" #include "lldb/API/SBFileSpecList.h" #include "lldb/API/SBFrame.h" diff --git a/lldb/include/lldb/API/SBDefines.h b/lldb/include/lldb/API/SBDefines.h --- a/lldb/include/lldb/API/SBDefines.h +++ b/lldb/include/lldb/API/SBDefines.h @@ -41,6 +41,7 @@ class LLDB_API SBEventList; class LLDB_API SBExecutionContext; class LLDB_API SBExpressionOptions; +class LLDB_API SBFile; class LLDB_API SBFileSpec; class LLDB_API SBFileSpecList; class LLDB_API SBFrame; diff --git a/lldb/include/lldb/API/SBError.h b/lldb/include/lldb/API/SBError.h --- a/lldb/include/lldb/API/SBError.h +++ b/lldb/include/lldb/API/SBError.h @@ -70,6 +70,7 @@ friend class SBTrace; friend class SBValue; friend class SBWatchpoint; + friend class SBFile; lldb_private::Status *get(); diff --git a/lldb/include/lldb/API/SBFile.h b/lldb/include/lldb/API/SBFile.h new file mode 100644 --- /dev/null +++ b/lldb/include/lldb/API/SBFile.h @@ -0,0 +1,38 @@ +//===-- SBFile.h --------------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_SBFile_h_ +#define LLDB_SBFile_h_ + +#include "lldb/API/SBDefines.h" + +namespace lldb { + +class LLDB_API SBFile { +public: + SBFile(); + SBFile(FILE *file, bool transfer_ownership); + SBFile(int fd, const char *mode, bool transfer_ownership); + ~SBFile(); + + SBError Read(uint8_t *buf, size_t num_bytes, size_t *bytes_read); + SBError Write(const uint8_t *buf, size_t num_bytes, size_t *bytes_written); + SBError Flush(); + bool IsValid() const; + SBError Close(); + + operator bool() const; + bool operator!() const; + +private: + FileSP m_opaque_sp; +}; + +} // namespace lldb + +#endif // LLDB_SBFile_h_ 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 @@ -136,6 +136,8 @@ int GetDescriptor() const; + static uint32_t GetOptionsFromMode(llvm::StringRef mode); + WaitableHandle GetWaitableHandle() override; FILE *GetStream(); diff --git a/lldb/packages/Python/lldbsuite/test/python_api/default-constructor/TestDefaultConstructorForAPIObjects.py b/lldb/packages/Python/lldbsuite/test/python_api/default-constructor/TestDefaultConstructorForAPIObjects.py --- a/lldb/packages/Python/lldbsuite/test/python_api/default-constructor/TestDefaultConstructorForAPIObjects.py +++ b/lldb/packages/Python/lldbsuite/test/python_api/default-constructor/TestDefaultConstructorForAPIObjects.py @@ -192,6 +192,20 @@ import sb_function sb_function.fuzz_obj(obj) + @add_test_categories(['pyapi']) + @no_debug_info_test + def test_SBFile(self): + sbf = lldb.SBFile() + self.assertFalse(sbf.IsValid()) + self.assertFalse(bool(sbf)) + e, n = sbf.Write(b'foo') + self.assertTrue(e.Fail()) + self.assertEqual(n, 0) + buffer = bytearray(100) + e, n = sbf.Read(buffer) + self.assertEqual(n, 0) + self.assertTrue(e.Fail()) + @add_test_categories(['pyapi']) @no_debug_info_test def test_SBInstruction(self): 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 @@ -0,0 +1,150 @@ +""" +Test lldb Python API for file handles. +""" + +from __future__ import print_function + +import os +import io +import re +import sys +from contextlib import contextmanager + +import lldb +from lldbsuite.test import lldbtest +from lldbsuite.test.decorators import add_test_categories, no_debug_info_test + + +def readStrippedLines(f): + def i(): + for line in f: + line = line.strip() + if line: + yield line + return list(i()) + + +class FileHandleTestCase(lldbtest.TestBase): + + mydir = lldbtest.Base.compute_mydir(__file__) + + # The way this class interacts with the debugger is different + # than normal. Most of these test cases will mess with the + # debugger I/O streams, so we want a fresh debugger for each + # test so those mutations don't interfere with each other. + # + # Also, the way normal tests evaluate debugger commands is + # by using a SBCommandInterpreter directly, which captures + # the output in a result object. For many of tests tests + # we want the debugger to write the output directly to + # its I/O streams like it would have done interactively. + # + # For this reason we also define handleCmd() here, even though + # it is similar to runCmd(). + + def setUp(self): + super(FileHandleTestCase, self).setUp() + self.debugger = lldb.SBDebugger.Create() + self.out_filename = self.getBuildArtifact('output') + self.in_filename = self.getBuildArtifact('input') + + def tearDown(self): + lldb.SBDebugger.Destroy(self.debugger) + super(FileHandleTestCase, self).tearDown() + for name in (self.out_filename, self.in_filename): + if os.path.exists(name): + os.unlink(name) + + # Similar to runCmd(), but this uses the per-test debugger, and it + # supports, letting the debugger just print the results instead + # of collecting them. + def handleCmd(self, cmd, check=True, collect_result=True): + assert not check or collect_result + ret = lldb.SBCommandReturnObject() + if collect_result: + interpreter = self.debugger.GetCommandInterpreter() + interpreter.HandleCommand(cmd, ret) + else: + self.debugger.HandleCommand(cmd) + if collect_result and check: + self.assertTrue(ret.Succeeded()) + return ret.GetOutput() + + + @add_test_categories(['pyapi']) + @no_debug_info_test + def test_legacy_file_out_script(self): + with open(self.out_filename, 'w') as f: + self.debugger.SetOutputFileHandle(f, False) + # scripts print to output even if you capture the results + # I'm not sure I love that behavior, but that's the way + # it's been for a long time. That's why this test works + # even with collect_result=True. + self.handleCmd('script 1+1') + self.debugger.GetOutputFileHandle().write('FOO\n') + lldb.SBDebugger.Destroy(self.debugger) + with open(self.out_filename, 'r') as f: + self.assertEqual(readStrippedLines(f), ['2', 'FOO']) + + + @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) + self.handleCmd('p/x 3735928559', collect_result=False, check=False) + lldb.SBDebugger.Destroy(self.debugger) + with open(self.out_filename, 'r') as f: + self.assertIn('deadbeef', f.read()) + + + @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) + self.handleCmd('lol', check=False, collect_result=False) + lldb.SBDebugger.Destroy(self.debugger) + with open(self.out_filename, 'r') as f: + self.assertIn("is not a valid command", f.read()) + + + @add_test_categories(['pyapi']) + @no_debug_info_test + def test_sbfile_type_errors(self): + sbf = lldb.SBFile() + self.assertRaises(TypeError, sbf.Write, None) + self.assertRaises(TypeError, sbf.Read, None) + self.assertRaises(TypeError, sbf.Read, b'this bytes is not mutable') + self.assertRaises(TypeError, sbf.Write, u"ham sandwich") + self.assertRaises(TypeError, sbf.Read, u"ham sandwich") + + + @add_test_categories(['pyapi']) + @no_debug_info_test + def test_sbfile_write(self): + with open(self.out_filename, 'w') as f: + sbf = lldb.SBFile(f.fileno(), "w", False) + self.assertTrue(sbf.IsValid()) + e, n = sbf.Write(b'FOO\nBAR') + self.assertTrue(e.Success()) + self.assertEqual(n, 7) + sbf.Close() + self.assertFalse(sbf.IsValid()) + with open(self.out_filename, 'r') as f: + self.assertEqual(readStrippedLines(f), ['FOO', 'BAR']) + + + @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.fileno(), "r", False) + self.assertTrue(sbf.IsValid()) + buffer = bytearray(100) + e, n = sbf.Read(buffer) + self.assertTrue(e.Success()) + self.assertEqual(buffer[:n], b'FOO') + 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 @@ -224,7 +224,7 @@ namespace { template T PyLongAsT(PyObject *obj) { - static_assert(true, "unsupported type"); + static_assert(true, "unsupported type"); } template <> uint64_t PyLongAsT(PyObject *obj) { @@ -461,3 +461,44 @@ return NULL; } } + +// These two pybuffer macros are copied out of swig/Lib/python/pybuffer.i, +// and fixed so they will not crash if PyObject_GetBuffer fails. +// https://github.com/swig/swig/issues/1640 + +%define %pybuffer_mutable_binary(TYPEMAP, SIZE) +%typemap(in) (TYPEMAP, SIZE) { + int res; Py_ssize_t size = 0; void *buf = 0; + Py_buffer view; + res = PyObject_GetBuffer($input, &view, PyBUF_WRITABLE); + if (res < 0) { + PyErr_Clear(); + %argument_fail(res, "(TYPEMAP, SIZE)", $symname, $argnum); + } + size = view.len; + buf = view.buf; + PyBuffer_Release(&view); + $1 = ($1_ltype) buf; + $2 = ($2_ltype) (size/sizeof($*1_type)); +} +%enddef + +%define %pybuffer_binary(TYPEMAP, SIZE) +%typemap(in) (TYPEMAP, SIZE) { + int res; Py_ssize_t size = 0; const void *buf = 0; + Py_buffer view; + res = PyObject_GetBuffer($input, &view, PyBUF_CONTIG_RO); + if (res < 0) { + PyErr_Clear(); + %argument_fail(res, "(TYPEMAP, SIZE)", $symname, $argnum); + } + size = view.len; + buf = view.buf; + PyBuffer_Release(&view); + $1 = ($1_ltype) buf; + $2 = ($2_ltype) (size / sizeof($*1_type)); +} +%enddef + +%pybuffer_binary(const uint8_t *buf, size_t num_bytes); +%pybuffer_mutable_binary(uint8_t *buf, size_t num_bytes); diff --git a/lldb/scripts/interface/SBFile.i b/lldb/scripts/interface/SBFile.i new file mode 100644 --- /dev/null +++ b/lldb/scripts/interface/SBFile.i @@ -0,0 +1,38 @@ +//===-- SWIG Interface for SBFile -----------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +namespace lldb { + +%feature("docstring", +"Represents a file." +) SBFile; + +class SBFile +{ +public: + SBFile(); + SBFile(int fd, const char *mode, bool transfer_ownership); + + ~SBFile (); + + %feature("autodoc", "Read(buffer) -> SBError, bytes_read") Read; + SBError Read(uint8_t *buf, size_t num_bytes, size_t *OUTPUT); + + %feature("autodoc", "Write(buffer) -> SBError, written_read") Write; + SBError Write(const uint8_t *buf, size_t num_bytes, size_t *OUTPUT); + + void Flush(); + + bool IsValid() const; + + operator bool() const; + + SBError Close(); +}; + +} // namespace lldb diff --git a/lldb/scripts/lldb.swig b/lldb/scripts/lldb.swig --- a/lldb/scripts/lldb.swig +++ b/lldb/scripts/lldb.swig @@ -123,6 +123,7 @@ #include "lldb/API/SBExecutionContext.h" #include "lldb/API/SBExpressionOptions.h" #include "lldb/API/SBFileSpec.h" +#include "lldb/API/SBFile.h" #include "lldb/API/SBFileSpecList.h" #include "lldb/API/SBFrame.h" #include "lldb/API/SBFunction.h" @@ -210,6 +211,7 @@ %include "./interface/SBExecutionContext.i" %include "./interface/SBExpressionOptions.i" %include "./interface/SBFileSpec.i" +%include "./interface/SBFile.i" %include "./interface/SBFileSpecList.i" %include "./interface/SBFrame.i" %include "./interface/SBFunction.i" diff --git a/lldb/source/API/CMakeLists.txt b/lldb/source/API/CMakeLists.txt --- a/lldb/source/API/CMakeLists.txt +++ b/lldb/source/API/CMakeLists.txt @@ -34,6 +34,7 @@ SBExecutionContext.cpp SBExpressionOptions.cpp SBFileSpec.cpp + SBFile.cpp SBFileSpecList.cpp SBFrame.cpp SBFunction.cpp diff --git a/lldb/source/API/SBFile.cpp b/lldb/source/API/SBFile.cpp new file mode 100644 --- /dev/null +++ b/lldb/source/API/SBFile.cpp @@ -0,0 +1,113 @@ +//===-- SBFile.cpp ------------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "lldb/API/SBFile.h" +#include "SBReproducerPrivate.h" +#include "lldb/API/SBError.h" +#include "lldb/Host/File.h" + +using namespace lldb; +using namespace lldb_private; + +SBFile::~SBFile() {} + +SBFile::SBFile() { LLDB_RECORD_CONSTRUCTOR_NO_ARGS(SBFile); } + +SBFile::SBFile(FILE *file, bool transfer_ownership) { + m_opaque_sp = std::make_shared(file, transfer_ownership); +} + +SBFile::SBFile(int fd, const char *mode, bool transfer_owndership) { + LLDB_RECORD_CONSTRUCTOR(SBFile, (int, const char *, bool), fd, mode, + transfer_owndership); + auto options = File::GetOptionsFromMode(mode); + m_opaque_sp = std::make_shared(fd, options, transfer_owndership); +} + +SBError SBFile::Read(uint8_t *buf, size_t num_bytes, size_t *bytes_read) { + LLDB_RECORD_DUMMY(lldb::SBError, SBFile, Read, (uint8_t *, size_t, size_t *), + buf, num_bytes, bytes_read); + SBError error; + if (!m_opaque_sp) { + error.SetErrorString("invalid SBFile"); + *bytes_read = 0; + } else { + Status status = m_opaque_sp->Read(buf, num_bytes); + error.SetError(status); + *bytes_read = num_bytes; + } + return LLDB_RECORD_RESULT(error); +} + +SBError SBFile::Write(const uint8_t *buf, size_t num_bytes, + size_t *bytes_written) { + LLDB_RECORD_DUMMY(lldb::SBError, SBFile, Write, + (const uint8_t *, size_t, size_t *), buf, num_bytes, + bytes_written); + SBError error; + if (!m_opaque_sp) { + error.SetErrorString("invalid SBFile"); + *bytes_written = 0; + } else { + Status status = m_opaque_sp->Write(buf, num_bytes); + error.SetError(status); + *bytes_written = num_bytes; + } + return LLDB_RECORD_RESULT(error); +} + +SBError SBFile::Flush() { + LLDB_RECORD_METHOD_NO_ARGS(lldb::SBError, SBFile, Flush); + SBError error; + if (!m_opaque_sp) { + error.SetErrorString("invalid SBFile"); + } else { + Status status = m_opaque_sp->Flush(); + error.SetError(status); + } + return LLDB_RECORD_RESULT(error); +} + +bool SBFile::IsValid() const { + LLDB_RECORD_METHOD_CONST_NO_ARGS(bool, SBFile, IsValid); + return m_opaque_sp && m_opaque_sp->IsValid(); +} + +SBError SBFile::Close() { + LLDB_RECORD_METHOD_NO_ARGS(lldb::SBError, SBFile, Close); + SBError error; + if (m_opaque_sp) { + Status status = m_opaque_sp->Close(); + error.SetError(status); + } + return LLDB_RECORD_RESULT(error); +} + +SBFile::operator bool() const { + LLDB_RECORD_METHOD_CONST_NO_ARGS(bool, SBFile, operator bool); + return LLDB_RECORD_RESULT(IsValid()); +} + +bool SBFile::operator!() const { + LLDB_RECORD_METHOD_CONST_NO_ARGS(bool, SBFile, operator!); + return LLDB_RECORD_RESULT(!IsValid()); +} + +namespace lldb_private { +namespace repro { +template <> void RegisterMethods(Registry &R) { + LLDB_REGISTER_CONSTRUCTOR(SBFile, ()); + LLDB_REGISTER_CONSTRUCTOR(SBFile, (int, const char *, bool)); + LLDB_REGISTER_METHOD(lldb::SBError, SBFile, Flush, ()); + LLDB_REGISTER_METHOD_CONST(bool, SBFile, IsValid, ()); + LLDB_REGISTER_METHOD_CONST(bool, SBFile, operator bool,()); + LLDB_REGISTER_METHOD_CONST(bool, SBFile, operator!,()); + LLDB_REGISTER_METHOD(lldb::SBError, SBFile, Close, ()); +} +} // namespace repro +} // namespace lldb_private diff --git a/lldb/source/API/SBReproducer.cpp b/lldb/source/API/SBReproducer.cpp --- a/lldb/source/API/SBReproducer.cpp +++ b/lldb/source/API/SBReproducer.cpp @@ -52,6 +52,7 @@ RegisterMethods(R); RegisterMethods(R); RegisterMethods(R); + RegisterMethods(R); RegisterMethods(R); RegisterMethods(R); RegisterMethods(R); 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 @@ -68,6 +68,20 @@ return nullptr; } +uint32_t File::GetOptionsFromMode(llvm::StringRef mode) { + return llvm::StringSwitch(mode) + .Case("r", File::eOpenOptionRead) + .Case("w", File::eOpenOptionWrite) + .Case("a", File::eOpenOptionWrite | File::eOpenOptionAppend | + File::eOpenOptionCanCreate) + .Case("r+", File::eOpenOptionRead | File::eOpenOptionWrite) + .Case("w+", File::eOpenOptionRead | File::eOpenOptionWrite | + File::eOpenOptionCanCreate | File::eOpenOptionTruncate) + .Case("a+", File::eOpenOptionRead | File::eOpenOptionWrite | + File::eOpenOptionAppend | File::eOpenOptionCanCreate) + .Default(0); +} + int File::kInvalidDescriptor = -1; FILE *File::kInvalidStream = nullptr; @@ -143,9 +157,14 @@ Status File::Close() { Status error; - if (StreamIsValid() && m_own_stream) { - if (::fclose(m_stream) == EOF) - error.SetErrorToErrno(); + if (StreamIsValid()) { + if (m_own_stream) { + if (::fclose(m_stream) == EOF) + error.SetErrorToErrno(); + } else { + if (::fflush(m_stream) == EOF) + error.SetErrorToErrno(); + } } if (DescriptorIsValid() && m_own_descriptor) { 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 @@ -466,8 +466,6 @@ void Reset(PyRefType type, PyObject *py_obj) override; void Reset(File &file, const char *mode); - static uint32_t GetOptionsFromMode(llvm::StringRef mode); - lldb::FileUP GetUnderlyingFile() const; }; 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 @@ -949,7 +949,6 @@ PythonFile::PythonFile(File &file, const char *mode) { Reset(file, mode); } - PythonFile::PythonFile(PyRefType type, PyObject *o) { Reset(type, o); } PythonFile::~PythonFile() {} @@ -1014,22 +1013,6 @@ #endif } -uint32_t PythonFile::GetOptionsFromMode(llvm::StringRef mode) { - if (mode.empty()) - return 0; - - return llvm::StringSwitch(mode.str()) - .Case("r", File::eOpenOptionRead) - .Case("w", File::eOpenOptionWrite) - .Case("a", File::eOpenOptionWrite | File::eOpenOptionAppend | - File::eOpenOptionCanCreate) - .Case("r+", File::eOpenOptionRead | File::eOpenOptionWrite) - .Case("w+", File::eOpenOptionRead | File::eOpenOptionWrite | - File::eOpenOptionCanCreate | File::eOpenOptionTruncate) - .Case("a+", File::eOpenOptionRead | File::eOpenOptionWrite | - File::eOpenOptionAppend | File::eOpenOptionCanCreate) - .Default(0); -} FileUP PythonFile::GetUnderlyingFile() const { if (!IsValid()) @@ -1038,7 +1021,7 @@ // We don't own the file descriptor returned by this function, make sure the // File object knows about that. PythonString py_mode = GetAttributeValue("mode").AsType(); - auto options = PythonFile::GetOptionsFromMode(py_mode.GetString()); + auto options = File::GetOptionsFromMode(py_mode.GetString()); auto file = std::make_unique(PyObject_AsFileDescriptor(m_py_obj), options, false); if (!file->IsValid())