Skip to content

Commit 3d3facd

Browse files
Leonardo Pereira Santosbarisione
Leonardo Pereira Santos
authored andcommitted
Read tokens correctly from GDB's output on Windows (#55)
Original patch by Leonardo Pereira Santos (see #55); updated by Marco Barisione.
1 parent 4c0534a commit 3d3facd

File tree

3 files changed

+52
-52
lines changed

3 files changed

+52
-52
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,15 @@
22

33
## dev
44

5+
**Breaking changes**
6+
7+
- Removed `pygdbmi.IoManager.make_non_blocking` which was never meant to be public API
8+
9+
Other changes
10+
511
- Fixed a bug where notifications without a payload were not recognized as such
612
- Invalid octal sequences produced by GDB are left unchanged instead of causing a `UnicodeDecodeError` (#64)
13+
- Fix IoManager not to mangle tokens when reading from stdout on Windows (#55)
714

815
Internal changes
916

pygdbmi/IoManager.py

+38-44
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,14 @@
1717
GdbTimeoutError,
1818
)
1919

20-
if USING_WINDOWS:
21-
import msvcrt
22-
from ctypes import windll, byref, wintypes, WinError, POINTER # type: ignore
23-
from ctypes.wintypes import HANDLE, DWORD, BOOL
24-
else:
20+
from threading import Thread
21+
from queue import Queue, Empty
22+
23+
if not USING_WINDOWS:
2524
import fcntl
2625

2726

28-
__all__ = [
29-
"IoManager",
30-
"make_non_blocking",
31-
]
27+
__all__ = ["IoManager"]
3228

3329

3430
logger = logging.getLogger(__name__)
@@ -68,9 +64,26 @@ def __init__(
6864
self._allow_overwrite_timeout_times = (
6965
self.time_to_check_for_additional_output_sec > 0
7066
)
71-
make_non_blocking(self.stdout)
72-
if self.stderr:
73-
make_non_blocking(self.stderr)
67+
68+
if USING_WINDOWS:
69+
self.queue_stdout = Queue() # type: Queue
70+
self.thread_stdout = Thread(
71+
target=_enqueue_output, args=(self.stdout, self.queue_stdout)
72+
)
73+
self.thread_stdout.daemon = True # thread dies with the program
74+
self.thread_stdout.start()
75+
76+
if self.stderr:
77+
self.queue_stderr = Queue() # type: Queue
78+
self.thread_stderr = Thread(
79+
target=_enqueue_output, args=(self.stderr, self.queue_stderr)
80+
)
81+
self.thread_stderr.daemon = True # thread dies with the program
82+
self.thread_stderr.start()
83+
else:
84+
fcntl.fcntl(self.stdout, fcntl.F_SETFL, os.O_NONBLOCK)
85+
if self.stderr:
86+
fcntl.fcntl(self.stderr, fcntl.F_SETFL, os.O_NONBLOCK)
7487

7588
def get_gdb_response(
7689
self, timeout_sec: float = DEFAULT_GDB_TIMEOUT_SEC, raise_error_on_timeout=True
@@ -110,22 +123,23 @@ def get_gdb_response(
110123

111124
def _get_responses_windows(self, timeout_sec):
112125
"""Get responses on windows. Assume no support for select and use a while loop."""
126+
assert USING_WINDOWS
127+
113128
timeout_time_sec = time.time() + timeout_sec
114129
responses = []
115130
while True:
116131
responses_list = []
132+
117133
try:
118-
self.stdout.flush()
119-
raw_output = self.stdout.readline().replace(b"\r", b"\n")
134+
raw_output = self.queue_stdout.get_nowait()
120135
responses_list = self._get_responses_list(raw_output, "stdout")
121-
except IOError:
136+
except Empty:
122137
pass
123138

124139
try:
125-
self.stderr.flush()
126-
raw_output = self.stderr.readline().replace(b"\r", b"\n")
140+
raw_output = self.queue_stderr.get_nowait()
127141
responses_list += self._get_responses_list(raw_output, "stderr")
128-
except IOError:
142+
except Empty:
129143
pass
130144

131145
responses += responses_list
@@ -138,11 +152,12 @@ def _get_responses_windows(self, timeout_sec):
138152
)
139153
elif time.time() > timeout_time_sec:
140154
break
141-
142155
return responses
143156

144157
def _get_responses_unix(self, timeout_sec):
145158
"""Get responses on unix-like system. Use select to wait for output."""
159+
assert not USING_WINDOWS
160+
146161
timeout_time_sec = time.time() + timeout_sec
147162
responses = []
148163
while True:
@@ -325,28 +340,7 @@ def _buffer_incomplete_responses(
325340
return (raw_output, buf)
326341

327342

328-
def make_non_blocking(file_obj: io.IOBase):
329-
"""make file object non-blocking
330-
Windows doesn't have the fcntl module, but someone on
331-
stack overflow supplied this code as an answer, and it works
332-
http://stackoverflow.com/a/34504971/2893090"""
333-
334-
if USING_WINDOWS:
335-
LPDWORD = POINTER(DWORD)
336-
PIPE_NOWAIT = wintypes.DWORD(0x00000001)
337-
338-
SetNamedPipeHandleState = windll.kernel32.SetNamedPipeHandleState
339-
SetNamedPipeHandleState.argtypes = [HANDLE, LPDWORD, LPDWORD, LPDWORD]
340-
SetNamedPipeHandleState.restype = BOOL
341-
342-
h = msvcrt.get_osfhandle(file_obj.fileno()) # type: ignore
343-
344-
res = windll.kernel32.SetNamedPipeHandleState(h, byref(PIPE_NOWAIT), None, None)
345-
if res == 0:
346-
raise ValueError(WinError())
347-
348-
else:
349-
# Set the file status flag (F_SETFL) on the pipes to be non-blocking
350-
# so we can attempt to read from a pipe with no new data without locking
351-
# the program up
352-
fcntl.fcntl(file_obj, fcntl.F_SETFL, os.O_NONBLOCK)
343+
def _enqueue_output(out, queue):
344+
for line in iter(out.readline, b""):
345+
queue.put(line.replace(b"\r", b"\n"))
346+
# Not necessary to close, it will be done in the main process.

tests/test_pygdbmi.py

+7-8
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,9 @@ def test_controller(self):
275275
assert response["stream"] == "stdout"
276276
assert response["token"] is None
277277

278-
responses = gdbmi.write(["-file-list-exec-source-files", "-break-insert main"])
278+
responses = gdbmi.write(
279+
["-file-list-exec-source-files", "-break-insert main"], timeout_sec=3
280+
)
279281
assert len(responses) != 0
280282

281283
responses = gdbmi.write(["-exec-run", "-exec-continue"], timeout_sec=3)
@@ -293,13 +295,10 @@ def test_controller(self):
293295
assert responses is None
294296
assert gdbmi.gdb_process is None
295297

296-
# Test NoGdbProcessError exception
297-
got_no_process_exception = False
298-
try:
299-
responses = gdbmi.write("-file-exec-and-symbols %s" % c_hello_world_binary)
300-
except IOError:
301-
got_no_process_exception = True
302-
assert got_no_process_exception is True
298+
# Test ValueError exception
299+
self.assertRaises(
300+
ValueError, gdbmi.write, "-file-exec-and-symbols %s" % c_hello_world_binary
301+
)
303302

304303
# Respawn and test signal handling
305304
gdbmi.spawn_new_gdb_subprocess()

0 commit comments

Comments
 (0)