Skip to content

Commit 3da78d2

Browse files
committed
support/unix_connect: mimic XCB behavior
Fallback to TCP when implicit Unix socket connection fails.
1 parent d1500f3 commit 3da78d2

File tree

6 files changed

+190
-82
lines changed

6 files changed

+190
-82
lines changed

Xlib/protocol/display.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,12 @@ class Display(object):
8282
event_classes = event.event_class.copy()
8383

8484
def __init__(self, display = None):
85-
name, host, displayno, screenno = connect.get_display(display)
85+
name, protocol, host, displayno, screenno = connect.get_display(display)
8686

8787
self.display_name = name
8888
self.default_screen = screenno
8989

90-
self.socket = connect.get_socket(name, host, displayno)
90+
self.socket = connect.get_socket(name, protocol, host, displayno)
9191

9292
auth_name, auth_data = connect.get_auth(self.socket,
9393
name, host, displayno)

Xlib/support/connect.py

+10-9
Original file line numberDiff line numberDiff line change
@@ -56,34 +56,35 @@ def _relative_import(modname):
5656

5757

5858
def get_display(display):
59-
"""dname, host, dno, screen = get_display(display)
59+
"""dname, protocol, host, dno, screen = get_display(display)
6060
6161
Parse DISPLAY into its components. If DISPLAY is None, use
6262
the default display. The return values are:
6363
64-
DNAME -- the full display name (string)
65-
HOST -- the host name (string, possibly empty)
66-
DNO -- display number (integer)
67-
SCREEN -- default screen number (integer)
64+
DNAME -- the full display name (string)
65+
PROTOCOL -- the protocol to use (None if automatic)
66+
HOST -- the host name (string, possibly empty)
67+
DNO -- display number (integer)
68+
SCREEN -- default screen number (integer)
6869
"""
6970

7071
modname = _display_mods.get(platform, _default_display_mod)
7172
mod = _relative_import(modname)
7273
return mod.get_display(display)
7374

7475

75-
def get_socket(dname, host, dno):
76-
"""socket = get_socket(dname, host, dno)
76+
def get_socket(dname, protocol, host, dno):
77+
"""socket = get_socket(dname, protocol, host, dno)
7778
78-
Connect to the display specified by DNAME, HOST and DNO, which
79+
Connect to the display specified by DNAME, PROTOCOL, HOST and DNO, which
7980
are the corresponding values from a previous call to get_display().
8081
8182
Return SOCKET, a new socket object connected to the X server.
8283
"""
8384

8485
modname = _socket_mods.get(platform, _default_socket_mod)
8586
mod = _relative_import(modname)
86-
return mod.get_socket(dname, host, dno)
87+
return mod.get_socket(dname, protocol, host, dno)
8788

8889

8990
def get_auth(sock, dname, host, dno):

Xlib/support/unix_connect.py

+31-24
Original file line numberDiff line numberDiff line change
@@ -62,50 +62,57 @@ def get_display(display):
6262

6363
name = display
6464
protocol, host, dno, screen = m.group('proto', 'host', 'dno', 'screen')
65-
if protocol == 'tcp':
65+
if protocol == 'tcp' and not host:
6666
# Host is mandatory when protocol is TCP.
67-
if not host:
68-
raise error.DisplayNameError(display)
69-
elif protocol == 'unix':
70-
# Clear host to force Unix socket connection.
71-
host = ''
72-
else:
73-
# Special case: `unix:0.0` is equivalent to `:0.0`.
74-
if host == 'unix':
75-
host = ''
67+
raise error.DisplayNameError(display)
7668
dno = int(dno)
7769
if screen:
7870
screen = int(screen)
7971
else:
8072
screen = 0
8173

82-
return name, host, dno, screen
74+
return name, protocol, host, dno, screen
75+
8376

77+
def _get_tcp_socket(host, dno):
78+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
79+
s.connect((host, 6000 + dno))
80+
return s
81+
82+
def _get_unix_socket(address):
83+
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
84+
s.connect(address)
85+
return s
8486

