Skip to content

Commit 0ef6ea1

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 25ddac0 commit 0ef6ea1

File tree

3 files changed

+44
-48
lines changed

3 files changed

+44
-48
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Other changes
1010

1111
- Fixed a bug where notifications without a payload were not recognized as such
1212
- 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)
1314

1415
Internal changes
1516

pygdbmi/IoManager.py

+36-40
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import select
99
import time
1010
from pprint import pformat
11+
from queue import Empty, Queue
12+
from threading import Thread
1113
from typing import Any, Dict, List, Optional, Tuple, Union
1214

1315
from pygdbmi import gdbmiparser
@@ -19,11 +21,7 @@
1921
)
2022

2123

22-
if USING_WINDOWS:
23-
import msvcrt
24-
from ctypes import POINTER, WinError, byref, windll, wintypes # type: ignore
25-
from ctypes.wintypes import BOOL, DWORD, HANDLE
26-
else:
24+
if not USING_WINDOWS:
2725
import fcntl
2826

2927

@@ -67,9 +65,26 @@ def __init__(
6765
self._allow_overwrite_timeout_times = (
6866
self.time_to_check_for_additional_output_sec > 0
6967
)
70-
_make_non_blocking(self.stdout)
71-
if self.stderr:
72-
_make_non_blocking(self.stderr)
68+
69+
if USING_WINDOWS:
70+
self.queue_stdout = Queue() # type: Queue
71+
self.thread_stdout = Thread(
72+
target=_enqueue_output, args=(self.stdout, self.queue_stdout)
73+
)
74+
self.thread_stdout.daemon = True # thread dies with the program
75+
self.thread_stdout.start()
76+
77+
if self.stderr:
78+
self.queue_stderr = Queue() # type: Queue
79+
self.thread_stderr = Thread(
80+
target=_enqueue_output, args=(self.stderr, self.queue_stderr)
81+
)
82+
self.thread_stderr.daemon = True # thread dies with the program
83+
self.thread_stderr.start()
84+
else:
85+
fcntl.fcntl(self.stdout, fcntl.F_SETFL, os.O_NONBLOCK)
86+
if self.stderr:
87+
fcntl.fcntl(self.stderr, fcntl.F_SETFL, os.O_NONBLOCK)
7388

7489
def get_gdb_response(
7590
self, timeout_sec: float = DEFAULT_GDB_TIMEOUT_SEC, raise_error_on_timeout=True
@@ -109,22 +124,23 @@ def get_gdb_response(
109124

110125
def _get_responses_windows(self, timeout_sec):
111126
"""Get responses on windows. Assume no support for select and use a while loop."""
127+
assert USING_WINDOWS
128+
112129
timeout_time_sec = time.time() + timeout_sec
113130
responses = []
114131
while True:
115132
responses_list = []
133+
116134
try:
117-
self.stdout.flush()
118-
raw_output = self.stdout.readline().replace(b"\r", b"\n")
135+
raw_output = self.queue_stdout.get_nowait()
119136
responses_list = self._get_responses_list(raw_output, "stdout")
120-
except IOError:
137+
except Empty:
121138
pass
122139

123140
try:
124-
self.stderr.flush()
125-
raw_output = self.stderr.readline().replace(b"\r", b"\n")
141+
raw_output = self.queue_stderr.get_nowait()
126142
responses_list += self._get_responses_list(raw_output, "stderr")
127-
except IOError:
143+
except Empty:
128144
pass
129145

130146
responses += responses_list
@@ -137,11 +153,12 @@ def _get_responses_windows(self, timeout_sec):
137153
)
138154
elif time.time() > timeout_time_sec:
139155
break
140-
141156
return responses
142157

143158
def _get_responses_unix(self, timeout_sec):
144159
"""Get responses on unix-like system. Use select to wait for output."""
160+
assert not USING_WINDOWS
161+
145162
timeout_time_sec = time.time() + timeout_sec
146163
responses = []
147164
while True:
@@ -324,28 +341,7 @@ def _buffer_incomplete_responses(
324341
return (raw_output, buf)
325342

326343

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

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

282284
responses = gdbmi.write(["-exec-run", "-exec-continue"], timeout_sec=3)
@@ -294,13 +296,10 @@ def test_controller(self):
294296
assert responses is None
295297
assert gdbmi.gdb_process is None
296298

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

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

0 commit comments

Comments
 (0)