Merge lp:~brandontschaefer/libertine/lsb-refactor into lp:libertine
- lsb-refactor
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Christopher Townsend |
Approved revision: | 284 |
Merged at revision: | 280 |
Proposed branch: | lp:~brandontschaefer/libertine/lsb-refactor |
Merge into: | lp:libertine |
Diff against target: |
838 lines (+466/-246) 10 files modified
debian/libertine-tools.install (+0/-1) python/libertine/Libertine.py (+198/-14) python/libertine/__init__.py (+13/-2) tests/unit/CMakeLists.txt (+16/-0) tests/unit/libertine_session_bridge_tests.py (+101/-0) tests/unit/libertine_socket_tests.py (+72/-0) tools/CMakeLists.txt (+2/-2) tools/libertine-launch (+64/-9) tools/libertine-session-bridge (+0/-205) tools/libertine-session-bridge.1 (+0/-13) |
To merge this branch: | bzr merge lp:~brandontschaefer/libertine/lsb-refactor |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Libertine CI Bot | continuous-integration | Approve | |
Christopher Townsend | Approve | ||
Larry Price | Approve | ||
Review via email: mp+300522@code.launchpad.net |
Commit message
Refactors the LSB to be a class. So we will be on the same process and we can actually test LSB more then just running it and checking exit code.
Description of the change
Refactors the LSB to be a class. So we will be on the same process and we can actually test LSB more then just running it and checking exit code.
Needs testing for maliit and on the phone. Will do tomorrow!
- 277. By Brandon Schaefer
-
* Remove lsb
- 278. By Brandon Schaefer
-
* Remove all mention of lsb script
Libertine CI Bot (libertine-ci-bot) wrote : | # |
Libertine CI Bot (libertine-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:278
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to tr...
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:278
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
Larry Price (larryprice) wrote : | # |
Nice! I've left you a sprinkling of inline comments, and I'll go ahead and check it out on U8.
Larry Price (larryprice) wrote : | # |
Also don't forget to merge in devel soon if you haven't already.
Larry Price (larryprice) wrote : | # |
Works with gedit, but not with firefox:
Exception in thread Thread-1:
Traceback (most recent call last):
File "/usr/lib/
self.run()
File "/usr/lib/
self.
File "/usr/lib/
data = sock.recv(4096)
ConnectionReset
- 279. By Brandon Schaefer
-
* Merge trunk
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:279
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
Brandon Schaefer (brandontschaefer) wrote : | # |
Commented on diff comments
- 280. By Brandon Schaefer
-
* Address inline comments
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:280
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
Larry Price (larryprice) wrote : | # |
inline - just a typo
Larry Price (larryprice) wrote : | # |
I verified the latest version of this branch is working quite well! Nicely done.
Since I'll be out, I'm going to go ahead and Approve this with the assumption that:
* The typo is fixed
* The "create" function is updated to use socket classes as discussed in IRC.
- 281. By Brandon Schaefer
-
* Makea session socket which will create an actual AF_UNIX socket to bind to a path
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:281
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
- 282. By Brandon Schaefer
-
* Fix the maliit server. It needs to go befoe dbus since it needs to talk
with the real dbus socket not the session one (before it gets setup).
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:282
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
Brandon Schaefer (brandontschaefer) wrote : | # |
Tested on desktop (dbus only), desktop (dbus + maliit). Both working. Attempting to test on the phone but its not being nice!
- 283. By Brandon Schaefer
-
* Spelling
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:283
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
- 284. By Brandon Schaefer
-
* Use process instead of threads so we dont have to timeout our select
Christopher Townsend (townsend) wrote : | # |
Ok, great, thanks!
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:284
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
Preview Diff
1 | === modified file 'debian/libertine-tools.install' |
2 | --- debian/libertine-tools.install 2016-07-29 18:59:07 +0000 |
3 | +++ debian/libertine-tools.install 2016-08-05 19:43:28 +0000 |
4 | @@ -1,5 +1,4 @@ |
5 | usr/bin/libertine-launch |
6 | -usr/bin/libertine-session-bridge |
7 | usr/bin/libertine-container-manager |
8 | usr/bin/libertine-xmir |
9 | usr/share/bash-completion/completions/libertine-container-manager |
10 | |
11 | === modified file 'python/libertine/Libertine.py' |
12 | --- python/libertine/Libertine.py 2016-07-11 19:54:31 +0000 |
13 | +++ python/libertine/Libertine.py 2016-08-05 19:43:28 +0000 |
14 | @@ -14,13 +14,18 @@ |
15 | |
16 | from .AppDiscovery import AppLauncherCache |
17 | from gi.repository import Libertine |
18 | +from multiprocessing import Process |
19 | +from socket import * |
20 | import abc |
21 | import contextlib |
22 | import libertine.utils |
23 | import os |
24 | import psutil |
25 | +import select |
26 | import shutil |
27 | import shlex |
28 | +import signal |
29 | +import sys |
30 | |
31 | from libertine.ContainersConfig import ContainersConfig |
32 | from libertine.HostInfo import HostInfo |
33 | @@ -435,17 +440,200 @@ |
34 | return handle_runtime_error(e) |
35 | |
36 | |
37 | +class Socket(object): |
38 | + """ |
39 | + A simple socket wrapper class. This will wrap a socket |
40 | + This is used for RAII and to take ownership of the socket |
41 | + |
42 | + :param socket: A python socket to be wrapped |
43 | + """ |
44 | + def __init__(self, sock): |
45 | + if not isinstance(sock, socket): |
46 | + raise TypeError("expected socket to be a python socket class instead found: '{}'".format(sock.__class__)) |
47 | + |
48 | + self.socket = sock |
49 | + |
50 | + def __del__(self): |
51 | + self.socket.shutdown(SHUT_RDWR) |
52 | + self.socket.close() |
53 | + |
54 | + """ |
55 | + Implement equality checking for other instances of this class or ints only. |
56 | + |
57 | + :param other: Either a Socket class or an int |
58 | + """ |
59 | + def __eq__(self, other): |
60 | + if isinstance(other, Socket): |
61 | + return self.socket == other.socket |
62 | + elif isinstance(other, socket): |
63 | + return self.socket == other |
64 | + elif isinstance(other, int): |
65 | + return self.socket.fileno() == other |
66 | + else: |
67 | + raise TypeError("unsupported operand type(s) for ==: '{}' and '{}'".format(self.__class__, type(other))) |
68 | + |
69 | + def __ne__(self, other): |
70 | + return not self == other |
71 | + |
72 | + def __hash__(self): |
73 | + return self.socket.fileno() |
74 | + |
75 | + def socket(self): |
76 | + return self.socket |
77 | + |
78 | +class SessionSocket(Socket): |
79 | + """ |
80 | + Creates a AF_UNIX socket from a path to be used as the session socket. |
81 | + |
82 | + :param path: The path the socket will be binded with |
83 | + """ |
84 | + def __init__(self, session_path): |
85 | + try: |
86 | + sock = socket(AF_UNIX, SOCK_STREAM) |
87 | + except: |
88 | + sock = None |
89 | + raise |
90 | + else: |
91 | + try: |
92 | + sock.bind(session_path) |
93 | + sock.listen(5) |
94 | + except: |
95 | + sock.close() |
96 | + sock = None |
97 | + raise |
98 | + else: |
99 | + super().__init__(sock) |
100 | + self.session_path = session_path |
101 | + |
102 | + def __del__(self): |
103 | + super().__del__() |
104 | + os.remove(self.session_path) |
105 | + |
106 | + |
107 | +class HostSessionSocketPair(): |
108 | + def __init__(self, host_socket_path, session_socket_path): |
109 | + self.host_socket_path = host_socket_path |
110 | + self.session_socket_path = session_socket_path |
111 | + |
112 | + |
113 | +class LibertineSessionBridge(object): |
114 | + """ |
115 | + Creates a session bridge which will pair host and session sockets to proxy the info |
116 | + from the session to the host and vice versa. |
117 | + |
118 | + :param host_session_socket_paths: A list of pairs container valid sockets to proxy {host:session} |
119 | + """ |
120 | + def __init__(self, host_session_socket_paths): |
121 | + self.descriptors = [] |
122 | + self.host_session_socket_path_map = {} |
123 | + self.socket_pairs = {} |
124 | + |
125 | + for host_session_socket_pair in host_session_socket_paths: |
126 | + host_path = host_session_socket_pair.host_socket_path |
127 | + session_path = host_session_socket_pair.session_socket_path |
128 | + |
129 | + socket = SessionSocket(session_path) |
130 | + |
131 | + self.descriptors.append(socket) |
132 | + self.host_session_socket_path_map.update({socket:host_path}) |
133 | + |
134 | + """ |
135 | + If we end up going out of scope/error let's make sure we clean up sockets and paths. |
136 | + """ |
137 | + def __del__(self): |
138 | + del self.socket_pairs |
139 | + del self.descriptors |
140 | + del self.host_session_socket_path_map |
141 | + |
142 | + """ |
143 | + When a new connection is made on one of the main sockets we have to create a new |
144 | + socket pairing with the container socket. |
145 | + |
146 | + :param host_path: The raw path to the container socket |
147 | + :param container_sock: The socket which received a new connection |
148 | + """ |
149 | + def accept_new_connection(self, host_path, container_sock): |
150 | + newconn = Socket(container_sock.accept()[0]) |
151 | + self.descriptors.append(newconn) |
152 | + |
153 | + host_sock = Socket(socket(AF_UNIX, SOCK_STREAM)) |
154 | + host_sock.socket.connect(host_path) |
155 | + self.descriptors.append(host_sock) |
156 | + |
157 | + self.socket_pairs.update({newconn:host_sock}) |
158 | + self.socket_pairs.update({host_sock:newconn}) |
159 | + |
160 | + """ |
161 | + Cleans up a socket that has had its connection closed. |
162 | + |
163 | + :param socket_to_remove: The socket that had its connection closed |
164 | + """ |
165 | + def close_connections(self, socket_to_remove): |
166 | + partner_socket = self.socket_pairs[socket_to_remove.fileno()] |
167 | + |
168 | + self.socket_pairs.pop(socket_to_remove.fileno()) |
169 | + self.socket_pairs.pop(partner_socket.socket.fileno()) |
170 | + |
171 | + self.descriptors.remove(socket_to_remove) |
172 | + self.descriptors.remove(partner_socket) |
173 | + |
174 | + if socket_to_remove in self.host_session_socket_path_map: |
175 | + self.host_session_socket_path_map.pop(socket_to_remove) |
176 | + |
177 | + """ |
178 | + The main loop which uses select to block until one of the sockets we are listening to becomes readable. |
179 | + It is advised this be started in its own process or thread, as this function blocks! |
180 | + """ |
181 | + def main_loop(self): |
182 | + while 1: |
183 | + try: |
184 | + raw_sockets = list(map(lambda x : x.socket, self.descriptors)) |
185 | + rlist, wlist, elist = select.select(raw_sockets, [], []) |
186 | + except InterruptedError: |
187 | + continue |
188 | + except: |
189 | + print("Unexpected error:", sys.exc_info()[0]) |
190 | + break |
191 | + |
192 | + for sock in rlist: |
193 | + if sock.fileno() == -1: |
194 | + continue |
195 | + |
196 | + if sock.fileno() in self.host_session_socket_path_map: |
197 | + self.accept_new_connection(self.host_session_socket_path_map[sock.fileno()], sock) |
198 | + |
199 | + else: |
200 | + data = sock.recv(4096) |
201 | + if len(data) == 0: |
202 | + self.close_connections(sock) |
203 | + continue |
204 | + |
205 | + send_sock = self.socket_pairs[sock.fileno()].socket |
206 | + |
207 | + if send_sock.fileno() < 0: |
208 | + continue |
209 | + |
210 | + totalsent = 0 |
211 | + while totalsent < len(data): |
212 | + sent = send_sock.send(data) |
213 | + |
214 | + if sent == 0: |
215 | + close_connections(sock) |
216 | + break |
217 | + totalsent = totalsent + sent |
218 | + |
219 | + |
220 | class LibertineApplication(object): |
221 | """ |
222 | Launches a libertine container with a session bridge for sockets such as dbus |
223 | |
224 | :param container_id: The container id. |
225 | - "param app_exec_line: The exec line used to start the app in the container. |
226 | + :param app_exec_line: The exec line used to start the app in the container. |
227 | """ |
228 | def __init__(self, container_id, app_exec_line): |
229 | - self.container_id = container_id |
230 | - self.app_exec_line = app_exec_line |
231 | - self.session_bridge = None |
232 | + self.container_id = container_id |
233 | + self.app_exec_line = app_exec_line |
234 | + self.lsb = None |
235 | |
236 | """ |
237 | Launches the libertine session bridge. This creates a proxy socket to read to and from |
238 | @@ -454,14 +642,9 @@ |
239 | :param session_socket_paths: A list of socket paths the session will create. |
240 | """ |
241 | def launch_session_bridge(self, session_socket_paths): |
242 | - session_bridge_arguments = '' |
243 | - for paths in session_socket_paths: |
244 | - session_bridge_arguments += paths + ' ' |
245 | - |
246 | - libertine_session_bridge_cmd = "libertine-session-bridge " + session_bridge_arguments |
247 | - |
248 | - args = shlex.split(libertine_session_bridge_cmd) |
249 | - self.session_bridge = psutil.Popen(args) |
250 | + self.lsb = LibertineSessionBridge(session_socket_paths) |
251 | + self.lsb_process = Process(target=self.lsb.main_loop) |
252 | + self.lsb_process.start() |
253 | |
254 | """ |
255 | Launches the container from the id and attempts to run the application exec. |
256 | @@ -477,5 +660,6 @@ |
257 | except: |
258 | raise |
259 | finally: |
260 | - if self.session_bridge is not None: |
261 | - self.session_bridge.terminate() |
262 | + if self.lsb is not None: |
263 | + self.lsb_process.terminate() |
264 | + self.lsb_process.join() |
265 | |
266 | === modified file 'python/libertine/__init__.py' |
267 | --- python/libertine/__init__.py 2016-06-27 23:15:13 +0000 |
268 | +++ python/libertine/__init__.py 2016-08-05 19:43:28 +0000 |
269 | @@ -22,10 +22,21 @@ |
270 | |
271 | __all__ = [ |
272 | # from Libertine |
273 | - 'LibertineContainer', 'utils', 'LibertineApplication' |
274 | + 'LibertineApplication', |
275 | + 'LibertineContainer', |
276 | + 'LibertineSessionBridge', |
277 | + 'LibertineSessionBridge', |
278 | + 'HostSessionSocketPair', |
279 | + 'SessionSocket', |
280 | + 'Socket', |
281 | + 'utils', |
282 | ] |
283 | |
284 | __docformat__ = "restructuredtext en" |
285 | |
286 | +from libertine.Libertine import LibertineApplication |
287 | from libertine.Libertine import LibertineContainer |
288 | -from libertine.Libertine import LibertineApplication |
289 | +from libertine.Libertine import LibertineSessionBridge |
290 | +from libertine.Libertine import HostSessionSocketPair |
291 | +from libertine.Libertine import SessionSocket |
292 | +from libertine.Libertine import Socket |
293 | |
294 | === modified file 'tests/unit/CMakeLists.txt' |
295 | --- tests/unit/CMakeLists.txt 2016-06-27 23:15:13 +0000 |
296 | +++ tests/unit/CMakeLists.txt 2016-08-05 19:43:28 +0000 |
297 | @@ -51,3 +51,19 @@ |
298 | PROPERTIES |
299 | ENVIRONMENT |
300 | "GI_TYPELIB_PATH=${CMAKE_BINARY_DIR}/liblibertine;LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/liblibertine:${LD_LIBRARY_PATH};CMAKE_BINARY_DIR=${CMAKE_BINARY_DIR};PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR}:${CMAKE_SOURCE_DIR}/python;CMAKE_SOURCE_DIR=${CMAKE_SOURCE_DIR}") |
301 | + |
302 | +add_test(test_libertine_session_bridge |
303 | + "/usr/bin/python3" "-m" "testtools.run" "libertine_session_bridge_tests" |
304 | +) |
305 | +set_tests_properties(test_libertine_session_bridge |
306 | + PROPERTIES |
307 | + ENVIRONMENT |
308 | + "GI_TYPELIB_PATH=${CMAKE_BINARY_DIR}/liblibertine;LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/liblibertine:${LD_LIBRARY_PATH};CMAKE_BINARY_DIR=${CMAKE_BINARY_DIR};PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR}:${CMAKE_SOURCE_DIR}/python;CMAKE_SOURCE_DIR=${CMAKE_SOURCE_DIR}") |
309 | + |
310 | +add_test(test_libertine_socket |
311 | + "/usr/bin/python3" "-m" "testtools.run" "libertine_socket_tests" |
312 | +) |
313 | +set_tests_properties(test_libertine_socket |
314 | + PROPERTIES |
315 | + ENVIRONMENT |
316 | + "GI_TYPELIB_PATH=${CMAKE_BINARY_DIR}/liblibertine;LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/liblibertine:${LD_LIBRARY_PATH};CMAKE_BINARY_DIR=${CMAKE_BINARY_DIR};PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR}:${CMAKE_SOURCE_DIR}/python;CMAKE_SOURCE_DIR=${CMAKE_SOURCE_DIR}") |
317 | |
318 | === added file 'tests/unit/libertine_session_bridge_tests.py' |
319 | --- tests/unit/libertine_session_bridge_tests.py 1970-01-01 00:00:00 +0000 |
320 | +++ tests/unit/libertine_session_bridge_tests.py 2016-08-05 19:43:28 +0000 |
321 | @@ -0,0 +1,101 @@ |
322 | +# Copyright 2016 Canonical Ltd. |
323 | +# |
324 | +# This program is free software: you can redistribute it and/or modify it |
325 | +# under the terms of the GNU General Public License version 3, as published |
326 | +# by the Free Software Foundation. |
327 | +# |
328 | +# This program is distributed in the hope that it will be useful, but |
329 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
330 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
331 | +# PURPOSE. See the GNU General Public License for more details. |
332 | +# |
333 | +# You should have received a copy of the GNU General Public License along |
334 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
335 | + |
336 | +import os |
337 | +import tempfile |
338 | +from socket import * |
339 | + |
340 | +from libertine import LibertineSessionBridge, HostSessionSocketPair, SessionSocket |
341 | +from libertine import Socket |
342 | +from multiprocessing import Process |
343 | + |
344 | +from testtools import TestCase |
345 | +from testtools.matchers import Equals, NotEquals |
346 | + |
347 | +import time |
348 | +import threading |
349 | + |
350 | +class TestLibertineSessionBridge(TestCase): |
351 | + def setUp(self): |
352 | + super(TestLibertineSessionBridge, self).setUp() |
353 | + |
354 | + xdg_runtime_path = tempfile.mkdtemp() |
355 | + |
356 | + # Set necessary enviroment variables |
357 | + os.environ['XDG_RUNTIME_DIR'] = xdg_runtime_path |
358 | + |
359 | + # Create r/w fake temp sockets, the session will be created by the lsb |
360 | + self.host_path = os.path.join(xdg_runtime_path, 'HOST') |
361 | + self.session_path = os.path.join(xdg_runtime_path, 'SESSION') |
362 | + |
363 | + self.host_socket = SessionSocket(self.host_path) |
364 | + self.assertTrue(os.path.exists(self.host_path)) |
365 | + |
366 | + """ |
367 | + Make sure we assert out socket is cleaned up. If we are failing here RAII |
368 | + is broken. |
369 | + """ |
370 | + def cleanup(self): |
371 | + self.assertFalse(os.path.exists(self.session_path)) |
372 | + self.assertFalse(os.path.exists(self.host_path)) |
373 | + shutil.rmtree(os.environ['XDG_RUNTIME_DIR']) |
374 | + |
375 | + """ |
376 | + A function used to just read from the host socket. In a different thread |
377 | + """ |
378 | + def host_read(self, expected_bytes): |
379 | + conn, addr = self.host_socket.socket.accept() |
380 | + data = conn.recv(1024) |
381 | + conn.close() |
382 | + self.assertThat(data, Equals(expected_bytes)) |
383 | + |
384 | + def test_creates_socket_file(self): |
385 | + """ |
386 | + We assert when we create a lsb we create the session socket |
387 | + """ |
388 | + lsb = LibertineSessionBridge([HostSessionSocketPair(self.host_path, self.session_path)]) |
389 | + self.assertTrue(os.path.exists(self.session_path)) |
390 | + |
391 | + |
392 | + def test_data_read_from_host_to_session(self): |
393 | + """ |
394 | + This test shows we are able to proxy data from a host socket to a session client. |
395 | + We do this by: |
396 | + 1) Create a valid host socket |
397 | + 2) Start a thread which waits to asserts out host socket recv the expected data |
398 | + 3) Start the lsb thread to create a proxy session socket |
399 | + 4) We create a fake session client (socket) and connect to the session path |
400 | + 5) We send the expected bytes to the fake session client socket |
401 | + 6) We join on the host sock loop, if we never get the expected bytes in the host socket loop we fail |
402 | + 7) Clean up, and assert all our threads are not alive and have been cleaned up |
403 | + """ |
404 | + expected_bytes = b'Five exclamation marks, the sure sign of an insane mind.' |
405 | + host_sock_loop = threading.Thread(target=self.host_read, args=(expected_bytes,)) |
406 | + host_sock_loop.start() |
407 | + |
408 | + lsb = LibertineSessionBridge([HostSessionSocketPair(self.host_path, self.session_path)]) |
409 | + lsb_process = Process(target=lsb.main_loop) |
410 | + lsb_process.start() |
411 | + |
412 | + fake_session_client = socket(AF_UNIX, SOCK_STREAM) |
413 | + fake_session_client.connect(self.session_path) |
414 | + fake_session_client.sendall(expected_bytes) |
415 | + |
416 | + host_sock_loop.join(timeout=1) |
417 | + fake_session_client.close() |
418 | + lsb_process.terminate() |
419 | + lsb_process.join(timeout=1) |
420 | + |
421 | + self.assertFalse(host_sock_loop.is_alive()) |
422 | + self.assertFalse(lsb_process.is_alive()) |
423 | |
424 | === added file 'tests/unit/libertine_socket_tests.py' |
425 | --- tests/unit/libertine_socket_tests.py 1970-01-01 00:00:00 +0000 |
426 | +++ tests/unit/libertine_socket_tests.py 2016-08-05 19:43:28 +0000 |
427 | @@ -0,0 +1,72 @@ |
428 | +# Copyright 2016 Canonical Ltd. |
429 | +# |
430 | +# This program is free software: you can redistribute it and/or modify it |
431 | +# under the terms of the GNU General Public License version 3, as published |
432 | +# by the Free Software Foundation. |
433 | +# |
434 | +# This program is distributed in the hope that it will be useful, but |
435 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
436 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
437 | +# PURPOSE. See the GNU General Public License for more details. |
438 | +# |
439 | +# You should have received a copy of the GNU General Public License along |
440 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
441 | + |
442 | +from libertine import Socket |
443 | + |
444 | +from socket import socket |
445 | + |
446 | +from testtools import TestCase |
447 | +from testtools.matchers import Equals, NotEquals |
448 | + |
449 | +class FakeSocket(Socket): |
450 | + """ |
451 | + We are making things up here... soo lets not attempt to remove anything! |
452 | + """ |
453 | + def __del__(self): |
454 | + pass |
455 | + |
456 | + |
457 | +class TestSocket(TestCase): |
458 | + def setUp(self): |
459 | + super(TestSocket, self).setUp() |
460 | + self.socket1 = socket() |
461 | + self.socket2 = socket() |
462 | + |
463 | + """ |
464 | + Test we can wrap a python socket.socket in our Socket class |
465 | + """ |
466 | + def test_socket_warp(self): |
467 | + s = FakeSocket(self.socket1) |
468 | + self.assertThat(s, Equals(self.socket1)) |
469 | + |
470 | + """ |
471 | + Test our equivalence operator works on three types: |
472 | + 1: Other Socket classes |
473 | + 2: Python socket classes |
474 | + 3: The fd/socket raw Int value |
475 | + """ |
476 | + def test_socket_eq_op(self): |
477 | + s1 = FakeSocket(self.socket1) |
478 | + s2 = FakeSocket(self.socket1) |
479 | + self.assertThat(s1, Equals(s2)) |
480 | + self.assertThat(s2, Equals(self.socket1)) |
481 | + self.assertThat(s2, Equals(self.socket1.fileno())) |
482 | + |
483 | + """ |
484 | + Test our not equivalence works on the same three types |
485 | + """ |
486 | + def test_socket_not_eq_op(self): |
487 | + s1 = FakeSocket(self.socket1) |
488 | + s2 = FakeSocket(self.socket2) |
489 | + self.assertThat(s1, NotEquals(s2)) |
490 | + self.assertThat(s2, NotEquals(self.socket1)) |
491 | + self.assertThat(s2, NotEquals(self.socket1.fileno())) |
492 | + |
493 | + """ |
494 | + Test our Socket wrapper class is also hashable |
495 | + """ |
496 | + def test_socket_is_hashable(self): |
497 | + s = FakeSocket(self.socket1) |
498 | + dic = {s: 17} |
499 | + self.assertThat(dic[s], Equals(17)) |
500 | |
501 | === modified file 'tools/CMakeLists.txt' |
502 | --- tools/CMakeLists.txt 2016-07-15 18:39:58 +0000 |
503 | +++ tools/CMakeLists.txt 2016-08-05 19:43:28 +0000 |
504 | @@ -1,6 +1,6 @@ |
505 | -install(PROGRAMS libertine-container-manager libertine-launch libertine-session-bridge libertine-lxc-manager libertine-xmir libertine-lxc-setup |
506 | +install(PROGRAMS libertine-container-manager libertine-launch libertine-lxc-manager libertine-xmir libertine-lxc-setup |
507 | DESTINATION ${CMAKE_INSTALL_BINDIR}) |
508 | -install(FILES libertine-launch.1 libertine-container-manager.1 libertine-session-bridge.1 libertine-lxc-manager.1 libertine-xmir.1 |
509 | +install(FILES libertine-launch.1 libertine-container-manager.1 libertine-lxc-manager.1 libertine-xmir.1 |
510 | DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 |
511 | COMPONENT doc) |
512 | install(PROGRAMS update-puritine-containers |
513 | |
514 | === modified file 'tools/libertine-launch' |
515 | --- tools/libertine-launch 2016-07-06 16:05:36 +0000 |
516 | +++ tools/libertine-launch 2016-08-05 19:43:28 +0000 |
517 | @@ -17,13 +17,56 @@ |
518 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
519 | |
520 | import argparse |
521 | +import dbus |
522 | import os |
523 | import random |
524 | import string |
525 | import libertine.utils |
526 | import shlex |
527 | import time |
528 | -from libertine import LibertineApplication |
529 | +from libertine import LibertineApplication, HostSessionSocketPair |
530 | + |
531 | + |
532 | +def get_host_maliit_socket(): |
533 | + address_bus_name = 'org.maliit.server' |
534 | + address_object_path = '/org/maliit/server/address' |
535 | + address_interface = 'org.maliit.Server.Address' |
536 | + address_property = 'address' |
537 | + address = '' |
538 | + |
539 | + try: |
540 | + session_bus = dbus.SessionBus() |
541 | + maliit_object = session_bus.get_object('org.maliit.server', '/org/maliit/server/address') |
542 | + |
543 | + interface = dbus.Interface(maliit_object, dbus.PROPERTIES_IFACE) |
544 | + address = interface.Get('org.maliit.Server.Address', 'address') |
545 | + |
546 | + partition_key = 'unix:abstract=' |
547 | + address = address.split(',')[0] |
548 | + address = address.partition(partition_key)[2] |
549 | + address = "\0%s" % address |
550 | + except: |
551 | + pass |
552 | + |
553 | + return address |
554 | + |
555 | + |
556 | +def get_host_dbus_socket(): |
557 | + host_dbus_socket = '' |
558 | + |
559 | + with open(os.path.join(libertine.utils.get_user_runtime_dir(), 'dbus-session'), 'r') as fd: |
560 | + dbus_session_str = fd.read() |
561 | + |
562 | + fd.close() |
563 | + |
564 | + dbus_session_split = dbus_session_str.rsplit('=', 1) |
565 | + if len(dbus_session_split) > 1: |
566 | + host_dbus_socket = dbus_session_split[1].rstrip('\n') |
567 | + # We need to add a \0 to the start of an abstract socket path to connect to it |
568 | + if dbus_session_str.find('abstract') >= 0: |
569 | + host_dbus_socket = "\0%s" % host_dbus_socket |
570 | + |
571 | + return host_dbus_socket |
572 | |
573 | |
574 | def get_session_socket_path(session_socket_name): |
575 | @@ -85,15 +128,27 @@ |
576 | if e in os.environ: |
577 | del os.environ[e] |
578 | |
579 | - dbus_socket_path = get_dbus_session_socket_path() |
580 | - maliit_socket_path = get_maliit_session_socket_path() |
581 | - |
582 | - la.launch_session_bridge([dbus_socket_path, maliit_socket_path]) |
583 | - |
584 | - set_dbus_env_socket_path(dbus_socket_path) |
585 | - set_maliit_env_socket_path(maliit_socket_path) |
586 | + socket_paths = [] |
587 | + |
588 | + maliit_host_path = get_host_maliit_socket() |
589 | + |
590 | + # Maliit needs to check with the real session dbus, so it needs to go before setting |
591 | + # the DBUS_SESSION_ADDRESS to the session socket path |
592 | + if maliit_host_path: |
593 | + maliit_session_path = get_maliit_session_socket_path() |
594 | + socket_paths.append(HostSessionSocketPair(maliit_host_path, maliit_session_path)) |
595 | + set_maliit_env_socket_path(maliit_session_path) |
596 | + |
597 | + dbus_host_path = get_host_dbus_socket() |
598 | + |
599 | + if dbus_host_path: |
600 | + dbus_session_path = get_dbus_session_socket_path() |
601 | + socket_paths.append(HostSessionSocketPair(dbus_host_path, dbus_session_path)) |
602 | + set_dbus_env_socket_path(dbus_session_path) |
603 | + |
604 | + la.launch_session_bridge(socket_paths) |
605 | |
606 | # should detect the maliit socket, but dont know if its around or not here. |
607 | - detect_session_bridge_socket(dbus_socket_path) |
608 | + detect_session_bridge_socket(dbus_session_path) |
609 | |
610 | la.launch_application() |
611 | |
612 | === removed file 'tools/libertine-session-bridge' |
613 | --- tools/libertine-session-bridge 2016-08-03 20:11:57 +0000 |
614 | +++ tools/libertine-session-bridge 1970-01-01 00:00:00 +0000 |
615 | @@ -1,205 +0,0 @@ |
616 | -#!/usr/bin/python3 |
617 | - |
618 | -import dbus |
619 | -import libertine.utils |
620 | -import os |
621 | -import select |
622 | -import signal |
623 | -import sys |
624 | - |
625 | -from socket import * |
626 | - |
627 | - |
628 | -def accept_new_connection(host_adder, container_sock): |
629 | - newconn = container_sock.accept()[0] |
630 | - descriptors.append(newconn) |
631 | - |
632 | - host_sock = socket(AF_UNIX, SOCK_STREAM) |
633 | - host_sock.connect(host_adder) |
634 | - descriptors.append(host_sock) |
635 | - |
636 | - socket_pairs.append([newconn, host_sock]) |
637 | - |
638 | - |
639 | -def get_socket_pair(socket): |
640 | - for i in range(len(socket_pairs)): |
641 | - if socket in socket_pairs[i]: |
642 | - return socket_pairs[i] |
643 | - |
644 | - |
645 | -def get_socket_partner(socket): |
646 | - socket_pair = get_socket_pair(socket) |
647 | - |
648 | - for i in range(len(socket_pair)): |
649 | - if socket != socket_pair[i]: |
650 | - return socket_pair[i] |
651 | - |
652 | - |
653 | -def close_connections(remove_socket): |
654 | - partner_socket = get_socket_partner(remove_socket) |
655 | - |
656 | - socket_pair = get_socket_pair(remove_socket) |
657 | - socket_pairs.remove(socket_pair) |
658 | - |
659 | - descriptors.remove(remove_socket) |
660 | - remove_socket.shutdown(SHUT_RDWR) |
661 | - remove_socket.close() |
662 | - |
663 | - descriptors.remove(partner_socket) |
664 | - partner_socket.shutdown(SHUT_RDWR) |
665 | - partner_socket.close() |
666 | - |
667 | - |
668 | -def close_all_connections(): |
669 | - for i, j in socket_pairs: |
670 | - i.shutdown(SHUT_RDWR) |
671 | - i.close() |
672 | - j.shutdown(SHUT_RDWR) |
673 | - j.close() |
674 | - |
675 | - |
676 | -def get_host_maliit_socket(): |
677 | - address_bus_name = "org.maliit.server" |
678 | - address_object_path = "/org/maliit/server/address" |
679 | - address_interface = "org.maliit.Server.Address" |
680 | - address_property = "address" |
681 | - |
682 | - session_bus = dbus.SessionBus() |
683 | - maliit_object = session_bus.get_object('org.maliit.server', '/org/maliit/server/address') |
684 | - |
685 | - interface = dbus.Interface(maliit_object, dbus.PROPERTIES_IFACE) |
686 | - address = interface.Get('org.maliit.Server.Address', 'address') |
687 | - |
688 | - partition_key = 'unix:abstract=' |
689 | - address = address.split(',')[0] |
690 | - address = address.partition(partition_key)[2] |
691 | - address = "\0%s" % address |
692 | - |
693 | - return address |
694 | - |
695 | - |
696 | -def get_host_dbus_socket(): |
697 | - host_dbus_socket = '' |
698 | - |
699 | - with open(os.path.join(libertine.utils.get_user_runtime_dir(), 'dbus-session'), 'r') as fd: |
700 | - dbus_session_str = fd.read() |
701 | - |
702 | - fd.close() |
703 | - |
704 | - dbus_session_split = dbus_session_str.rsplit('=', 1) |
705 | - if len(dbus_session_split) > 1: |
706 | - host_dbus_socket = dbus_session_split[1].rstrip('\n') |
707 | - # We need to add a \0 to the start of an abstract socket path to connect to it |
708 | - if dbus_session_str.find('abstract') >= 0: |
709 | - host_dbus_socket = "\0%s" % host_dbus_socket |
710 | - |
711 | - return host_dbus_socket |
712 | - |
713 | - |
714 | -def socket_cleanup(signum, frame): |
715 | - for socket in descriptors: |
716 | - socket.close() |
717 | - |
718 | - close_all_connections() |
719 | - |
720 | - for socket_path in session_socket_paths: |
721 | - os.remove(socket_path) |
722 | - |
723 | - |
724 | -def main_loop(): |
725 | - signal.signal(signal.SIGTERM, socket_cleanup) |
726 | - signal.signal(signal.SIGINT, socket_cleanup) |
727 | - |
728 | - while 1: |
729 | - try: |
730 | - rlist, wlist, elist = select.select(descriptors, [], []) |
731 | - except InterruptedError: |
732 | - continue |
733 | - except: |
734 | - break |
735 | - |
736 | - for sock in rlist: |
737 | - if sock.fileno() == -1: |
738 | - continue |
739 | - |
740 | - if sock in host_session_socket_path_map: |
741 | - accept_new_connection(host_session_socket_path_map[sock], sock) |
742 | - |
743 | - else: |
744 | - try: |
745 | - data = sock.recv(4096) |
746 | - except: |
747 | - close_connections(sock) |
748 | - continue |
749 | - |
750 | - if len(data) == 0: |
751 | - close_connections(sock) |
752 | - continue |
753 | - |
754 | - send_sock = get_socket_partner(sock) |
755 | - |
756 | - if send_sock.fileno() < 0: |
757 | - continue |
758 | - |
759 | - totalsent = 0 |
760 | - while totalsent < len(data): |
761 | - sent = send_sock.send(data) |
762 | - |
763 | - if sent == 0: |
764 | - close_connections(sock) |
765 | - break |
766 | - totalsent = totalsent + sent |
767 | - |
768 | - |
769 | -def create_socket(session_socket_path): |
770 | - try: |
771 | - sock = socket(AF_UNIX, SOCK_STREAM) |
772 | - except: |
773 | - sock = None |
774 | - else: |
775 | - try: |
776 | - sock.bind(session_socket_path) |
777 | - sock.listen(5) |
778 | - except: |
779 | - sock.close() |
780 | - sock = None |
781 | - else: |
782 | - return sock |
783 | - |
784 | - return None |
785 | - |
786 | - |
787 | -def create_container_socket(session_socket_path, get_host_session_path_function): |
788 | - container_session_sock = create_socket(session_socket_path) |
789 | - |
790 | - if container_session_sock is not None: |
791 | - try: |
792 | - host_session_path = get_host_session_path_function() |
793 | - except: |
794 | - container_session_sock.close() |
795 | - container_session_sock = None |
796 | - os.remove(session_socket_path) |
797 | - raise |
798 | - else: |
799 | - host_session_socket_path_map.update({container_session_sock:host_session_path}) |
800 | - |
801 | - session_socket_paths.append(session_socket_path) |
802 | - descriptors.append(container_session_sock) |
803 | - |
804 | - |
805 | -descriptors = [] |
806 | -host_session_socket_path_map = {} |
807 | -session_socket_paths = [] |
808 | - |
809 | -# Required sockets: |
810 | -create_container_socket(sys.argv[1], get_host_dbus_socket) |
811 | - |
812 | -# Optional sockets: |
813 | -try: |
814 | - create_container_socket(sys.argv[2], get_host_maliit_socket) |
815 | -except: |
816 | - pass |
817 | - |
818 | -socket_pairs = [] |
819 | - |
820 | -main_loop() |
821 | |
822 | === removed file 'tools/libertine-session-bridge.1' |
823 | --- tools/libertine-session-bridge.1 2016-04-06 12:31:26 +0000 |
824 | +++ tools/libertine-session-bridge.1 1970-01-01 00:00:00 +0000 |
825 | @@ -1,13 +0,0 @@ |
826 | -.TH libertine-session-bridge "1" "April 2016" "libertine-session-bridge 0.99" "User Commands" |
827 | - |
828 | -.SH NAME |
829 | -libertine-session-bridge \- listen for data from a DBUS session |
830 | - |
831 | -.SH DESCRIPTION |
832 | -usage: libertine\-session-bridge DBUS_SOCKET |
833 | -.PP |
834 | -listen for data from a DBUS session on the given socket path |
835 | -.SS "positional arguments:" |
836 | -.TP |
837 | -DBUS_SOCKET |
838 | -path to DBUS socket |
FAILED: Continuous integration, rev:276 /code.launchpad .net/~brandonts chaefer/ libertine/ lsb-refactor/ +merge/ 300522/ +edit-commit- message
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
https:/ /jenkins. canonical. com/libertine/ job/lp- libertine- ci/69/ /jenkins. canonical. com/libertine/ job/build/ 238 /jenkins. canonical. com/libertine/ job/test- 0-autopkgtest/ label=amd64, release= vivid+overlay, testname= default/ 195 /jenkins. canonical. com/libertine/ job/test- 0-autopkgtest/ label=amd64, release= xenial+ overlay, testname= default/ 195 /jenkins. canonical. com/libertine/ job/test- 0-autopkgtest/ label=amd64, release= yakkety, testname= default/ 195 /jenkins. canonical. com/libertine/ job/test- 0-autopkgtest/ label=i386, release= vivid+overlay, testname= default/ 195 /jenkins. canonical. com/libertine/ job/test- 0-autopkgtest/ label=i386, release= xenial+ overlay, testname= default/ 195 /jenkins. canonical. com/libertine/ job/test- 0-autopkgtest/ label=i386, release= yakkety, testname= default/ 195 /jenkins. canonical. com/libertine/ job/lp- generic- update- mp/181/ console /jenkins. canonical. com/libertine/ job/build- 0-fetch/ 241 /jenkins. canonical. com/libertine/ job/build- 1-sourcepkg/ release= vivid+overlay/ 226 /jenkins. canonical. com/libertine/ job/build- 1-sourcepkg/ release= xenial+ overlay/ 226 /jenkins. canonical. com/libertine/ job/build- 1-sourcepkg/ release= yakkety/ 226 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= vivid+overlay/ 219 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= vivid+overlay/ 219/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= xenial+ overlay/ 219 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= xenial+ overlay/ 219/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= yakkety/ 219 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= yakkety/ 219/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= vivid+overlay/ 219 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= vivid+overlay/ 219/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= xenial+ overlay/ 219 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= xenial+ overlay/ 219/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= yakkety/ 219 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= yakkety/ 219/artifact/ output/ *zip*/output. zip
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to tr...