85-
def get_socket(dname, host, dno):
87+
def get_socket(dname, protocol, host, dno):
88+
assert protocol in (None, 'tcp', 'unix')
8689
try:
87-
# Darwin funky socket
88-
if (uname[0] == 'Darwin') and host and host.startswith('/private/tmp/'):
89-
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
90-
s.connect(dname)
90+
# Darwin funky socket.
91+
if uname[0] == 'Darwin' and host and host.startswith('/private/tmp/'):
92+
s = _get_unix_socket(dname)
9193

92-
# If hostname (or IP) is provided, use TCP socket
93-
elif host:
94-
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
95-
s.connect((host, 6000 + dno))
94+
# TCP socket, note the special case: `unix:0.0` is equivalent to `:0.0`.
95+
elif (not protocol or protocol != 'unix') and host and host != 'unix':
96+
s = _get_tcp_socket(host, dno)
9697

97-
# Else use Unix socket
98+
# Unix socket.
9899
else:
99100
address = '/tmp/.X11-unix/X%d' % dno
100101
if not os.path.exists(address):
101102
# Use abstract address.
102103
address = '\0' + address
103-
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
104-
s.connect(address)
104+
try:
105+
s = _get_unix_socket(address)
106+
except socket.error:
107+
if not protocol and not host:
108+
# If no protocol/host was specified, fallback to TCP.
109+
s = _get_tcp_socket(host, dno)
110+
else:
111+
raise
105112
except socket.error as val:
106113
raise error.DisplayConnectionError(dname, str(val))
107114

108-
# Make sure that the connection isn't inherited in child processes
115+
# Make sure that the connection isn't inherited in child processes.
109116
fcntl.fcntl(s.fileno(), F_SETFD, FD_CLOEXEC)
110117

111118
return s

Xlib/support/vms_connect.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def get_display(display):
3232
# check DECW$DISPLAY instead, but that has to wait
3333

3434
if display is None:
35-
return ':0.0', 'localhost', 0, 0
35+
return ':0.0', None, 'localhost', 0, 0
3636

3737
m = display_re.match(display)
3838
if not m:
@@ -52,10 +52,10 @@ def get_display(display):
5252
else:
5353
screen = 0
5454

55-
return name, host, dno, screen
55+
return name, None, host, dno, screen
5656

5757

58-
def get_socket(dname, host, dno):
58+
def get_socket(dname, protocol, host, dno):
5959
try:
6060
# Always use TCP/IP sockets. Later it would be nice to
6161
# be able to use DECNET och LOCAL connections.

dev-requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
coverage
22
codecov
3+
mock
34
nose
45
setuptools-scm

test/test_unix_connect.py

+143-44
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,158 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8 -*
33

4+
from functools import partial
5+
import socket
46
import sys
57
import unittest
68

9+
from mock import patch
10+
711
from Xlib.support import unix_connect
8-
from Xlib.error import DisplayNameError
12+
from Xlib.error import DisplayConnectionError, DisplayNameError
913

1014

1115
@unittest.skipUnless(sys.platform.startswith('linux'), 'Linux specific tests')
1216
class TestUnixConnect(unittest.TestCase):
1317

