Skip to content

Commit a4e1d20

Browse files
authored
Merge pull request #30 from kmaork/fixing_todos
Fixing todos
2 parents 0daa098 + 5324576 commit a4e1d20

File tree

10 files changed

+159
-151
lines changed

10 files changed

+159
-151
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
with:
1919
python-version: ${{ matrix.python-version }}
2020
- name: Install dependencies
21-
run: pip install tox tox-wheel
21+
run: pip install tox tox-gh-actions tox-wheel
2222
- name: Test sdist with tox
2323
run: python -m tox
2424
- name: Upload sdist

madbg/client.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import termios
88
from contextlib import contextmanager
99

10-
from .communication import pipe_until_closed, send_message
10+
from .communication import Piping, send_message
1111
from .consts import DEFAULT_IP, DEFAULT_PORT, STDIN_FILENO, STDOUT_FILENO, DEFAULT_CONNECT_TIMEOUT
1212

1313

@@ -60,7 +60,8 @@ def connect_to_server(ip, port, timeout):
6060
s.close()
6161

6262

63-
def connect_to_debugger(ip=DEFAULT_IP, port=DEFAULT_PORT, timeout=DEFAULT_CONNECT_TIMEOUT):
63+
def connect_to_debugger(ip=DEFAULT_IP, port=DEFAULT_PORT, timeout=DEFAULT_CONNECT_TIMEOUT,
64+
in_fd=STDIN_FILENO, out_fd=STDOUT_FILENO):
6465
with connect_to_server(ip, port, timeout) as socket:
6566
tty_handle = get_tty_handle()
6667
term_size = os.get_terminal_size(tty_handle)
@@ -71,4 +72,4 @@ def connect_to_debugger(ip=DEFAULT_IP, port=DEFAULT_PORT, timeout=DEFAULT_CONNEC
7172
send_message(socket, term_data)
7273
with prepare_terminal():
7374
socket_fd = socket.fileno()
74-
pipe_until_closed({STDIN_FILENO: socket_fd, socket_fd: STDOUT_FILENO})
75+
Piping({in_fd: socket_fd, socket_fd: out_fd}).run()

madbg/communication.py

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import pickle
2-
import select
32
import fcntl
43
import os
54
import struct
5+
from collections import defaultdict
66
from contextlib import contextmanager
7+
from functools import partial
8+
from asyncio import new_event_loop
79
from io import BytesIO
810

9-
from madbg.utils import loop_in_thread, opposite_dict
11+
from .utils import run_thread, opposite_dict
1012

1113
MESSAGE_LENGTH_FMT = 'I'
1214

@@ -28,41 +30,51 @@ def blocking_read(fd, n):
2830
return io.getvalue()
2931

3032

31-
def pipe_once(pipe_dict):
32-
# If read fails, write will fail. But if write fail, we might be still able to read.
33-
# TODO: use wakeup fd instead of 0 timeout polling (os.pipe? eventfd?)
34-
# TODO: can use splice or ebpf
35-
if not pipe_dict:
36-
return
37-
reverse_pipe_dict = opposite_dict(pipe_dict)
38-
for read_fd in select.select(list(pipe_dict), [], [], 0)[0]:
39-
write_fd = pipe_dict[read_fd]
33+
class Piping:
34+
def __init__(self, pipe_dict):
35+
self.buffers = defaultdict(bytes)
36+
self.loop = new_event_loop()
37+
for src_fd, dest_fd in pipe_dict.items():
38+
self.loop.add_reader(src_fd, partial(self._read, src_fd, dest_fd))
39+
self.loop.add_writer(dest_fd, partial(self._write, dest_fd))
40+
self.readers_to_writers = dict(pipe_dict)
41+
self.writers_to_readers = opposite_dict(pipe_dict)
42+
43+
def _remove_writer(self, writer_fd):
44+
self.loop.remove_writer(writer_fd)
45+
for reader_fd in self.writers_to_readers.pop(writer_fd):
46+
self.readers_to_writers.pop(reader_fd)
47+
48+
def _remove_reader(self, reader_fd):
49+
# remove all writers that im the last to write to, remove all that write to me, if nothing left stop loop
50+
self.loop.remove_reader(reader_fd)
51+
writer_fd = self.readers_to_writers.pop(reader_fd)
52+
writer_readers = self.writers_to_readers[writer_fd]
53+
writer_readers.remove(reader_fd)
54+
if not writer_fd:
55+
self._remove_writer(writer_fd)
56+
57+
def _read(self, src_fd, dest_fd):
4058
try:
41-
data = os.read(read_fd, 1024)
42-
if not data:
43-
raise OSError('EOF')
59+
data = os.read(src_fd, 1024)
4460
except OSError:
45-
pipe_dict.pop(read_fd, None)
46-
for writing_fd in reverse_pipe_dict[read_fd]:
47-
pipe_dict.pop(writing_fd, None)
61+
data = ''
62+
if data:
63+
self.buffers[dest_fd] += data
4864
else:
49-
try:
50-
os.write(write_fd, data)
51-
except OSError:
52-
pipe_dict.pop(read_fd, None)
53-
54-
55-
@contextmanager
56-
def pipe_in_background(pipe_dict):
57-
pipe_dict = dict(pipe_dict)
58-
with loop_in_thread(pipe_once, pipe_dict):
59-
yield
60-
61-
62-
def pipe_until_closed(pipe_dict):
63-
pipe_dict = dict(pipe_dict)
64-
while pipe_dict:
65-
pipe_once(pipe_dict)
65+
self._remove_reader(src_fd)
66+
if src_fd in self.writers_to_readers:
67+
self._remove_writer(src_fd)
68+
if not self.readers_to_writers:
69+
self.loop.stop()
70+
71+
def _write(self, dest_fd):
72+
buffer = self.buffers[dest_fd]
73+
if buffer:
74+
self.buffers[dest_fd] = buffer[os.write(dest_fd, buffer):]
75+
76+
def run(self):
77+
self.loop.run_forever()
6678

6779

6880
def send_message(sock, obj):

madbg/debugger.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88
from IPython.terminal.interactiveshell import TerminalInteractiveShell
99
from prompt_toolkit.input.vt100 import Vt100Input
1010
from prompt_toolkit.output.vt100 import Vt100_Output
11+
from inspect import currentframe
1112

12-
from .utils import preserve_sys_state, get_client_connection, use_context
13-
from .tty_utils import print_to_ctty, open_pty, resize_terminal, modify_terminal, set_ctty
14-
from .communication import receive_message, pipe_in_background
13+
from .utils import preserve_sys_state, get_client_connection, use_context, run_thread
14+
from .tty_utils import print_to_ctty, PTY
15+
from .communication import receive_message, Piping
1516

1617

1718
class RemoteIPythonDebugger(TerminalPdb):
@@ -58,7 +59,7 @@ def set_trace(self, frame=None, done_callback=None):
5859
""" Overriding super to add the done_callback argument, allowing cleanup after a debug session """
5960
td = lambda *args: self.trace_dispatch(*args, done_callback=done_callback)
6061
if frame is None:
61-
frame = sys._getframe().f_back
62+
frame = currentframe().f_back
6263
self.reset()
6364
while frame:
6465
frame.f_trace = td
@@ -116,24 +117,22 @@ def connect_and_set_trace(cls, ip, port, frame=None):
116117
def start(cls, sock_fd):
117118
term_data = receive_message(sock_fd)
118119
term_attrs, term_type, term_size = term_data['term_attrs'], term_data['term_type'], term_data['term_size']
119-
with open_pty() as (master_fd, slave_fd):
120-
resize_terminal(slave_fd, term_size[0], term_size[1])
121-
modify_terminal(slave_fd, term_attrs)
122-
set_ctty(slave_fd)
123-
with pipe_in_background({sock_fd: master_fd, master_fd: sock_fd}):
124-
slave_reader = os.fdopen(slave_fd, 'r')
125-
slave_writer = os.fdopen(slave_fd, 'w')
120+
with PTY.open() as pty:
121+
pty.resize(term_size[0], term_size[1])
122+
pty.set_tty_attrs(term_attrs)
123+
pty.make_ctty()
124+
piping = Piping({sock_fd: pty.master_fd, pty.master_fd: sock_fd})
125+
with run_thread(piping.run):
126+
slave_reader = os.fdopen(pty.slave_fd, 'r')
127+
slave_writer = os.fdopen(pty.slave_fd, 'w')
126128
try:
127129
yield cls(slave_reader, slave_writer, term_type)
128130
except Exception:
129131
print(traceback.format_exc(), file=slave_writer)
130132
raise
131133
finally:
132-
# We flush to make sure the message is written before the last pipe iteration
133134
print('Closing connection', file=slave_writer, flush=True)
134-
# TODO: solve race
135-
import time
136-
time.sleep(0.05)
135+
slave_writer.close()
137136

138137
@classmethod
139138
@contextmanager

madbg/tty_utils.py

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pty
44
import struct
55
from contextlib import contextmanager
6+
from dataclasses import dataclass
67
from multiprocessing.pool import Pool
78
import signal
89
import fcntl
@@ -77,40 +78,49 @@ def detach_current_ctty():
7778
os.close(ctty_fd)
7879

7980

80-
def set_ctty(fd):
81-
detach_current_ctty()
82-
attach_ctty(fd)
83-
84-
85-
def resize_terminal(fd, rows, cols):
86-
winsize = struct.pack("HHHH", rows, cols, 0, 0)
87-
fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
88-
89-
90-
def modify_terminal(fd, tc_attrs, when=termios.TCSANOW):
91-
return
92-
IFLAG = 0
93-
OFLAG = 1
94-
CFLAG = 2
95-
LFLAG = 3
96-
ISPEED = 4
97-
OSPEED = 5
98-
CC = 6
99-
tc_attrs = tc_attrs[:]
100-
current_attrs = termios.tcgetattr(fd)
101-
tc_attrs[CC] = current_attrs[termios.CC]
102-
termios.tcsetattr(fd, when, tc_attrs)
103-
104-
105-
@contextmanager
106-
def open_pty():
107-
master_fd, slave_fd = pty.openpty()
108-
try:
109-
yield master_fd, slave_fd
110-
finally:
111-
with ignore_signal(signal.SIGHUP):
112-
os.close(master_fd)
113-
os.close(slave_fd)
81+
@dataclass
82+
class PTY:
83+
master_fd: int
84+
slave_fd: int
85+
_closed: bool = False
86+
87+
def resize(self, rows, cols):
88+
winsize = struct.pack("HHHH", rows, cols, 0, 0)
89+
fcntl.ioctl(self.slave_fd, termios.TIOCSWINSZ, winsize)
90+
91+
def close(self):
92+
if not self._closed:
93+
with ignore_signal(signal.SIGHUP):
94+
os.close(self.master_fd)
95+
self._closed = True
96+
97+
def set_tty_attrs(self, tc_attrs, when=termios.TCSANOW):
98+
return
99+
IFLAG = 0
100+
OFLAG = 1
101+
CFLAG = 2
102+
LFLAG = 3
103+
ISPEED = 4
104+
OSPEED = 5
105+
CC = 6
106+
tc_attrs = tc_attrs[:]
107+
current_attrs = termios.tcgetattr(fd)
108+
tc_attrs[CC] = current_attrs[termios.CC]
109+
termios.tcsetattr(self.slave_fd, when, tc_attrs)
110+
111+
def make_ctty(self):
112+
detach_current_ctty()
113+
attach_ctty(self.slave_fd)
114+
115+
@classmethod
116+
@contextmanager
117+
def open(cls):
118+
master_fd, slave_fd = pty.openpty()
119+
self = cls(master_fd, slave_fd)
120+
try:
121+
yield self
122+
finally:
123+
self.close()
114124

115125

116126
def print_to_ctty(string):

madbg/utils.py

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from concurrent.futures.thread import ThreadPoolExecutor
77
from contextlib import contextmanager, ExitStack
88

9+
from madbg.tty_utils import print_to_ctty
10+
911

1012
@contextmanager
1113
def preserve_sys_state():
@@ -24,7 +26,8 @@ def get_client_connection(ip, port):
2426
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
2527
server_socket.bind((ip, port))
2628
server_socket.listen(1)
27-
sock, _ = server_socket.accept()
29+
sock, (client_ip, client_port) = server_socket.accept()
30+
print_to_ctty(f'Client connected from {client_ip}:{client_port}')
2831
server_socket.close()
2932
try:
3033
yield sock.fileno()
@@ -60,25 +63,8 @@ def run_thread(func, *args, **kwargs):
6063
future.result()
6164

6265

63-
@contextmanager
64-
def loop_in_thread(func, *args, iteration_after_exit=True, **kwargs):
65-
cont = True
66-
67-
def loop():
68-
while cont:
69-
func(*args, **kwargs)
70-
71-
with run_thread(loop) as future:
72-
try:
73-
yield future
74-
finally:
75-
cont = False
76-
if iteration_after_exit:
77-
func(*args, **kwargs)
78-
79-
80-
def opposite_dict(dikt):
81-
opposite = defaultdict(list)
82-
for key, value in dikt.items():
83-
opposite[value].append(key)
66+
def opposite_dict(dict_):
67+
opposite = defaultdict(set)
68+
for key, value in dict_.items():
69+
opposite[value].add(key)
8470
return opposite

tests/system/test_set_trace.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def test_set_trace(port, start_debugger_with_ctty):
2525
client_future = run_in_process(run_client, port, b'value_to_change += 1\nc\n')
2626
assert debugger_future.result(JOIN_TIMEOUT)
2727
client_output = client_future.result(JOIN_TIMEOUT)
28-
# TODO: why does this assert fail? Problem in piping?
28+
# TODO: waiting for fix in run_client
2929
# assert b'Closing connection' in client_output
3030

3131

0 commit comments

Comments
 (0)