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 @@ -10,6 +10,7 @@ #define LLDB_HOST_FILE_H #include "lldb/Host/PosixApi.h" +#include "lldb/Host/Terminal.h" #include "lldb/Utility/IOObject.h" #include "lldb/Utility/Status.h" #include "lldb/lldb-private.h" @@ -433,6 +434,44 @@ const NativeFile &operator=(const NativeFile &) = delete; }; +class SerialPort : public NativeFile { +public: + struct Options { + llvm::Optional BaudRate = llvm::None; + llvm::Optional Parity = llvm::None; + llvm::Optional StopBits = llvm::None; + }; + + // Obtain Options corresponding to the passed URL query string + // (i.e. the part after '?'). + static llvm::Expected OptionsFromURL(llvm::StringRef urlqs); + + static llvm::Expected> + Create(int fd, OpenOptions options, Options serial_options, + bool transfer_ownership); + + bool IsValid() const override { + return NativeFile::IsValid() && m_is_interactive == eLazyBoolYes; + } + + Status Close() override; + + static char ID; + virtual bool isA(const void *classID) const override { + return classID == &ID || File::isA(classID); + } + static bool classof(const File *file) { return file->isA(&ID); } + +private: + SerialPort(int fd, OpenOptions options, Options serial_options, + bool transfer_ownership); + + SerialPort(const SerialPort &) = delete; + const SerialPort &operator=(const SerialPort &) = delete; + + TerminalState m_state; +}; + } // namespace lldb_private #endif // LLDB_HOST_FILE_H diff --git a/lldb/include/lldb/Host/posix/ConnectionFileDescriptorPosix.h b/lldb/include/lldb/Host/posix/ConnectionFileDescriptorPosix.h --- a/lldb/include/lldb/Host/posix/ConnectionFileDescriptorPosix.h +++ b/lldb/include/lldb/Host/posix/ConnectionFileDescriptorPosix.h @@ -88,6 +88,9 @@ lldb::ConnectionStatus ConnectFile(llvm::StringRef args, Status *error_ptr); + lldb::ConnectionStatus ConnectSerialPort(llvm::StringRef args, + Status *error_ptr); + lldb::IOObjectSP m_io_sp; Predicate 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 @@ -769,5 +769,87 @@ return mode; } +llvm::Expected +SerialPort::OptionsFromURL(llvm::StringRef urlqs) { + SerialPort::Options serial_options; + for (llvm::StringRef x : llvm::Split(urlqs, '&')) { + if (x.consume_front("baud=")) { + unsigned int baud_rate; + if (!llvm::to_integer(x, baud_rate, 10)) + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "Invalid baud rate: %s", + x.str().c_str()); + serial_options.BaudRate = baud_rate; + } else if (x.consume_front("parity=")) { + serial_options.Parity = + llvm::StringSwitch>(x) + .Case("no", Terminal::Parity::No) + .Case("even", Terminal::Parity::Even) + .Case("odd", Terminal::Parity::Odd) + .Case("mark", Terminal::Parity::Mark) + .Case("space", Terminal::Parity::Space) + .Default(llvm::None); + if (!serial_options.Parity) + return llvm::createStringError( + llvm::inconvertibleErrorCode(), + "Invalid parity (must be no, even, odd, mark or space): %s", + x.str().c_str()); + } else if (x.consume_front("stop-bits=")) { + unsigned int stop_bits; + if (!llvm::to_integer(x, stop_bits, 10) || + (stop_bits != 1 && stop_bits != 2)) + return llvm::createStringError( + llvm::inconvertibleErrorCode(), + "Invalid stop bit number (must be 1 or 2): %s", x.str().c_str()); + serial_options.StopBits = stop_bits; + } else + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "Unknown parameter: %s", x.str().c_str()); + } + return serial_options; +} + +llvm::Expected> +SerialPort::Create(int fd, OpenOptions options, Options serial_options, + bool transfer_ownership) { + std::unique_ptr out{ + new SerialPort(fd, options, serial_options, transfer_ownership)}; + + if (!out->GetIsInteractive()) + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "the specified file is not a teletype"); + + Terminal term{fd}; + if (llvm::Error error = term.SetRaw()) + return error; + if (serial_options.BaudRate) { + if (llvm::Error error = + term.SetBaudRate(serial_options.BaudRate.getValue())) + return error; + } + if (serial_options.Parity) { + if (llvm::Error error = term.SetParity(serial_options.Parity.getValue())) + return error; + } + if (serial_options.StopBits) { + if (llvm::Error error = + term.SetStopBits(serial_options.StopBits.getValue())) + return error; + } + + return out; +} + +SerialPort::SerialPort(int fd, OpenOptions options, + SerialPort::Options serial_options, + bool transfer_ownership) + : NativeFile(fd, options, transfer_ownership), m_state(fd) {} + +Status SerialPort::Close() { + m_state.Restore(); + return NativeFile::Close(); +} + char File::ID = 0; char NativeFile::ID = 0; +char SerialPort::ID = 0; diff --git a/lldb/source/Host/posix/ConnectionFileDescriptorPosix.cpp b/lldb/source/Host/posix/ConnectionFileDescriptorPosix.cpp --- a/lldb/source/Host/posix/ConnectionFileDescriptorPosix.cpp +++ b/lldb/source/Host/posix/ConnectionFileDescriptorPosix.cpp @@ -156,6 +156,7 @@ #if LLDB_ENABLE_POSIX .Case("fd", &ConnectionFileDescriptor::ConnectFD) .Case("file", &ConnectionFileDescriptor::ConnectFile) + .Case("serial", &ConnectionFileDescriptor::ConnectSerialPort) #endif .Default(nullptr); @@ -735,6 +736,55 @@ llvm_unreachable("this function should be only called w/ LLDB_ENABLE_POSIX"); } +ConnectionStatus +ConnectionFileDescriptor::ConnectSerialPort(llvm::StringRef s, + Status *error_ptr) { +#if LLDB_ENABLE_POSIX + llvm::StringRef path, qs; + // serial:///PATH?k1=v1&k2=v2... + std::tie(path, qs) = s.split('?'); + + llvm::Expected serial_options = + SerialPort::OptionsFromURL(qs); + if (!serial_options) { + if (error_ptr) + *error_ptr = serial_options.takeError(); + else + llvm::consumeError(serial_options.takeError()); + return eConnectionStatusError; + } + + int fd = llvm::sys::RetryAfterSignal(-1, ::open, path.str().c_str(), O_RDWR); + if (fd == -1) { + if (error_ptr) + error_ptr->SetErrorToErrno(); + return eConnectionStatusError; + } + + int flags = ::fcntl(fd, F_GETFL, 0); + if (flags >= 0) { + if ((flags & O_NONBLOCK) == 0) { + flags |= O_NONBLOCK; + ::fcntl(fd, F_SETFL, flags); + } + } + + llvm::Expected> serial_sp = SerialPort::Create( + fd, File::eOpenOptionReadWrite, serial_options.get(), true); + if (!serial_sp) { + if (error_ptr) + *error_ptr = serial_sp.takeError(); + else + llvm::consumeError(serial_sp.takeError()); + return eConnectionStatusError; + } + m_io_sp = std::move(serial_sp.get()); + + return eConnectionStatusSuccess; +#endif // LLDB_ENABLE_POSIX + llvm_unreachable("this function should be only called w/ LLDB_ENABLE_POSIX"); +} + uint16_t ConnectionFileDescriptor::GetListeningPort(const Timeout &timeout) { auto Result = m_port_predicate.WaitForValueNotEqualTo(0, timeout); diff --git a/lldb/test/API/functionalities/gdb_remote_client/TestPty.py b/lldb/test/API/functionalities/gdb_remote_client/TestPty.py --- a/lldb/test/API/functionalities/gdb_remote_client/TestPty.py +++ b/lldb/test/API/functionalities/gdb_remote_client/TestPty.py @@ -9,6 +9,38 @@ mydir = TestBase.compute_mydir(__file__) server_socket_class = PtyServerSocket + def get_term_attrs(self): + import termios + return termios.tcgetattr(self.server._socket._slave) + + def setUp(self): + super().setUp() + self.orig_attr = self.get_term_attrs() + + def assert_raw_mode(self, current_attr): + import termios + self.assertEqual(current_attr[0] & (termios.BRKINT | + termios.PARMRK | + termios.ISTRIP | termios.INLCR | + termios.IGNCR | termios.ICRNL | + termios.IXON), + 0) + self.assertEqual(current_attr[1] & termios.OPOST, 0) + self.assertEqual(current_attr[2] & termios.CSIZE, termios.CS8) + self.assertEqual(current_attr[3] & (termios.ICANON | termios.ECHO | + termios.ISIG | termios.IEXTEN), + 0) + self.assertEqual(current_attr[6][termios.VMIN], 1) + self.assertEqual(current_attr[6][termios.VTIME], 0) + + def get_parity_flags(self, attr): + import termios + return attr[2] & (termios.PARENB | termios.PARODD) + + def get_stop_bit_flags(self, attr): + import termios + return attr[2] & termios.CSTOPB + def test_process_connect_sync(self): """Test the process connect command in synchronous mode""" try: @@ -17,8 +49,20 @@ substrs=['Platform: remote-gdb-server', 'Connected: no']) self.expect("process connect " + self.server.get_connect_url(), substrs=['Process', 'stopped']) + + current_attr = self.get_term_attrs() + # serial:// should set raw mode + self.assert_raw_mode(current_attr) + # other parameters should be unmodified + self.assertEqual(current_attr[4:6], self.orig_attr[4:6]) + self.assertEqual(self.get_parity_flags(current_attr), + self.get_parity_flags(self.orig_attr)) + self.assertEqual(self.get_stop_bit_flags(current_attr), + self.get_stop_bit_flags(self.orig_attr)) finally: self.dbg.GetSelectedTarget().GetProcess().Kill() + # original mode should be restored on exit + self.assertEqual(self.get_term_attrs(), self.orig_attr) def test_process_connect_async(self): """Test the process connect command in asynchronous mode""" @@ -31,7 +75,63 @@ substrs=['Process', 'stopped']) lldbutil.expect_state_changes(self, self.dbg.GetListener(), self.process(), [lldb.eStateStopped]) + + current_attr = self.get_term_attrs() + # serial:// should set raw mode + self.assert_raw_mode(current_attr) + # other parameters should be unmodified + self.assertEqual(current_attr[4:6], self.orig_attr[4:6]) + self.assertEqual(self.get_parity_flags(current_attr), + self.get_parity_flags(self.orig_attr)) + self.assertEqual(self.get_stop_bit_flags(current_attr), + self.get_stop_bit_flags(self.orig_attr)) finally: self.dbg.GetSelectedTarget().GetProcess().Kill() lldbutil.expect_state_changes(self, self.dbg.GetListener(), self.process(), [lldb.eStateExited]) + # original mode should be restored on exit + self.assertEqual(self.get_term_attrs(), self.orig_attr) + + def test_connect_via_file(self): + """Test connecting via the legacy file:// URL""" + import termios + try: + self.expect("platform select remote-gdb-server", + substrs=['Platform: remote-gdb-server', 'Connected: no']) + self.expect("process connect file://" + + self.server.get_connect_address(), + substrs=['Process', 'stopped']) + + # file:// sets baud rate and some raw-related flags + current_attr = self.get_term_attrs() + self.assertEqual(current_attr[3] & (termios.ICANON | termios.ECHO | + termios.ECHOE | termios.ISIG), + 0) + self.assertEqual(current_attr[4], termios.B115200) + self.assertEqual(current_attr[5], termios.B115200) + self.assertEqual(current_attr[6][termios.VMIN], 1) + self.assertEqual(current_attr[6][termios.VTIME], 0) + finally: + self.dbg.GetSelectedTarget().GetProcess().Kill() + + def test_process_connect_params(self): + """Test serial:// URL with parameters""" + import termios + try: + self.expect("platform select remote-gdb-server", + substrs=['Platform: remote-gdb-server', 'Connected: no']) + self.expect("process connect " + self.server.get_connect_url() + + "?baud=115200&stop-bits=2", + substrs=['Process', 'stopped']) + + current_attr = self.get_term_attrs() + self.assert_raw_mode(current_attr) + self.assertEqual(current_attr[4:6], 2 * [termios.B115200]) + self.assertEqual(self.get_parity_flags(current_attr), + self.get_parity_flags(self.orig_attr)) + self.assertEqual(self.get_stop_bit_flags(current_attr), + termios.CSTOPB) + finally: + self.dbg.GetSelectedTarget().GetProcess().Kill() + # original mode should be restored on exit + self.assertEqual(self.get_term_attrs(), self.orig_attr) diff --git a/lldb/test/API/functionalities/gdb_remote_client/gdbclientutils.py b/lldb/test/API/functionalities/gdb_remote_client/gdbclientutils.py --- a/lldb/test/API/functionalities/gdb_remote_client/gdbclientutils.py +++ b/lldb/test/API/functionalities/gdb_remote_client/gdbclientutils.py @@ -427,7 +427,7 @@ return libc.ptsname(self._master.fileno()).decode() def get_connect_url(self): - return "file://" + self.get_connect_address() + return "serial://" + self.get_connect_address() def close_server(self): self._slave.close()