14-
def test_get_display(self):
15-
# Valid cases.
16-
for display, expected in (
17-
# Implicit Unix socket connections.
18-
(':0.1', ('', 0, 1)),
19-
(':4', ('', 4, 0)),
20-
# Implicit TCP connections.
21-
('foo:1.2', ('foo', 1, 2)),
22-
('bar:5', ('bar', 5, 0)),
23-
# Explicit Unix socket connections.
24-
('unix/foo:4.3', ('', 4, 3)),
25-
('unix/:66', ('', 66, 0)),
26-
# Explicit TCP connections.
27-
('tcp/foo:11.1', ('foo', 11, 1)),
28-
('tcp/bar:66.6', ('bar', 66, 6)),
29-
('tcp/unix:54.3', ('unix', 54, 3)),
30-
# Special case: `unix:0.0` is equivalent to `:0.0`.
31-
('unix:99.5', ('', 99, 5)),
32-
('unix:42', ('', 42, 0)),
33-
):
34-
result = unix_connect.get_display(display)
35-
self.assertEqual(result, (display,) + expected)
36-
# Invalid cases.
37-
for display in (
38-
# No display number.
39-
'',
40-
':',
41-
'foo',
42-
'bar:',
43-
# Bad screen number.
44-
':48.',
45-
':47.f',
46-
# Bad hostname.
47-
u'fòó:0',
48-
u'tcp/bàr:1',
49-
u'unix/fòóbàr:2',
50-
# Bad protocol.
51-
'udp/foo:0'
52-
# With explicit TCP connections, hostname must be set.
53-
'tcp/:0',
54-
):
55-
with self.assertRaises(DisplayNameError):
56-
unix_connect.get_display(display)
18+
def test_get_display(self):
19+
# Valid cases.
20+
for display, expected in (
21+
# Implicit Unix socket connections.
22+
(':0.1', (None, '', 0, 1)),
23+
(':4', (None, '', 4, 0)),
24+
# Implicit TCP connections.
25+
('foo:1.2', (None, 'foo', 1, 2)),
26+
('bar:5', (None, 'bar', 5, 0)),
27+
# Explicit Unix socket connections.
28+
('unix/foo:4.3', ('unix', 'foo', 4, 3)),
29+
('unix/:66', ('unix', '', 66, 0)),
30+
# Explicit TCP connections.
31+
('tcp/foo:11.1', ('tcp', 'foo', 11, 1)),
32+
('tcp/bar:66.6', ('tcp', 'bar', 66, 6)),
33+
('tcp/unix:54.3', ('tcp', 'unix', 54, 3)),
34+
# Special case: `unix:0.0` is equivalent to `:0.0`.
35+
('unix:99.5', (None, 'unix', 99, 5)),
36+
('unix:42', (None, 'unix', 42, 0)),
37+
):
38+
result = unix_connect.get_display(display)
39+
self.assertEqual(result, (display,) + expected)
40+
# Invalid cases.
41+
for display in (
42+
# No display number.
43+
'',
44+
':',
45+
'foo',
46+
'bar:',
47+
# Bad screen number.
48+
':48.',
49+
':47.f',
50+
# Bad hostname.
51+
u'fòó:0',
52+
u'tcp/bàr:1',
53+
u'unix/fòóbàr:2',
54+
# Bad protocol.
55+
'udp/foo:0'
56+
# With explicit TCP connections, hostname must be set.
57+
'tcp/:0',
58+
):
59+
with self.assertRaises(DisplayNameError):
60+
unix_connect.get_display(display)
61+
62+
def test_get_socket(self):
63+
class FakeSocket(object):
64+
def fileno(self):
65+
return 42
66+
calls = []
67+
def _get_socket(socket_type, raises, *params):
68+
calls.append(('_get_%s_socket' % socket_type,) + params)
69+
if raises:
70+
raise socket.error()
71+
return FakeSocket()
72+
def path_exists(returns, path):
73+
calls.append(('os.path.exists', path))
74+
return returns
75+
def fcntl(*args):
76+
calls.append(('fcntl',) + args)
77+
for params, allow_unix, unix_addr_exists, allow_tcp, expect_connection_error, expected_calls in (
78+
# Successful explicit TCP socket connection.
79+
(('tcp/host:6', None, 'host', 6), False, False, True, False, [
80+
('_get_tcp_socket', 'host', 6),
81+
]),
82+
# Failed explicit TCP socket connection.
83+
(('tcp/host:6', None, 'host', 6), False, False, False, True, [
84+
('_get_tcp_socket', 'host', 6),
85+
]),
86+
# Successful implicit TCP socket connection.
87+
(('host:5', None, 'host', 5), False, False, True, False, [
88+
('_get_tcp_socket', 'host', 5),
89+
]),
90+
# Failed implicit TCP socket connection.
91+
(('host:5', None, 'host', 5), False, False, False, True, [
92+
('_get_tcp_socket', 'host', 5),
93+
]),
94+
# Successful explicit Unix socket connection.
95+
(('unix/name:0', 'unix', 'name', 0), True, True, False, False, [
96+
('os.path.exists', '/tmp/.X11-unix/X0'),
97+
('_get_unix_socket', '/tmp/.X11-unix/X0'),
98+
]),
99+
# Failed explicit Unix socket connection.
100+
(('unix/name:0', 'unix', 'name', 0), False, True, False, True, [
101+
('os.path.exists', '/tmp/.X11-unix/X0'),
102+
('_get_unix_socket', '/tmp/.X11-unix/X0'),
103+
]),
104+
# Successful explicit Unix socket connection, variant.
105+
(('unix:0', None, 'unix', 0), True, True, False, False, [
106+
('os.path.exists', '/tmp/.X11-unix/X0'),
107+
('_get_unix_socket', '/tmp/.X11-unix/X0'),
108+
]),
109+
# Failed explicit Unix socket connection, variant.
110+
(('unix:0', None, 'unix', 0), False, True, False, True, [
111+
('os.path.exists', '/tmp/.X11-unix/X0'),
112+
('_get_unix_socket', '/tmp/.X11-unix/X0'),
113+
]),
114+
# Successful implicit Unix socket connection.
115+
((':4', None, '', 4), True, True, False, False, [
116+
('os.path.exists', '/tmp/.X11-unix/X4'),
117+
('_get_unix_socket', '/tmp/.X11-unix/X4'),
118+
]),
119+
# Successful implicit Unix socket connection, abstract address.
120+
((':3', None, '', 3), True, False, False, False, [
121+
('os.path.exists', '/tmp/.X11-unix/X3'),
122+
('_get_unix_socket', '\0/tmp/.X11-unix/X3'),
123+
]),
124+
# Failed implicit Unix socket connection, successful fallback on TCP.
125+
((':2', None, '', 2), False, False, True, False, [
126+
('os.path.exists', '/tmp/.X11-unix/X2'),
127+
('_get_unix_socket', '\0/tmp/.X11-unix/X2'),
128+
('_get_tcp_socket', '', 2),
129+
]),
130+
# Failed implicit Unix socket connection, failed fallback on TCP.
131+
((':1', None, '', 1), False, False, False, True, [
132+
('os.path.exists', '/tmp/.X11-unix/X1'),
133+
('_get_unix_socket', '\0/tmp/.X11-unix/X1'),
134+
('_get_tcp_socket', '', 1),
135+
]),
136+
):
137+
with \
138+
patch('Xlib.support.unix_connect._get_unix_socket',
139+
partial(_get_socket, 'unix', not allow_unix)), \
140+
patch('Xlib.support.unix_connect._get_tcp_socket',
141+
partial(_get_socket, 'tcp', not allow_tcp)), \
142+
patch('os.path.exists',
143+
partial(path_exists, unix_addr_exists)), \
144+
patch('fcntl.fcntl', fcntl):
145+
del calls[:]
146+
if expect_connection_error:
147+
with self.assertRaises(DisplayConnectionError):
148+
unix_connect.get_socket(*params)
149+
else:
150+
s = unix_connect.get_socket(*params)
151+
self.assertIsInstance(s, FakeSocket)
152+
expected_calls.append(('fcntl', 42,
153+
unix_connect.F_SETFD,
154+
unix_connect.FD_CLOEXEC))
155+
self.assertEqual(calls, expected_calls)
57156

58157

59158
if __name__ == '__main__':

0 commit comments

Comments
 (0)