Merge lp:~libertine-team/libertine/refactor-app-wrapper into lp:libertine
- refactor-app-wrapper
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Christopher Townsend |
Approved revision: | 346 |
Merged at revision: | 320 |
Proposed branch: | lp:~libertine-team/libertine/refactor-app-wrapper |
Merge into: | lp:libertine |
Diff against target: |
2156 lines (+1575/-308) 19 files modified
.bzrignore (+2/-0) debian/control (+2/-0) debian/python3-libertine.install (+1/-0) python/libertine/ChrootContainer.py (+14/-7) python/libertine/Libertine.py (+37/-4) python/libertine/LxcContainer.py (+15/-7) python/libertine/launcher/__init__.py (+28/-0) python/libertine/launcher/config.py (+290/-0) python/libertine/launcher/session.py (+340/-0) python/libertine/launcher/task.py (+72/-0) python/libertine/utils.py (+4/-9) tests/unit/CMakeLists.txt (+10/-55) tests/unit/libertine_public_gir_tests.py (+0/-63) tests/unit/test_launcher.py (+557/-4) tests/unit/test_launcher_with_dbus.py (+103/-0) tests/unit/test_libertine_gir.py (+67/-0) tests/unit/test_logger.py (+16/-20) tests/unit/test_session_bridge.py (+2/-0) tools/libertine-launch (+15/-139) |
To merge this branch: | bzr merge lp:~libertine-team/libertine/refactor-app-wrapper |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Christopher Townsend | Approve | ||
Stephen M. Webb (community) | Approve | ||
Larry Price | Approve | ||
Libertine CI Bot | continuous-integration | Approve | |
Brandon Schaefer (community) | Approve | ||
Review via email: mp+305544@code.launchpad.net |
Commit message
libertine-launch: refactored core components of application session management
Description of the change
Refactor libertine-launch session into various classes to better enable unit testing, and added unit testing.
Adding unit tests required refactoring some unrelated chunkies of code.
A follow-up merge will remove the replaced code.
Libertine CI Bot (libertine-ci-bot) wrote : | # |
Brandon Schaefer (brandontschaefer) wrote : | # |
+1 for skipping that test for now. Its really a huge intergeneration test that should be looked at after we finish refactoring.
1 inline comment
Brandon Schaefer (brandontschaefer) : | # |
- 321. By Stephen M. Webb
-
debian/control: added python3-dbusmock build dep
Libertine CI Bot (libertine-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:321
https:/
Executed test runs:
FAILURE: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
FAILURE: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
FAILURE: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
- 322. By Stephen M. Webb
-
python/
libertine/ launcher/ session. py:_create_ bridge_ listener( ) set listen() buffer to 5
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:322
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:/
Christopher Townsend (townsend) wrote : | # |
Couple of issues I'm seeing thus far.
1. pasted is not running. This is the output from the upstart log for the app:
pasted: UbuntuClientInt
running, and the correct socket is being used and is accessible. The shell may have
rejected the incoming connection, so check its log file
2. Whenever I close an app via the File->Quit operation, I get this crash:
Traceback (most recent call last):
File "/usr/bin/
main()
File "/usr/bin/
session.run()
File "/usr/lib/
handler(key.fd, datum)
File "/usr/lib/
if not self._handle_
File "/usr/lib/
if child.wait():
File "/usr/lib/
(pid, status) = waitpid(
ChildProcessError: [Errno 10] No child processes
- 323. By Stephen M. Webb
-
libertine/
launcher/ config: separated host_environ from session_environ - 324. By Stephen M. Webb
-
libertine_launch: passed cleansed host environ to launch tasks
Libertine CI Bot (libertine-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:324
https:/
Executed test runs:
FAILURE: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
FAILURE: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
- 325. By Stephen M. Webb
-
test/unit/
test_launcher( SessionRunning) : add multiple join attempts for race conditions
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:325
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 : | # |
Cool i can confirm as well that i can no longer get this race. We were able to reproduce it with:
stress --cpu 8 --io 4 --vm 2 --vm-bytes 128M --timeout 10s & make test
Christopher Townsend (townsend) wrote : | # |
Still seeing issues.
Latest traceback(s) when staring up an app:
(terminator:166): GConf-WARNING **: Client failed to connect to the D-BUS daemon:
Failed to connect to socket /tmp/dbus-
Traceback (most recent call last):
File "/usr/bin/
TERMINATOR.
File "/usr/share/
window, terminal = self.new_window()
File "/usr/share/
terminal = maker.make(
File "/usr/share/
output = func(**kwargs)
File "/usr/share/
return(
File "/usr/share/
self.
File "/usr/share/
self.
File "/usr/share/
title_font = pango.FontDescr
File "/usr/share/
'/desktop/
glib.GError: No D-BUS daemon running
Traceback (most recent call last):
File "/usr/bin/
main()
File "/usr/bin/
session.run()
File "/usr/lib/
self.
File "/usr/lib/
self.
File "/usr/lib/
utils.
NameError: name 'window_manager' is not defined
So it seems the DBUS_SESSION_
And obviously the other error.
Larry Price (larryprice) wrote : | # |
tiny diff comments commonly about the year 2106 CE, will attempt to build/test on u8...
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:327
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:/
- 326. By Christopher Townsend
-
Merge lp:libertine.
- 327. By Christopher Townsend
-
Fix unknown 'window_manager' variable.
- 328. By Christopher Townsend
-
Fix typos & unused code per review comments.
Libertine CI Bot (libertine-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:328
https:/
Executed test runs:
FAILURE: https:/
None: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
- 329. By Christopher Townsend
-
Prepend "unix:path=" to env vars.
- 330. By Christopher Townsend
-
Merge lp:libertine.
Libertine CI Bot (libertine-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:330
https:/
Executed test runs:
FAILURE: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
- 331. By Christopher Townsend
-
Fix unit tests due to last merge.
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:331
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:/
- 332. By Christopher Townsend
-
Fix environment update and add missing os import.
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:332
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:/
- 333. By Christopher Townsend
-
Properly add the prefixed '\0' character to abstract socket paths.
- 334. By Christopher Townsend
-
Catch exceptions that can and will occur when using the session DBus bridge and ignore them.
Also unregister fd selectors when we are done with them.
Libertine CI Bot (libertine-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:334
https:/
Executed test runs:
FAILURE: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
- 335. By Christopher Townsend
-
When receiving a SIGCHLD, actually check for the launched app instead of the helper services.
Libertine CI Bot (libertine-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:335
https:/
Executed test runs:
FAILURE: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
Libertine CI Bot (libertine-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:336
https:/
Executed test runs:
FAILURE: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
- 336. By Christopher Townsend
-
Use 'unix:path' instead of 'unix:abstract' in unit tests to make cleaning up mock sockets easier.
Add a unit test for checking correct unix abstract socket path address (with '\0' prefix).
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:336
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:/
- 337. By Christopher Townsend
-
Remove the session socket path upon deletion of the Session object.
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:337
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:/
- 338. By Christopher Townsend
-
Move the session socket address removal to the shutdown method.
Libertine CI Bot (libertine-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:338
https:/
Executed test runs:
FAILURE: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
- 339. By Christopher Townsend
-
Fix typo from last commit.
Libertine CI Bot (libertine-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:339
https:/
Executed test runs:
FAILURE: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
- 340. By Christopher Townsend
-
Fix unit tests to account for socket removal in the Session class's _shutdown method.
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:340
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:/
- 341. By Christopher Townsend
-
Merge lp:libertine.
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:341
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:/
- 342. By Christopher Townsend
-
Use 'with' to open the Session class ContextManager in order to force the _shutdown callback to be called on Vivid.
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:342
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:/
- 343. By Christopher Townsend
-
Need to use 'argparse.
REMAINDER' to capture the whole passed in exec line, otherwise, options passed in with the exec line will be treated like options on the libertine-laucnh exec line and cause failures.
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:343
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:/
- 344. By Christopher Townsend
-
Split out some test arguments to make testing a bit easier.
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:344
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:/
- 345. By Christopher Townsend
-
Add test/unit/.cache to .bzrignore.
- 346. By Christopher Townsend
-
Remove unnecessary import of ContextDecorator.
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:345
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:/
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:346
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 : | # |
Seems good to me, a little slow on starting the first app but I think that's how it's always been. I tested opening a few apps at a time from different containers, copy/paste, and opening apps again after their container should have been closed.
Stephen M. Webb (bregma) : | # |
Christopher Townsend (townsend) wrote : | # |
Ok, approving this as well. Let's get it merged!
Preview Diff
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2016-02-29 18:07:02 +0000 |
3 | +++ .bzrignore 2016-10-25 17:37:59 +0000 |
4 | @@ -1,2 +1,4 @@ |
5 | po/POTFILES.in |
6 | po/Makefile.in.in |
7 | +__pycache__ |
8 | +tests/unit/.cache |
9 | |
10 | === modified file 'debian/control' |
11 | --- debian/control 2016-10-07 21:09:41 +0000 |
12 | +++ debian/control 2016-10-25 17:37:59 +0000 |
13 | @@ -19,11 +19,13 @@ |
14 | pkg-config, |
15 | python3-apt, |
16 | python3-dbus, |
17 | + python3-dbusmock, |
18 | python3-dev, |
19 | python3-distro-info, |
20 | python3-gi, |
21 | python3-lxc, |
22 | python3-psutil, |
23 | + python3-pytest, |
24 | python3-testtools, |
25 | python3-xdg, |
26 | qtdeclarative5-dev |
27 | |
28 | === modified file 'debian/python3-libertine.install' |
29 | --- debian/python3-libertine.install 2016-10-07 21:09:41 +0000 |
30 | +++ debian/python3-libertine.install 2016-10-25 17:37:59 +0000 |
31 | @@ -3,4 +3,5 @@ |
32 | usr/lib/python*/*/libertine/HostInfo.py |
33 | usr/lib/python*/*/libertine/Libertine.py |
34 | usr/lib/python*/*/libertine/__init__.py |
35 | +usr/lib/python*/*/libertine/launcher |
36 | usr/lib/python*/*/libertine/utils.py |
37 | |
38 | === modified file 'python/libertine/ChrootContainer.py' |
39 | --- python/libertine/ChrootContainer.py 2016-09-30 18:55:22 +0000 |
40 | +++ python/libertine/ChrootContainer.py 2016-10-25 17:37:59 +0000 |
41 | @@ -199,26 +199,29 @@ |
42 | |
43 | return proot_cmd |
44 | |
45 | - def launch_application(self, app_exec_line): |
46 | + def start_application(self, app_exec_line, environ): |
47 | # FIXME: Disabling seccomp is a temporary measure until we fully understand why we need |
48 | # it or figure out when we need it. |
49 | - os.environ['PROOT_NO_SECCOMP'] = '1' |
50 | + environ['PROOT_NO_SECCOMP'] = '1' |
51 | |
52 | # Workaround issue where a custom dconf profile is on the machine |
53 | - if 'DCONF_PROFILE' in os.environ: |
54 | - del os.environ['DCONF_PROFILE'] |
55 | + if 'DCONF_PROFILE' in environ: |
56 | + del environ['DCONF_PROFILE'] |
57 | |
58 | proot_cmd = self._build_proot_command() |
59 | |
60 | args = shlex.split(proot_cmd) |
61 | args.extend(utils.setup_window_manager(self.container_id, enable_toolbars=True)) |
62 | - window_manager = psutil.Popen(args) |
63 | + window_manager = psutil.Popen(args, env=environ) |
64 | |
65 | args = shlex.split(proot_cmd) |
66 | args.extend(app_exec_line) |
67 | - psutil.Popen(args).wait() |
68 | + app = psutil.Popen(args, env=environ) |
69 | + return app |
70 | |
71 | + def finish_application(self, app): |
72 | utils.terminate_window_manager(window_manager) |
73 | + app.wait() |
74 | |
75 | def _run_ldconfig(self, verbosity=1): |
76 | if verbosity == 1: |
77 | @@ -227,4 +230,8 @@ |
78 | command_line = self._build_privileged_proot_cmd() + " ldconfig.REAL" |
79 | |
80 | args = shlex.split(command_line) |
81 | - subprocess.Popen(args).wait() |
82 | + app = subprocess.Popen(args) |
83 | + return app |
84 | + |
85 | + def finish_application(self, app): |
86 | + app.wait() |
87 | |
88 | === modified file 'python/libertine/Libertine.py' |
89 | --- python/libertine/Libertine.py 2016-10-13 14:57:33 +0000 |
90 | +++ python/libertine/Libertine.py 2016-10-25 17:37:59 +0000 |
91 | @@ -249,11 +249,14 @@ |
92 | def run_in_container(self, command_string): |
93 | return 0 |
94 | |
95 | - def launch_application(self, app_exec_line): |
96 | + def start_application(self, app_exec_line, environ): |
97 | import subprocess |
98 | |
99 | - cmd = subprocess.Popen(app_exec_line) |
100 | - cmd.wait() |
101 | + app = subprocess.Popen(app_exec_line, env=environ) |
102 | + return app |
103 | + |
104 | + def finish_application(self, app): |
105 | + app.wait() |
106 | |
107 | |
108 | class ContainerRunning(contextlib.ExitStack): |
109 | @@ -381,6 +384,35 @@ |
110 | except RuntimeError as e: |
111 | return handle_runtime_error(e) |
112 | |
113 | + def connect(self): |
114 | + """ |
115 | + Connects to the container in preparation to launch an application. May |
116 | + do something like start up daemons or bind-mount directories, I dunno, |
117 | + it's up to the concrete container class. Maybe it does nothing. |
118 | + """ |
119 | + pass |
120 | + |
121 | + def disconnect(self): |
122 | + """ |
123 | + The inverse of connect() above. |
124 | + """ |
125 | + pass |
126 | + |
127 | + def start_application(self, app_exec_line, environ): |
128 | + """ |
129 | + Launches an application in the container. |
130 | + |
131 | + :param app_exec_line: the application exec line as passed in by |
132 | + ubuntu-app-launch |
133 | + """ |
134 | + return self.container.start_application(app_exec_line, environ) |
135 | + |
136 | + def finish_application(self, app): |
137 | + """ |
138 | + Finishes the currently running application in the container. |
139 | + """ |
140 | + self.container.finish_application(app) |
141 | + |
142 | def launch_application(self, app_exec_line): |
143 | """ |
144 | Launches an application in the container. |
145 | @@ -393,7 +425,8 @@ |
146 | if '/usr/games' not in os.environ['PATH']: |
147 | os.environ['PATH'] = os.environ['PATH'] + ":/usr/games" |
148 | |
149 | - self.container.launch_application(app_exec_line) |
150 | + app = self.start_application(app_exec_line, environ=os.environ) |
151 | + self.finish_application(app) |
152 | else: |
153 | raise RuntimeError("Container with id %s does not exist." % self.container.container_id) |
154 | |
155 | |
156 | === modified file 'python/libertine/LxcContainer.py' |
157 | --- python/libertine/LxcContainer.py 2016-10-07 15:42:59 +0000 |
158 | +++ python/libertine/LxcContainer.py 2016-10-25 17:37:59 +0000 |
159 | @@ -139,6 +139,7 @@ |
160 | self.container = lxc_container(container_id) |
161 | self._set_lxc_log() |
162 | self.lxc_manager_interface = None |
163 | + self.window_manager = None |
164 | |
165 | utils.set_session_dbus_env_var() |
166 | |
167 | @@ -309,11 +310,14 @@ |
168 | # Dump it all to disk |
169 | self.container.save_config() |
170 | |
171 | - def launch_application(self, app_exec_line): |
172 | + def start_application(self, app_exec_line, environ): |
173 | if self.lxc_manager_interface == None: |
174 | print("No interface to libertine-lxc-manager. Failing application launch.") |
175 | return |
176 | |
177 | + os.environ.clear() |
178 | + os.environ.update(environ) |
179 | + |
180 | (result, error) = self.lxc_manager_interface.app_start(self.container_id, self.lxc_log_file) |
181 | |
182 | if not result: |
183 | @@ -321,18 +325,22 @@ |
184 | print("%s" % error) |
185 | return |
186 | |
187 | - window_manager = self.container.attach(lxc.attach_run_command, |
188 | - utils.setup_window_manager(self.container_id)) |
189 | + self.window_manager = self.container.attach(lxc.attach_run_command, |
190 | + utils.setup_window_manager(self.container_id)) |
191 | |
192 | # Setup pulse to work inside the container |
193 | os.environ['PULSE_SERVER'] = utils.get_libertine_lxc_pulse_socket_path() |
194 | |
195 | app_launch_cmd = "sudo -E -u " + os.environ['USER'] + " env PATH=" + os.environ['PATH'] |
196 | cmd = shlex.split(app_launch_cmd) |
197 | - self.container.attach_wait(lxc.attach_run_command, |
198 | - cmd + app_exec_line) |
199 | - |
200 | - utils.terminate_window_manager(psutil.Process(window_manager)) |
201 | + app = self.container.attach(lxc.attach_run_command, |
202 | + cmd + app_exec_line) |
203 | + return psutil.Process(app) |
204 | + |
205 | + def finish_application(self, app): |
206 | + os.waitpid(app.pid, 0) |
207 | + |
208 | + utils.terminate_window_manager(psutil.Process(self.window_manager)) |
209 | |
210 | # Tell libertine-lxc-manager that the app has stopped. |
211 | self.lxc_manager_interface.app_stop(self.container_id) |
212 | |
213 | === added directory 'python/libertine/launcher' |
214 | === added file 'python/libertine/launcher/__init__.py' |
215 | --- python/libertine/launcher/__init__.py 1970-01-01 00:00:00 +0000 |
216 | +++ python/libertine/launcher/__init__.py 2016-10-25 17:37:59 +0000 |
217 | @@ -0,0 +1,28 @@ |
218 | +# Copyright 2016 Canonical Ltd. |
219 | +# |
220 | +# This program is free software: you can redistribute it and/or modify it |
221 | +# under the terms of the GNU General Public License version 3, as published |
222 | +# by the Free Software Foundation. |
223 | +# |
224 | +# This program is distributed in the hope that it will be useful, but |
225 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
226 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
227 | +# PURPOSE. See the GNU General Public License for more details. |
228 | +# |
229 | +# You should have received a copy of the GNU General Public License along |
230 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
231 | + |
232 | +"""Provides the Libertine launcher functionality. |
233 | + |
234 | +All the things used specifically for launching and running an application under |
235 | +a Libertine aegis are in this subpackage. It is the principal guts of the |
236 | +libertine-launch tool and associated test suites. |
237 | + |
238 | +This is the public interface of the Libertine launcher package. |
239 | +""" |
240 | + |
241 | +from .config import Config, SocketBridge |
242 | +from .session import Session, translate_to_real_address |
243 | +from .task import LaunchServiceTask, TaskConfig, TaskType |
244 | + |
245 | +__all__ = ('Config', 'Session') |
246 | |
247 | === added file 'python/libertine/launcher/config.py' |
248 | --- python/libertine/launcher/config.py 1970-01-01 00:00:00 +0000 |
249 | +++ python/libertine/launcher/config.py 2016-10-25 17:37:59 +0000 |
250 | @@ -0,0 +1,290 @@ |
251 | +# Copyright 2016 Canonical Ltd. |
252 | +# |
253 | +# This program is free software: you can redistribute it and/or modify it |
254 | +# under the terms of the GNU General Public License version 3, as published |
255 | +# by the Free Software Foundation. |
256 | +# |
257 | +# This program is distributed in the hope that it will be useful, but |
258 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
259 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
260 | +# PURPOSE. See the GNU General Public License for more details. |
261 | +# |
262 | +# You should have received a copy of the GNU General Public License along |
263 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
264 | + |
265 | +"""Configure the libertine launcher.""" |
266 | + |
267 | +import argparse |
268 | +import dbus |
269 | +import os |
270 | +import random |
271 | +import string |
272 | +import sys |
273 | +from .task import TaskType, TaskConfig |
274 | +from .. import utils |
275 | + |
276 | +import gettext |
277 | +gettext.textdomain('libertine') |
278 | +_ = gettext.gettext |
279 | + |
280 | +log = utils.get_logger() |
281 | + |
282 | + |
283 | +def _generate_unique_id(): |
284 | + """Generate a (hopefully) unique identifier string.""" |
285 | + return ''.join(random.choice(string.ascii_lowercase + string.digits) for i in range(8)) |
286 | + |
287 | + |
288 | +class SocketBridge(object): |
289 | + """Configuration for a single socket bridge entry. |
290 | + |
291 | + This is a 3-tuple of a label, the address of a Unix-domain socket in the |
292 | + host environment, and the address of a Unix-domain socket in the session |
293 | + environment. |
294 | + |
295 | + .. data:: env_var |
296 | + |
297 | + The label of the socket bridge. This is used as the environment variable |
298 | + in the session environment. It should follow the naming rules for |
299 | + environment variables. |
300 | + |
301 | + .. data:: host_address |
302 | + |
303 | + The address of a Unix-domain socket in the host environment. |
304 | + |
305 | + .. data:: session_address |
306 | + |
307 | + The address of a Unix-domain socket in the session environment. |
308 | + |
309 | + """ |
310 | + |
311 | + def __init__(self, env_var, host_address, session_address): |
312 | + """Initialize the socket bridge address pair. |
313 | + |
314 | + :param env_var: The socket bridge label. |
315 | + :param host_address: The host socket address. |
316 | + :param session_address: The session socket address. |
317 | + |
318 | + """ |
319 | + self.env_var = env_var |
320 | + self.host_address = host_address |
321 | + self.session_address = session_address |
322 | + |
323 | + def __repr__(self): |
324 | + """Get a human-readable string representation.""" |
325 | + return '{{{}:{}->{}}}'.format(self.env_var, self.host_address, self.session_address) |
326 | + |
327 | + |
328 | +def _get_maliit_address_from_dbus(): |
329 | + """Query the session D-Bus for the address of the Maliit server. |
330 | + |
331 | + If there is no response from the D-Bus (let's just say there is no maliit |
332 | + server running) None is returned. |
333 | + """ |
334 | + try: |
335 | + address_bus_name = 'org.maliit.server' |
336 | + address_object_path = '/org/maliit/server/address' |
337 | + address_interface = 'org.maliit.Server.Address' |
338 | + address_property = 'address' |
339 | + |
340 | + session_bus = dbus.SessionBus() |
341 | + maliit_object = session_bus.get_object('org.maliit.server', '/org/maliit/server/address') |
342 | + interface = dbus.Interface(maliit_object, dbus.PROPERTIES_IFACE) |
343 | + return interface.Get('org.maliit.Server.Address', 'address') |
344 | + except Exception as ex: |
345 | + log.warning(ex) |
346 | + return None |
347 | + |
348 | + |
349 | +class Config(object): |
350 | + """Configuration for the libertine application launcher. |
351 | + |
352 | + The main configuration items are the container ID and the application |
353 | + command line. Additional configuration items include and environment |
354 | + variables to be passed in to the application session and any sockets to be |
355 | + bridged between the session and the host. |
356 | + |
357 | + The following members can be read from objects of this class. |
358 | + |
359 | + ================== ================== |
360 | + Attribute Description |
361 | + ------------------ ------------------ |
362 | + **id** A unique session identifier. A random string of letters and numbers. |
363 | + **container_id** The ID of the container in which the session will run. |
364 | + **exec_line** The program and arguments to execute. |
365 | + **host_environ** A sanitized dictionary of environment variables to |
366 | + export in host operations. |
367 | + **session_environ** A sanitized, remapped, and extended dictionary of environment variables to |
368 | + export in the session environment. Effectively a copy of the host |
369 | + environment but with some changes. |
370 | + **socket_bridges** A collection of SocketBridge objects to implement. |
371 | + **prelaunch_tasks** A collection of tasks to perform before launching the application. |
372 | + ================== ================== |
373 | + |
374 | + """ |
375 | + |
376 | + def __init__(self, argv=sys.argv[1:]): |
377 | + """ |
378 | + :param argv: Command-line arguments used to configure the launcher. Default is ``sys.argv``. |
379 | + |
380 | + """ |
381 | + log.debug('Config.__init__() begins') |
382 | + arg_parser = argparse.ArgumentParser(description=_('Launch an application natively or in a Libertine container')) |
383 | + arg_parser.add_argument('-i', '--id', |
384 | + help=_('Container identifier when launching containerized apps')) |
385 | + arg_parser.add_argument('-E', '--env', |
386 | + default=[], |
387 | + dest='environ', |
388 | + action='append', |
389 | + help=_('Set an environment variable')) |
390 | + arg_parser.add_argument('app_exec_line', |
391 | + nargs=argparse.REMAINDER, |
392 | + help=_('exec line')) |
393 | + options = arg_parser.parse_args(args=argv) |
394 | + |
395 | + if not options.app_exec_line: |
396 | + arg_parser.error('Must specify an exec line') |
397 | + |
398 | + if options.id: |
399 | + self.container_id = options.id |
400 | + else: |
401 | + self.container_id = None |
402 | + |
403 | + self.id = _generate_unique_id() |
404 | + self.exec_line = options.app_exec_line |
405 | + self.host_environ = self._sanitize_host_environment(options) |
406 | + self.socket_bridges = self._create_socket_bridges() |
407 | + self.prelaunch_tasks = self._add_prelaunch_tasks() |
408 | + self.session_environ = self._generate_session_environment() |
409 | + |
410 | + log.debug('id = "{}"'.format(self.id)) |
411 | + log.debug('container_id = "{}"'.format(self.container_id)) |
412 | + log.debug('exec_line = "{}"'.format(self.exec_line)) |
413 | + log.debug('session_environ = {}'.format(self.session_environ)) |
414 | + for bridge in self.socket_bridges: |
415 | + log.debug('socket bridge: {}'.format(bridge)) |
416 | + log.debug('Config.__init__() ends') |
417 | + |
418 | + def _sanitize_host_environment(self, options): |
419 | + """Create a dictionary of sanitized environment variables. |
420 | + |
421 | + The environment available in the Libertine aegis are effectively those |
422 | + of the launcher, but santized to remove any unwanted or dangerous stuff |
423 | + and with and changes requested on the command line. |
424 | + |
425 | + :param options: A collection of option values including a dictionary |
426 | + named 'environ'. |
427 | + :param type: An argparse namespace. |
428 | + |
429 | + :returns: A sanitized host environment dictionary. |
430 | + """ |
431 | + # inherit from the host |
432 | + environ = os.environ.copy() |
433 | + |
434 | + # remove problematic environment variables |
435 | + for e in ['QT_QPA_PLATFORM', 'LD_LIBRARY_PATH', 'FAKECHROOT_BASE', 'FAKECHROOT_CMD_SUBST']: |
436 | + if e in environ: |
437 | + del environ[e] |
438 | + |
439 | + # add or replace CLI-speicifed variables |
440 | + for e in options.environ: |
441 | + k, v = e.split(sep='=', maxsplit=2) |
442 | + environ[k] = v |
443 | + |
444 | + return environ |
445 | + |
446 | + def _generate_session_environment(self): |
447 | + """Generate the session environment.""" |
448 | + |
449 | + # Start with the host environment. |
450 | + environ = self.host_environ.copy() |
451 | + |
452 | + # Fudge some values. |
453 | + path = environ.get('PATH', '/usr/bin') |
454 | + if '/usr/games' not in path.split(sep=':'): |
455 | + environ['PATH'] = path + os.pathsep + '/usr/games' |
456 | + |
457 | + # Add the remapped socket bridge variables. |
458 | + for bridge in self.socket_bridges: |
459 | + environ[bridge.env_var] = 'unix:path=' + bridge.session_address |
460 | + |
461 | + return environ |
462 | + |
463 | + def _add_prelaunch_tasks(self): |
464 | + """Create a collection of pre-launch tasks.""" |
465 | + tasks = [] |
466 | + tasks.append(TaskConfig(TaskType.LAUNCH_SERVICE, ["pasted"])) |
467 | + return tasks |
468 | + |
469 | + def _create_socket_bridges(self): |
470 | + """Create the required socket bridge configs.""" |
471 | + bridges = [] |
472 | + maliit_host_bridge = self._create_maliit_host_bridge() |
473 | + if maliit_host_bridge: |
474 | + bridges.append(maliit_host_bridge) |
475 | + dbus_host_bridge = self._create_dbus_host_bridge() |
476 | + if dbus_host_bridge: |
477 | + bridges.append(dbus_host_bridge) |
478 | + return bridges |
479 | + |
480 | + def _generate_session_socket_name(self, socket_target): |
481 | + """Generate a socket name for the target. |
482 | + |
483 | + :param target: A string used as a base for the socket address. |
484 | + |
485 | + If there is a seession ID, the session ID is appended to the socket name |
486 | + to make it (hopefully) unique. |
487 | + """ |
488 | + socket_name = os.path.join(utils.get_libertine_runtime_dir(), socket_target) |
489 | + if self.id: |
490 | + socket_name += ('-' + self.id) |
491 | + return socket_name |
492 | + |
493 | + def _get_dbus_host_address(self): |
494 | + """Get (or try to get) the D-Bus server socket address. |
495 | + |
496 | + If the D-Bus server socket address environment variable is set, the |
497 | + value of that environment variable is used, otherwise None is returned. |
498 | + """ |
499 | + return self.host_environ.get('DBUS_SESSION_BUS_ADDRESS', None) |
500 | + |
501 | + def _create_dbus_host_bridge(self): |
502 | + """Create a socket bridge for the D-Bus session. |
503 | + |
504 | + Creates a socket bridge configuration for the D-Bus session, if a socket |
505 | + can be found for the host, otherwise returns None. |
506 | + """ |
507 | + socket_bridge = None |
508 | + server_address = self._get_dbus_host_address() |
509 | + if server_address: |
510 | + socket_bridge = SocketBridge('DBUS_SESSION_BUS_ADDRESS', |
511 | + server_address, |
512 | + self._generate_session_socket_name('dbus')) |
513 | + return socket_bridge |
514 | + |
515 | + def _get_maliit_host_address(self): |
516 | + """Get (or try to get) the Maliit server socket address. |
517 | + |
518 | + If the Maliit server socket address environment variable is set, the |
519 | + value of that environment variable is used, otherwise an attempt is made |
520 | + to query the session D-Bus. If neither works, Maliit is probably not |
521 | + there. |
522 | + """ |
523 | + env_var = self.host_environ.get('MALIIT_SERVER_ADDRESS', None) |
524 | + if env_var: |
525 | + return env_var |
526 | + return _get_maliit_address_from_dbus() |
527 | + |
528 | + def _create_maliit_host_bridge(self): |
529 | + """Create a socket bridge for the Maliit server. |
530 | + |
531 | + Creates a socket bridge configuration for the Maliit server, if a socket |
532 | + can be found for the host, otherwise returns None. |
533 | + """ |
534 | + socket_bridge = None |
535 | + server_address = self._get_maliit_host_address() |
536 | + if server_address: |
537 | + socket_bridge = SocketBridge('MALIIT_SERVER_ADDRESS', |
538 | + server_address, |
539 | + self._generate_session_socket_name('maliit')) |
540 | + return socket_bridge |
541 | |
542 | === added file 'python/libertine/launcher/session.py' |
543 | --- python/libertine/launcher/session.py 1970-01-01 00:00:00 +0000 |
544 | +++ python/libertine/launcher/session.py 2016-10-25 17:37:59 +0000 |
545 | @@ -0,0 +1,340 @@ |
546 | +# Copyright 2016 Canonical Ltd. |
547 | +# |
548 | +# This program is free software: you can redistribute it and/or modify it |
549 | +# under the terms of the GNU General Public License version 3, as published |
550 | +# by the Free Software Foundation. |
551 | +# |
552 | +# This program is distributed in the hope that it will be useful, but |
553 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
554 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
555 | +# PURPOSE. See the GNU General Public License for more details. |
556 | +# |
557 | +# You should have received a copy of the GNU General Public License along |
558 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
559 | + |
560 | +"""High-level interface for starting and running an application in Libertine.""" |
561 | + |
562 | +import os |
563 | +import selectors |
564 | +import signal |
565 | +import struct |
566 | +import sys |
567 | + |
568 | +from .config import Config |
569 | +from .. import utils |
570 | +from contextlib import ExitStack, suppress |
571 | +from psutil import STATUS_ZOMBIE |
572 | +from socket import socket, AF_UNIX, SOCK_STREAM |
573 | +from .task import LaunchServiceTask, TaskType |
574 | + |
575 | + |
576 | +log = utils.get_logger() |
577 | + |
578 | + |
579 | +def translate_to_real_address(abstract_address): |
580 | + """Translate the notional text address to a real UNIX-domain address string. |
581 | + |
582 | + :param abstract_address: The human-readable abstract socket address string. |
583 | + |
584 | + Trims off any suffix starting with a comma and replaces any leading string of |
585 | + 'unix:abstract=' with a zero byte. |
586 | + """ |
587 | + addr = abstract_address.split(',')[0] |
588 | + if addr.startswith('unix:abstract='): |
589 | + return "\0" + ' '.join(addr.split('=')[1:]) |
590 | + elif addr.startswith('unix:path='): |
591 | + return ' '.join(addr.split('=')[1:]) |
592 | + |
593 | + return addr |
594 | + |
595 | + |
596 | +class BridgePair(object): |
597 | + """A pair of sockets that make up a bridge between host and session.""" |
598 | + |
599 | + def __init__(self, session_socket, host_address): |
600 | + """Create a pair of sockets bridging host and session. |
601 | + |
602 | + A socket bridge pair takes an (already-opened) session socket and the |
603 | + address of the host socket and opens a connection to that. |
604 | + """ |
605 | + self.session_socket = session_socket |
606 | + self.session_socket.setblocking(False) |
607 | + |
608 | + self.host_socket = socket(AF_UNIX, SOCK_STREAM) |
609 | + self.host_socket.connect(translate_to_real_address(host_address)) |
610 | + self.host_socket.setblocking(False) |
611 | + |
612 | + def handle_read_fd(self, fd, session): |
613 | + """Handle read-available events on one of the sockets. |
614 | + |
615 | + :param fd: A file descriptor on which a read is available. |
616 | + :type fd: int -- valid file descriptor. |
617 | + :param session: A libertine application session object. |
618 | + :type session: libertine.launcher.Session |
619 | + |
620 | + Callback to handle a read-available event on one of the bridge pair |
621 | + socket fds. |
622 | + """ |
623 | + if fd == self.session_socket.fileno(): |
624 | + if self._copy_data(self.session_socket, self.host_socket) == 0: |
625 | + self._close_up_shop(session) |
626 | + elif fd == self.host_socket.fileno(): |
627 | + if self._copy_data(self.host_socket, self.session_socket) == 0: |
628 | + self._close_up_shop(session) |
629 | + |
630 | + def _copy_data(self, from_socket, to_socket): |
631 | + """Copy data between the sockets. |
632 | + |
633 | + :param from_socket: A socket to be read from. |
634 | + :type from_socket: socket-object |
635 | + :param to_socket: A socket to be written to. |
636 | + :type to_socket: socket-object |
637 | + |
638 | + Note that this chunks the data 4 kilobytes at a time (which maybe should |
639 | + be a tuneable parameter). |
640 | + Also, it's a non-blocking write and that may affect things. Honestly, it |
641 | + should be a non-blocking write and logic should be added to track |
642 | + write-available evnts and unsent bytes and all that stuff but not today. |
643 | + """ |
644 | + try: |
645 | + b = from_socket.recv(4096) |
646 | + if len(b) > 0: |
647 | + to_socket.sendall(b) |
648 | + log.debug('copied {} bytes from fd to {}'.format(len(b), from_socket, to_socket)) |
649 | + else: |
650 | + log.info('close detected on {}'.format(from_socket)) |
651 | + return len(b) |
652 | + except Exception as e: |
653 | + log.exception(e) |
654 | + return 0 |
655 | + |
656 | + def _close_up_shop(self, session): |
657 | + """Clean up. |
658 | + |
659 | + :param session: A libertine application session object. |
660 | + :type session: libertine.launcher.Session |
661 | + |
662 | + Closes both sockets in the pair and calls back the session to remove |
663 | + this object from its watch list. |
664 | + """ |
665 | + session.remove_bridge_pair(self) |
666 | + self.session_socket.close() |
667 | + self.host_socket.close() |
668 | + |
669 | + |
670 | +class Session(ExitStack): |
671 | + """Encapsulation of a running application under a Libertine aegis. |
672 | + |
673 | + A session includes the following. |
674 | + |
675 | + * A cleansed and remapped set of environment variables. |
676 | + * A collection of redirected sockets. |
677 | + * Zero or more running auxiliary programs. |
678 | + * An executable command line. |
679 | + |
680 | + A session must be associated with a single aegis (a container, a snap, or |
681 | + the host itself, depending). The associated aegis is termed the *container* |
682 | + for historical reasons. |
683 | + |
684 | + Each application run under a Libertine aegis must have its own session. |
685 | + |
686 | + A session is constructed in a 'stopped' state. It needs to be started and |
687 | + the main loop entered, and once the main loop exits it gets torn down and |
688 | + returned to a stopped state. |
689 | + """ |
690 | + |
691 | + def __init__(self, config, container): |
692 | + """Construct a libertine application session for a container. |
693 | + |
694 | + :param config: A session configuration object. |
695 | + :param container: The container in which the application will be run. |
696 | + """ |
697 | + super().__init__() |
698 | + self._app = None |
699 | + self._config = config |
700 | + self._container = container |
701 | + self._bridge_pairs = [] |
702 | + self._child_processes = [] |
703 | + self._selector = selectors.DefaultSelector() |
704 | + self._set_signal_handlers() |
705 | + self.callback(self._shutdown) |
706 | + |
707 | + self._ensure_paths_exist() |
708 | + |
709 | + with suppress(AttributeError): |
710 | + for bridge_config in self._config.socket_bridges: |
711 | + self._create_bridge_listener(bridge_config) |
712 | + |
713 | + with suppress(AttributeError): |
714 | + for task_config in self._config.prelaunch_tasks: |
715 | + if task_config.task_type == TaskType.LAUNCH_SERVICE: |
716 | + log.info("launching {}".format(task_config.datum[0])) |
717 | + task = LaunchServiceTask(task_config) |
718 | + self._child_processes.append(task) |
719 | + task.start(self._config.host_environ) |
720 | + |
721 | + @property |
722 | + def id(self): |
723 | + """A unique string identifying this session.""" |
724 | + return self._config.id |
725 | + |
726 | + def _add_read_fd_handler(self, fd, handler, datum): |
727 | + """Add a handler to be called when a read event is received on fd. |
728 | + |
729 | + :param fd: A file descriptor to watch for read events. |
730 | + :type fd: int -- valid file descriptor. |
731 | + :param handler: A function to be called when a read on fd becomes available. |
732 | + :param datum: Data to be passed to handler when called. |
733 | + """ |
734 | + self._selector.register(fd, selectors.EVENT_READ, (handler, datum)) |
735 | + |
736 | + def _remove_read_fd_handler(self, fd): |
737 | + """Remove a handler used for reading events on an fd. |
738 | + |
739 | + :param fd: A file descriptor to be removed from watching read events. |
740 | + """ |
741 | + self._selector.unregister(fd) |
742 | + |
743 | + def add_bridge_pair(self, bridge_pair): |
744 | + """Add a bridge pair to the list of those being monitored. |
745 | + |
746 | + :param bridge_pair: an active BridgePair object to add to the watch list. |
747 | + """ |
748 | + self._add_read_fd_handler(bridge_pair.session_socket.fileno(), |
749 | + bridge_pair.handle_read_fd, |
750 | + self) |
751 | + self._add_read_fd_handler(bridge_pair.host_socket.fileno(), |
752 | + bridge_pair.handle_read_fd, |
753 | + self) |
754 | + self._bridge_pairs.append(bridge_pair) |
755 | + |
756 | + def remove_bridge_pair(self, bridge_pair): |
757 | + """Remove a bridge pair from the list of those being monitored. |
758 | + |
759 | + :param bridge_pair: an active BridgePair object to remove from the watch list. |
760 | + """ |
761 | + self._remove_read_fd_handler(bridge_pair.session_socket.fileno()) |
762 | + self._remove_read_fd_handler(bridge_pair.host_socket.fileno()) |
763 | + self._bridge_pairs.remove(bridge_pair) |
764 | + |
765 | + def run(self): |
766 | + """Run the main event loop of the Session. |
767 | + |
768 | + The main loop monitors the various socket bridges and the contained |
769 | + child process(es) and dispatches events appropriately. |
770 | + |
771 | + The event loop is generally terminated by the receipt of a StopIteration |
772 | + exception. |
773 | + """ |
774 | + with suppress(StopIteration): |
775 | + while True: |
776 | + events = self._selector.select() |
777 | + for key, mask in events: |
778 | + handler, datum = key.data |
779 | + handler(key.fd, datum) |
780 | + self._container.finish_application(self._app) |
781 | + self._stop_services() |
782 | + |
783 | + def start_application(self): |
784 | + """Connect to the container and start the application running.""" |
785 | + self._container.connect() |
786 | + self.callback(self._container.disconnect) |
787 | + self._app = self._container.start_application(self._config.exec_line, |
788 | + self._config.session_environ) |
789 | + |
790 | + def _create_bridge_listener(self, bridge_config): |
791 | + """Create a socket bridge listener for a socket bridge configuration. |
792 | + |
793 | + :param bridge_config: A socket bridge configuration object. |
794 | + :type bridge_config: libertine.launcher.config.SocketBridge |
795 | + |
796 | + The purpose of the listener is to listen on the session socket and |
797 | + create a socket bridge to the host when a connection from the session is |
798 | + made. |
799 | + """ |
800 | + log.debug('creating bridge listener for {} on {}'. |
801 | + format(bridge_config.env_var, bridge_config.session_address)) |
802 | + sock = socket(AF_UNIX, SOCK_STREAM) |
803 | + sock.bind(translate_to_real_address(bridge_config.session_address)) |
804 | + sock.listen(5) |
805 | + self._add_read_fd_handler(sock.fileno(), |
806 | + self._accept_bridge_connection, |
807 | + (bridge_config, sock)) |
808 | + |
809 | + def _accept_bridge_connection(self, fd, datum): |
810 | + """Handle a connection on a session socket. |
811 | + |
812 | + :param fd: A file descriptor with an active connect operation in |
813 | + progress. |
814 | + :type fd: int -- valid file descriptor. |
815 | + :param datum: Data passed to handler. |
816 | + """ |
817 | + (bridge_config, sock) = datum |
818 | + conn = sock.accept() |
819 | + log.debug('connection of session socket {} accepted'.format(bridge_config.session_address)) |
820 | + self.add_bridge_pair(BridgePair(conn[0], bridge_config.host_address)) |
821 | + |
822 | + def _ensure_paths_exist(self): |
823 | + """Ensure the required paths all exist for supporting the session.""" |
824 | + directory = utils.get_libertine_runtime_dir() |
825 | + if not os.path.exists(directory): |
826 | + os.makedirs(directory) |
827 | + |
828 | + def _handle_child_died(self): |
829 | + """Take action when a SIGCHILD has been raised.""" |
830 | + for child in self._child_processes: |
831 | + if child.wait(): |
832 | + return True |
833 | + |
834 | + if self._app == None or self._app.status() == STATUS_ZOMBIE: |
835 | + return True |
836 | + |
837 | + return False |
838 | + |
839 | + def _handle_sig_fd(self, fd, dummy): |
840 | + """Handle read events for captured signals. |
841 | + |
842 | + :param fd: A file descriptor on which signal information will be sent. |
843 | + :type fd: int -- valid file descriptor. |
844 | + :param dummy: A dummy parameter. Required for magic. |
845 | + """ |
846 | + data = os.read(fd, 4) |
847 | + sig = struct.unpack('%uB' % len(data), data) |
848 | + if sig[0] == signal.SIGCHLD: |
849 | + log.info('SIGCHLD received') |
850 | + if self._handle_child_died(): |
851 | + raise StopIteration('launched program exited') |
852 | + elif sig[0] == signal.SIGINT: |
853 | + log.info('SIGINT received') |
854 | + raise StopIteration('keyboard interrupt') |
855 | + elif sig[0] == signal.SIGTERM: |
856 | + log.info('SIGTERM received') |
857 | + raise StopIteration('terminate') |
858 | + else: |
859 | + log.warning('unknown signal {} received'.format(sig[0])) |
860 | + |
861 | + def _set_signal_handlers(self): |
862 | + """Set the signal handlers.""" |
863 | + def noopSignalHandler(*args): |
864 | + pass |
865 | + self._sigchld_handler = signal.signal(signal.SIGCHLD, noopSignalHandler) |
866 | + self._sigint_handler = signal.signal(signal.SIGINT, noopSignalHandler) |
867 | + self._sigterm_handler = signal.signal(signal.SIGTERM, noopSignalHandler) |
868 | + |
869 | + sig_r_fd, sig_w_fd = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC) |
870 | + signal.set_wakeup_fd(sig_w_fd) |
871 | + self._add_read_fd_handler(sig_r_fd, self._handle_sig_fd, None) |
872 | + |
873 | + def _shutdown(self): |
874 | + """Restore the previous state to the world when the Session is torn down.""" |
875 | + signal.signal(signal.SIGCHLD, self._sigchld_handler) |
876 | + signal.signal(signal.SIGINT, self._sigint_handler) |
877 | + signal.signal(signal.SIGTERM, self._sigterm_handler) |
878 | + |
879 | + for bridge_pair in self._config.socket_bridges: |
880 | + os.remove(translate_to_real_address(bridge_pair.session_address)) |
881 | + |
882 | + def _stop_services(self): |
883 | + """Ask any started services to stop.""" |
884 | + for service in self._child_processes: |
885 | + service.stop() |
886 | |
887 | === added file 'python/libertine/launcher/task.py' |
888 | --- python/libertine/launcher/task.py 1970-01-01 00:00:00 +0000 |
889 | +++ python/libertine/launcher/task.py 2016-10-25 17:37:59 +0000 |
890 | @@ -0,0 +1,72 @@ |
891 | +# Copyright 2016 Canonical Ltd. |
892 | +# |
893 | +# This program is free software: you can redistribute it and/or modify it |
894 | +# under the terms of the GNU General Public License version 3, as published |
895 | +# by the Free Software Foundation. |
896 | +# |
897 | +# This program is distributed in the hope that it will be useful, but |
898 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
899 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
900 | +# PURPOSE. See the GNU General Public License for more details. |
901 | +# |
902 | +# You should have received a copy of the GNU General Public License along |
903 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
904 | + |
905 | +"""Pre- and post-launch tasks surrounding the Libertine launch of an application.""" |
906 | + |
907 | + |
908 | +from os import waitpid, WNOHANG |
909 | +from subprocess import Popen |
910 | + |
911 | + |
912 | +class TaskType: |
913 | + """Namespace used for task type enumeration.""" |
914 | + |
915 | + LAUNCH_SERVICE = 1 |
916 | + |
917 | + |
918 | +class TaskConfig(object): |
919 | + """Encapsulation of the configuration of a single launch task.""" |
920 | + |
921 | + def __init__(self, task_type, datum): |
922 | + """ |
923 | + :param task_type: The type of launch task. |
924 | + :tyoe task_type: a TaskType value |
925 | + :param datum: The data associated with the launch task. |
926 | + :type datum: anything -- usually a collection of data |
927 | + """ |
928 | + self.task_type = task_type |
929 | + self.datum = datum |
930 | + |
931 | + |
932 | +class LaunchServiceTask(object): |
933 | + """A task that launches a service.""" |
934 | + |
935 | + def __init__(self, config): |
936 | + """ |
937 | + :param config: The task configuraiton. |
938 | + :type config: TaskConfig |
939 | + |
940 | + The constructor unpacks the service commandline from the config datum. |
941 | + """ |
942 | + self._command_line = config.datum |
943 | + self._process = None |
944 | + |
945 | + def start(self, environ=None): |
946 | + """Start the service. |
947 | + |
948 | + :param env: An alternate environment dictionary. |
949 | + """ |
950 | + self._process = Popen(self._command_line, env=environ) |
951 | + |
952 | + def stop(self): |
953 | + """Shuts the service down. """ |
954 | + self._process.terminate() |
955 | + |
956 | + def wait(self): |
957 | + """Wait for service shutdown to complete. |
958 | + :return: True if the service process was successfully waited for, False |
959 | + otherwise (which implies the service is still running). |
960 | + """ |
961 | + (pid, status) = waitpid(self._process.pid, WNOHANG) |
962 | + return pid == self._process.pid |
963 | |
964 | === modified file 'python/libertine/utils.py' |
965 | --- python/libertine/utils.py 2016-10-19 12:57:34 +0000 |
966 | +++ python/libertine/utils.py 2016-10-25 17:37:59 +0000 |
967 | @@ -30,24 +30,19 @@ |
968 | |
969 | # If someone else sets a handler before this, we wont run this! |
970 | if not logger.hasHandlers(): |
971 | - logger.setLevel(logging.DEBUG) |
972 | - logger.disabled = True |
973 | - |
974 | stream_handler = logging.StreamHandler() |
975 | - stream_handler.setLevel(logging.DEBUG) |
976 | - |
977 | formatter = logging.Formatter('%(filename)s:' |
978 | '%(lineno)d: ' |
979 | '%(levelname)s: ' |
980 | '%(funcName)s():\t' |
981 | '%(message)s') |
982 | - |
983 | stream_handler.setFormatter(formatter) |
984 | logger.addHandler(stream_handler) |
985 | |
986 | - # Only enable the logger if we set this |
987 | - if os.getenv('LIBERTINE_DEBUG') is '1': |
988 | - logger.disabled = False |
989 | + if 'LIBERTINE_DEBUG' in os.environ: |
990 | + logger.setLevel(logging.DEBUG) |
991 | + else: |
992 | + logger.setLevel(logging.WARNING) |
993 | |
994 | return logger |
995 | |
996 | |
997 | === modified file 'tests/unit/CMakeLists.txt' |
998 | --- tests/unit/CMakeLists.txt 2016-08-09 22:37:08 +0000 |
999 | +++ tests/unit/CMakeLists.txt 2016-10-25 17:37:59 +0000 |
1000 | @@ -19,59 +19,14 @@ |
1001 | test_container_config |
1002 | ) |
1003 | |
1004 | -add_test(test_libertine_public_gir |
1005 | - "/usr/bin/python3" "-m" "testtools.run" "libertine_public_gir_tests" |
1006 | -) |
1007 | -set_tests_properties(test_libertine_public_gir |
1008 | - PROPERTIES |
1009 | - ENVIRONMENT |
1010 | - "GI_TYPELIB_PATH=${CMAKE_BINARY_DIR}/liblibertine;LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/liblibertine:${LD_LIBRARY_PATH};CMAKE_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR};PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR}") |
1011 | -set_tests_properties(test_libertine_public_gir |
1012 | - PROPERTIES |
1013 | - WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") |
1014 | - |
1015 | -add_test(test_libertine "/usr/bin/python3" "-m" "testtools.run" "discover" "-s" "${CMAKE_CURRENT_SOURCE_DIR}") |
1016 | +add_test(test_libertine "py.test-3" "${CMAKE_CURRENT_SOURCE_DIR}" "-v" "--junit-xml=test_libertine.xml" "--ignore=${CMAKE_CURRENT_SOURCE_DIR}/test_launcher_with_dbus.py") |
1017 | set_tests_properties(test_libertine |
1018 | - PROPERTIES |
1019 | - ENVIRONMENT |
1020 | - "GI_TYPELIB_PATH=${CMAKE_BINARY_DIR}/liblibertine;LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/liblibertine:${LD_LIBRARY_PATH};PYTHONPATH=${CMAKE_SOURCE_DIR}/python") |
1021 | - |
1022 | -add_test(test_libertine_container_manager |
1023 | - "/usr/bin/python3" "-m" "testtools.run" "libertine_container_manager_tests" |
1024 | -) |
1025 | -set_tests_properties(test_libertine_container_manager |
1026 | - PROPERTIES |
1027 | - ENVIRONMENT |
1028 | - "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}") |
1029 | - |
1030 | -add_test(test_libertine_launch |
1031 | - "/usr/bin/python3" "-m" "testtools.run" "libertine_launch_tests" |
1032 | -) |
1033 | -set_tests_properties(test_libertine_launch |
1034 | - PROPERTIES |
1035 | - ENVIRONMENT |
1036 | - "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}") |
1037 | - |
1038 | -add_test(test_libertine_session_bridge |
1039 | - "/usr/bin/python3" "-m" "testtools.run" "libertine_session_bridge_tests" |
1040 | -) |
1041 | -set_tests_properties(test_libertine_session_bridge |
1042 | - PROPERTIES |
1043 | - ENVIRONMENT |
1044 | - "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}") |
1045 | - |
1046 | -add_test(test_libertine_socket |
1047 | - "/usr/bin/python3" "-m" "testtools.run" "libertine_socket_tests" |
1048 | -) |
1049 | -set_tests_properties(test_libertine_socket |
1050 | - PROPERTIES |
1051 | - ENVIRONMENT |
1052 | - "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}") |
1053 | - |
1054 | -add_test(test_logger |
1055 | - "/usr/bin/python3" "-m" "testtools.run" "libertine_logger_tests" |
1056 | -) |
1057 | -set_tests_properties(test_logger |
1058 | - PROPERTIES |
1059 | - ENVIRONMENT |
1060 | - "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}") |
1061 | + PROPERTIES ENVIRONMENT |
1062 | + "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_SOURCE_DIR}/python;CMAKE_SOURCE_DIR=${CMAKE_SOURCE_DIR};LIBERTINE_DATA_DIR=${CMAKE_CURRENT_SOURCE_DIR}") |
1063 | + |
1064 | +add_test(test_libertine_with_dbus "py.test-3" "${CMAKE_CURRENT_SOURCE_DIR}/test_launcher_with_dbus.py" "-v" "--junit-xml=test_libertine_with_dbus.xml" ) |
1065 | +set_tests_properties(test_libertine_with_dbus |
1066 | + PROPERTIES |
1067 | + ENVIRONMENT |
1068 | + "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_SOURCE_DIR}/python;CMAKE_SOURCE_DIR=${CMAKE_SOURCE_DIR}") |
1069 | + |
1070 | |
1071 | === removed file 'tests/unit/libertine_public_gir_tests.py' |
1072 | --- tests/unit/libertine_public_gir_tests.py 2015-10-08 14:49:00 +0000 |
1073 | +++ tests/unit/libertine_public_gir_tests.py 1970-01-01 00:00:00 +0000 |
1074 | @@ -1,63 +0,0 @@ |
1075 | -# Copyright 2015 Canonical Ltd. |
1076 | -# |
1077 | -# This program is free software: you can redistribute it and/or modify it |
1078 | -# under the terms of the GNU General Public License version 3, as published |
1079 | -# by the Free Software Foundation. |
1080 | -# |
1081 | -# This program is distributed in the hope that it will be useful, but |
1082 | -# WITHOUT ANY WARRANTY; without even the implied warranties of |
1083 | -# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1084 | -# PURPOSE. See the GNU General Public License for more details. |
1085 | -# |
1086 | -# You should have received a copy of the GNU General Public License along |
1087 | -# with this program. If not, see <http://www.gnu.org/licenses/>. |
1088 | - |
1089 | -import os |
1090 | -from testtools import TestCase |
1091 | -from testtools.matchers import Equals |
1092 | -from gi.repository import Libertine |
1093 | - |
1094 | -class TestLibertineGir(TestCase): |
1095 | - |
1096 | - def setUp(self): |
1097 | - super(TestLibertineGir, self).setUp() |
1098 | - self.cmake_source_dir = os.environ['CMAKE_SOURCE_DIR'] |
1099 | - |
1100 | - def test_list_containers(self): |
1101 | - os.environ['XDG_DATA_HOME'] = self.cmake_source_dir + '/libertine-config' |
1102 | - |
1103 | - containers = Libertine.list_containers() |
1104 | - |
1105 | - self.assertThat(containers[0], Equals('wily')) |
1106 | - self.assertThat(containers[1], Equals('wily-2')) |
1107 | - |
1108 | - def test_container_path(self): |
1109 | - container_id = 'wily' |
1110 | - os.environ['XDG_CACHE_HOME'] = self.cmake_source_dir + '/libertine-data' |
1111 | - |
1112 | - container_path = Libertine.container_path(container_id) |
1113 | - self.assertThat(container_path, Equals(self.cmake_source_dir + '/libertine-data/libertine-container/wily/rootfs')) |
1114 | - |
1115 | - def test_container_home_path(self): |
1116 | - container_id = 'wily' |
1117 | - os.environ['XDG_DATA_HOME'] = self.cmake_source_dir + '/libertine-home' |
1118 | - |
1119 | - container_home_path = Libertine.container_home_path(container_id) |
1120 | - self.assertThat(container_home_path, Equals(self.cmake_source_dir + '/libertine-home/libertine-container/user-data/wily')) |
1121 | - |
1122 | - def test_container_name(self): |
1123 | - container_id = 'wily' |
1124 | - os.environ['XDG_DATA_HOME'] = self.cmake_source_dir + '/libertine-config' |
1125 | - |
1126 | - container_name = Libertine.container_name(container_id) |
1127 | - self.assertThat(container_name, Equals("Ubuntu 'Wily Werewolf'")) |
1128 | - |
1129 | - container_id = 'wily-2' |
1130 | - container_name = Libertine.container_name(container_id) |
1131 | - self.assertThat(container_name, Equals("Ubuntu 'Wily Werewolf' (2)")) |
1132 | - |
1133 | - def test_list_apps_for_container(self): |
1134 | - os.environ['XDG_DATA_HOME'] = self.cmake_source_dir + '/libertine-config' |
1135 | - |
1136 | - apps = Libertine.list_apps_for_container('wily') |
1137 | - self.assertThat(len(apps), Equals(0)) |
1138 | |
1139 | === renamed file 'tests/unit/libertine_launch_tests.py' => 'tests/unit/test_launcher.py' |
1140 | --- tests/unit/libertine_launch_tests.py 2016-06-30 20:45:38 +0000 |
1141 | +++ tests/unit/test_launcher.py 2016-10-25 17:37:59 +0000 |
1142 | @@ -1,4 +1,4 @@ |
1143 | -# Copyright 2015 Canonical Ltd. |
1144 | +# Copyright 2015-2016 Canonical Ltd. |
1145 | # |
1146 | # This program is free software: you can redistribute it and/or modify it |
1147 | # under the terms of the GNU General Public License version 3, as published |
1148 | @@ -13,15 +13,34 @@ |
1149 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
1150 | |
1151 | import os |
1152 | +import random |
1153 | import shlex |
1154 | import shutil |
1155 | +import signal |
1156 | +import string |
1157 | import subprocess |
1158 | +import sys |
1159 | import tempfile |
1160 | |
1161 | from libertine import LibertineApplication |
1162 | - |
1163 | -from testtools import TestCase |
1164 | -from testtools.matchers import Equals, NotEquals |
1165 | +from libertine import LibertineContainer |
1166 | +from libertine import launcher |
1167 | + |
1168 | +from contextlib import suppress |
1169 | +from io import StringIO |
1170 | +from selectors import DefaultSelector, EVENT_READ |
1171 | +from socket import socket, AF_UNIX, SOCK_STREAM |
1172 | +from testtools import TestCase, ExpectedException |
1173 | +from testtools.matchers import Equals, Not, Contains, MatchesPredicate |
1174 | +from threading import Thread, Barrier, BrokenBarrierError |
1175 | +from time import sleep |
1176 | +from unittest.mock import call, patch, MagicMock |
1177 | + |
1178 | + |
1179 | +def _generate_unique_string(prefix=''): |
1180 | + """Generates a (hopefully) unique string.""" |
1181 | + return prefix + ''.join(random.choice(string.ascii_lowercase + string.digits) for i in range(8)) |
1182 | + |
1183 | |
1184 | class TestLibertineLaunch(TestCase): |
1185 | def setUp(self): |
1186 | @@ -74,3 +93,537 @@ |
1187 | ''' |
1188 | la = LibertineApplication('test', 'foo') |
1189 | self.assertRaises(FileNotFoundError, la.launch_application) |
1190 | + |
1191 | + |
1192 | +class TestLauncherTaskConfig(TestCase): |
1193 | + """Unit tests for libertine.launcher.task module.""" |
1194 | + |
1195 | + def test_task_config_ctor(self): |
1196 | + """Verify Task constructor sets the required attributes.""" |
1197 | + fake_datum = [self.getUniqueString()] |
1198 | + |
1199 | + task = launcher.TaskConfig(launcher.TaskType.LAUNCH_SERVICE, fake_datum) |
1200 | + |
1201 | + self.assertThat(task.task_type, Equals(launcher.TaskType.LAUNCH_SERVICE)) |
1202 | + self.assertThat(task.datum, Equals(fake_datum)) |
1203 | + |
1204 | + |
1205 | +class TestLauncherConfig(TestCase): |
1206 | + """ |
1207 | + Verifies the defined behaviour of the Libertine Launcher Config class. |
1208 | + """ |
1209 | + |
1210 | + # a standard set of command-line arguments for when we don't care |
1211 | + id_arg = ['-i', 'container-id'] |
1212 | + exec_args = ['exec-line', 'one', 'two', 'three'] |
1213 | + basic_args = id_arg + exec_args |
1214 | + |
1215 | + def test_unqiue_id_generation(self): |
1216 | + """Ensure each unique configuration has a unique identifier.""" |
1217 | + config1 = launcher.Config(TestLauncherConfig.basic_args[:]) |
1218 | + config2 = launcher.Config(TestLauncherConfig.basic_args[:]) |
1219 | + self.assertThat(config1.id, Not(Equals(config2.id))) |
1220 | + |
1221 | + def test_mandatory_args(self): |
1222 | + """Verify that the mandatory CLI args get put where they're supposed to get put.""" |
1223 | + config = launcher.Config(TestLauncherConfig.basic_args[:]) |
1224 | + self.assertEquals(config.exec_line, TestLauncherConfig.exec_args) |
1225 | + |
1226 | + @patch('sys.stderr', new_callable=StringIO) |
1227 | + def test_missing_mandatory_args(self, stderr): |
1228 | + """Make sure missing mandatory CLI args are handled gracefully. |
1229 | + |
1230 | + Gracefully means a usage message is printed and the application exists. |
1231 | + """ |
1232 | + with ExpectedException(SystemExit): |
1233 | + config = launcher.Config(TestLauncherConfig.id_arg) |
1234 | + message = stderr.getvalue().strip() |
1235 | + self.assertThat(message, Contains("usage:")) |
1236 | + |
1237 | + def test_optional_id_arg(self): |
1238 | + """Make sure optional container id is handled.""" |
1239 | + config = launcher.Config(TestLauncherConfig.basic_args[:]) |
1240 | + self.assertEquals(config.container_id, 'container-id') |
1241 | + |
1242 | + def test_missing_optional_id_arg(self): |
1243 | + """Ensure command parsing works fine when container id is left off""" |
1244 | + config = launcher.Config(TestLauncherConfig.exec_args) |
1245 | + self.assertEquals(config.container_id, None) |
1246 | + |
1247 | + def test_one_explicit_environment_arg(self): |
1248 | + """Verify that one -E arg gets put into the environment.""" |
1249 | + argv = ['-EMY_ENV=something'] + TestLauncherConfig.basic_args[:] |
1250 | + config = launcher.Config(argv) |
1251 | + self.assertIn('MY_ENV', config.session_environ) |
1252 | + |
1253 | + def test_two_explicit_environment_args(self): |
1254 | + """Verify that multiple -E args gets put into the environment.""" |
1255 | + argv = ['-EMY_ENV=something', '-E', 'SOME_OTHER_ENV=somethingelse'] + TestLauncherConfig.basic_args[:] |
1256 | + config = launcher.Config(argv) |
1257 | + self.assertEquals(config.session_environ.get('MY_ENV', 'nothing'), 'something') |
1258 | + self.assertEquals(config.session_environ.get('SOME_OTHER_ENV', 'nothing'), 'somethingelse') |
1259 | + |
1260 | + def test_usr_games_is_in_path(self): |
1261 | + """Makes sure that /usr/games is in $PATH. |
1262 | + |
1263 | + This was a problem encountered early on. |
1264 | + """ |
1265 | + with patch.dict('os.environ', {'PATH': '/usr/local/bin:/bin:/usr/bin'}): |
1266 | + self.assertNotIn('/usr/games', os.environ['PATH'].split(sep=':')) |
1267 | + config = launcher.Config(TestLauncherConfig.basic_args[:]) |
1268 | + self.assertIn('/usr/games', config.session_environ['PATH'].split(sep=':')) |
1269 | + |
1270 | + def test_maliit_socket_bridge_from_env(self): |
1271 | + """Make sure the Maliit socket bridge gets configured. |
1272 | + |
1273 | + The Maliit socket address is normally found in an environment variable. |
1274 | + """ |
1275 | + env_key = 'MALIIT_SERVER_ADDRESS' |
1276 | + bogus_host_address = 'unix:abstract=/tmp/maliit-host-socket' |
1277 | + maliit_bridge = None |
1278 | + |
1279 | + with patch.dict('os.environ', {env_key: bogus_host_address}): |
1280 | + config = launcher.Config(TestLauncherConfig.basic_args[:]) |
1281 | + for bridge in config.socket_bridges: |
1282 | + if bridge.env_var == env_key: |
1283 | + maliit_bridge = bridge |
1284 | + |
1285 | + self.assertIsNotNone(maliit_bridge) |
1286 | + self.assertThat(maliit_bridge.host_address, Equals(bogus_host_address)) |
1287 | + self.assertThat(maliit_bridge.session_address, Contains('maliit')) |
1288 | + self.assertThat(config.session_environ.get(env_key, None), Not(Equals(None))) |
1289 | + self.assertThat(config.session_environ.get(env_key, bogus_host_address), |
1290 | + Not(Equals(bogus_host_address))) |
1291 | + |
1292 | + def test_dbus_socket_bridge_from_env(self): |
1293 | + """Make sure the D-Bus socket bridge gets configured. |
1294 | + |
1295 | + The D-Bus socket address is normally found in an environment variable. |
1296 | + """ |
1297 | + env_key = 'DBUS_SESSION_BUS_ADDRESS' |
1298 | + bogus_host_address = 'unix:abstract=/tmp/dbus-host-socket' |
1299 | + dbus_bridge = None |
1300 | + |
1301 | + with patch.dict('os.environ', {env_key: bogus_host_address}): |
1302 | + config = launcher.Config(TestLauncherConfig.basic_args[:]) |
1303 | + for bridge in config.socket_bridges: |
1304 | + if bridge.env_var == env_key: |
1305 | + dbus_bridge = bridge |
1306 | + |
1307 | + self.assertIsNotNone(dbus_bridge) |
1308 | + self.assertThat(dbus_bridge.host_address, Equals(bogus_host_address)) |
1309 | + self.assertThat(dbus_bridge.session_address, Contains('dbus')) |
1310 | + self.assertThat(config.session_environ.get(env_key, None), Not(Equals(None))) |
1311 | + self.assertThat(config.session_environ.get(env_key, bogus_host_address), |
1312 | + Not(Equals(bogus_host_address))) |
1313 | + |
1314 | + def test_default_prelaunch_tasks(self): |
1315 | + """Ensure 'pasted' is in the default pre-launch task list.""" |
1316 | + def pasted_is_in_list(task_list): |
1317 | + for t in task_list: |
1318 | + if t.task_type == launcher.TaskType.LAUNCH_SERVICE and t.datum[0] == "pasted": |
1319 | + return True |
1320 | + return False |
1321 | + |
1322 | + config = launcher.Config(TestLauncherConfig.basic_args[:]) |
1323 | + self.assertThat(config.prelaunch_tasks, |
1324 | + MatchesPredicate(pasted_is_in_list, "pasted is not in task list")) |
1325 | + |
1326 | + |
1327 | +class TestLauncherSession(TestCase): |
1328 | + """ |
1329 | + Verifies the defined bahaviour of a Libertine Lunacher session. |
1330 | + |
1331 | + This set of tests does the alien probe dance on things that can be verified |
1332 | + without actually requiring the launcher event loop be runing -- except for |
1333 | + the basic check that an event loop runs and can be terminated. |
1334 | + """ |
1335 | + |
1336 | + def setUp(self): |
1337 | + """Set up a mock config and a mock container.""" |
1338 | + super().setUp() |
1339 | + |
1340 | + fake_session_socket = 'unix:path=/tmp/garbage' |
1341 | + fake_bridge_config = launcher.SocketBridge('FAKE_SOCKET', 'dummy', fake_session_socket) |
1342 | + self._mock_config = MagicMock(spec=launcher.Config, |
1343 | + socket_bridges=[fake_bridge_config]) |
1344 | + |
1345 | + self._fake_session_address = launcher.translate_to_real_address(fake_session_socket) |
1346 | + |
1347 | + self._mock_container = MagicMock(spec=LibertineContainer) |
1348 | + |
1349 | + def test_basic_ctor(self): |
1350 | + """Just test the basic no-frill construction of a session.""" |
1351 | + self._mock_config.id = 'blah' |
1352 | + session = launcher.Session(self._mock_config, self._mock_container) |
1353 | + self.assertThat(session.id, Equals('blah')) |
1354 | + |
1355 | + def test_signals_are_restored(self): |
1356 | + """Verify that a Session object, when used correctly, cleans up any signal |
1357 | + handlers it has replaced. |
1358 | + |
1359 | + This test just makes sure the test environment is left in a clean state |
1360 | + after testing so that it does not corrupt the results of other unit |
1361 | + tests. |
1362 | + """ |
1363 | + old_sigchld_handler = signal.getsignal(signal.SIGCHLD) |
1364 | + old_sigint_handler = signal.getsignal(signal.SIGINT) |
1365 | + old_sigterm_handler = signal.getsignal(signal.SIGTERM) |
1366 | + |
1367 | + with launcher.Session(self._mock_config, self._mock_container) as session: |
1368 | + pass |
1369 | + |
1370 | + self.assertThat(signal.getsignal(signal.SIGCHLD), Equals(old_sigchld_handler)) |
1371 | + self.assertThat(signal.getsignal(signal.SIGINT), Equals(old_sigint_handler)) |
1372 | + self.assertThat(signal.getsignal(signal.SIGTERM), Equals(old_sigterm_handler)) |
1373 | + |
1374 | + @patch('libertine.launcher.session.socket.listen') |
1375 | + @patch('libertine.launcher.session.socket.bind') |
1376 | + def test_bridge_listener_is_created(self, mock_bind, mock_listen): |
1377 | + """Verify that a socket connection listener is set up for a socket bridge config passed in.""" |
1378 | + with launcher.Session(self._mock_config, self._mock_container) as session: |
1379 | + self.assertThat(mock_bind.call_args, Equals(call(self._fake_session_address))) |
1380 | + self.assertThat(mock_listen.called, Equals(True)) |
1381 | + |
1382 | + def test_sigchld_exits_run(self): |
1383 | + """Verify that the run() method returns on receipt of SIGCHLD. """ |
1384 | + with launcher.Session(self._mock_config, self._mock_container) as session: |
1385 | + def run_session_event_loop(): |
1386 | + session.run() |
1387 | + |
1388 | + session_event_loop = Thread(target=run_session_event_loop) |
1389 | + session_event_loop.start() |
1390 | + |
1391 | + os.kill(os.getpid(), signal.SIGCHLD) |
1392 | + |
1393 | + session_event_loop.join(timeout=0.5) |
1394 | + self.assertThat(session_event_loop.is_alive(), Equals(False)) |
1395 | + |
1396 | + def test_sigint_exits_run(self): |
1397 | + """Verify that the run() method returns on receipt of SIGINT. """ |
1398 | + with launcher.Session(self._mock_config, self._mock_container) as session: |
1399 | + def run_session_event_loop(): |
1400 | + session.run() |
1401 | + |
1402 | + session_event_loop = Thread(target=run_session_event_loop) |
1403 | + session_event_loop.start() |
1404 | + |
1405 | + os.kill(os.getpid(), signal.SIGINT) |
1406 | + |
1407 | + session_event_loop.join(timeout=0.5) |
1408 | + self.assertThat(session_event_loop.is_alive(), Equals(False)) |
1409 | + |
1410 | + def test_sigterm_exits_run(self): |
1411 | + """Verify that the run() method returns on receipt of SIGTERM.""" |
1412 | + with launcher.Session(self._mock_config, self._mock_container) as session: |
1413 | + def run_session_event_loop(): |
1414 | + session.run() |
1415 | + |
1416 | + session_event_loop = Thread(target=run_session_event_loop) |
1417 | + session_event_loop.start() |
1418 | + |
1419 | + os.kill(os.getpid(), signal.SIGTERM) |
1420 | + |
1421 | + session_event_loop.join(timeout=0.5) |
1422 | + self.assertThat(session_event_loop.is_alive(), Equals(False)) |
1423 | + |
1424 | + |
1425 | +class TestLauncherServiceTask(TestCase): |
1426 | + """Verify the expected bahaviour of launch tasks.""" |
1427 | + |
1428 | + def setUp(self): |
1429 | + """Set up an even loop fixture to watch for signals from spawned tasks. """ |
1430 | + super().setUp() |
1431 | + self._sigchld_caught = False |
1432 | + |
1433 | + def noopSignalHandler(*args): |
1434 | + pass |
1435 | + original_sigchld_handler = signal.signal(signal.SIGCHLD, noopSignalHandler) |
1436 | + self.addCleanup(lambda: signal.signal(signal.SIGCHLD, original_sigchld_handler)) |
1437 | + |
1438 | + sig_r_fd, sig_w_fd = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC) |
1439 | + signal.set_wakeup_fd(sig_w_fd) |
1440 | + |
1441 | + pre_loop_barrier = Barrier(2) |
1442 | + self._post_loop_barrier = Barrier(2) |
1443 | + |
1444 | + def loop(): |
1445 | + pre_loop_barrier.wait(1) |
1446 | + selector = DefaultSelector() |
1447 | + selector.register(sig_r_fd, EVENT_READ) |
1448 | + with suppress(StopIteration): |
1449 | + while True: |
1450 | + events = selector.select(timeout=5) |
1451 | + if len(events) == 0: |
1452 | + raise StopIteration |
1453 | + for key, mask in events: |
1454 | + if key.fd == sig_r_fd: |
1455 | + data = os.read(sig_r_fd, 4) |
1456 | + self._sigchld_caught = True |
1457 | + raise StopIteration |
1458 | + self._post_loop_barrier.wait(1) |
1459 | + |
1460 | + test_loop = Thread(target=loop) |
1461 | + self.addCleanup(lambda: test_loop.join(1)) |
1462 | + test_loop.start() |
1463 | + pre_loop_barrier.wait(1) |
1464 | + |
1465 | + def test_service_sends_SIGCHLD_on_exit(self): |
1466 | + """Verify a SIGCHLD signal gets raised when the service exits. |
1467 | + |
1468 | + Runs a fast-exiting 'service' (which should finish on its own in a |
1469 | + reasonable time) and verifies that a SIGCHLD is raised. |
1470 | + """ |
1471 | + config = launcher.TaskConfig(launcher.TaskType.LAUNCH_SERVICE, ("/bin/true")) |
1472 | + task = launcher.LaunchServiceTask(config) |
1473 | + task.start() |
1474 | + self._post_loop_barrier.wait(1) |
1475 | + self.assertThat(self._sigchld_caught, Equals(True)) |
1476 | + |
1477 | + def test_service_stop_exits_program(self): |
1478 | + """Verify the service exists when stopped. |
1479 | + |
1480 | + Runs a long-running service (which should not normally finish on its |
1481 | + own) and verifies that a SIGCHLD is received after calling stop() on the |
1482 | + task. |
1483 | + """ |
1484 | + config = launcher.TaskConfig(launcher.TaskType.LAUNCH_SERVICE, ("/usr/bin/yes")) |
1485 | + task = launcher.LaunchServiceTask(config) |
1486 | + task.start() |
1487 | + sleep(0.05) |
1488 | + task.stop() |
1489 | + self._post_loop_barrier.wait(1) |
1490 | + self.assertThat(self._sigchld_caught, Equals(True)) |
1491 | + |
1492 | + |
1493 | +class SessionEventLoopRunning(Thread): |
1494 | + """Provide a running, stoppable session event loop as a context manager thread.""" |
1495 | + |
1496 | + def __init__(self, session): |
1497 | + super().__init__() |
1498 | + self._session = session |
1499 | + |
1500 | + def __enter__(self): |
1501 | + """Start the session event loop thread running on context entry.""" |
1502 | + self.start() |
1503 | + return self |
1504 | + |
1505 | + def __exit__(self, *exc): |
1506 | + """Shut down the session event loop thread.""" |
1507 | + join_attempt_count = 0 |
1508 | + max_join_attempts = 5 |
1509 | + while self.is_alive() and join_attempt_count < max_join_attempts: |
1510 | + self.stop() |
1511 | + self.join(timeout=0.5) |
1512 | + join_attempt_count += 1 |
1513 | + assert join_attempt_count < max_join_attempts |
1514 | + return False |
1515 | + |
1516 | + def run(self): |
1517 | + with suppress(BrokenBarrierError): |
1518 | + self._session.run() |
1519 | + |
1520 | + def stop(self): |
1521 | + """Stop the session event loop. |
1522 | + |
1523 | + May be called multiple times without harm. |
1524 | + |
1525 | + Has a little nap after raising the TERM signal so the GIL gets |
1526 | + relinquished and the loop thread has a chance to process its signals. |
1527 | + """ |
1528 | + os.kill(os.getpid(), signal.SIGTERM) |
1529 | + sleep(0.0001) |
1530 | + |
1531 | + |
1532 | +class EchoServer(Thread): |
1533 | + """A test fixture providing some kind of service running on the host at a known address.""" |
1534 | + |
1535 | + socket_address = 'unix:path=/tmp/echo-host-' + _generate_unique_string() |
1536 | + |
1537 | + def __init__(self): |
1538 | + super().__init__() |
1539 | + self.daemon = True |
1540 | + |
1541 | + real_socket_address = launcher.translate_to_real_address(EchoServer.socket_address) |
1542 | + |
1543 | + self._socket = socket(AF_UNIX, SOCK_STREAM) |
1544 | + self._socket.bind(real_socket_address) |
1545 | + self._socket.listen(5) |
1546 | + self.start() |
1547 | + |
1548 | + def run(self): |
1549 | + while True: |
1550 | + (s, a) = self._socket.accept() |
1551 | + b = s.recv(512) |
1552 | + r = bytes("echo<<{}>>".format(b), 'ascii') |
1553 | + with suppress(BrokenPipeError): |
1554 | + s.sendall(r) |
1555 | + s.close() |
1556 | + |
1557 | + |
1558 | +class TestLauncherSessionSocketBridge(TestCase): |
1559 | + """Verify the Launcher Session socket bridge functionality.""" |
1560 | + |
1561 | + _fake_session_socket = 'unix:path=/tmp/session-' + _generate_unique_string() |
1562 | + |
1563 | + @patch('test_launcher.LibertineContainer') |
1564 | + def setUp(self, mock_container): |
1565 | + """Construct a test fixture with a single socket bridge configuration.""" |
1566 | + super().setUp() |
1567 | + |
1568 | + real_socket_address = launcher.translate_to_real_address(self._fake_session_socket) |
1569 | + with suppress(OSError): |
1570 | + os.remove(real_socket_address) |
1571 | + |
1572 | + config = MagicMock(spec=launcher.Config, |
1573 | + socket_bridges=[launcher.SocketBridge('FAKE_SOCKET', |
1574 | + host_address=EchoServer.socket_address, |
1575 | + session_address=self._fake_session_socket)]) |
1576 | + self._session = launcher.Session(config, mock_container) |
1577 | + |
1578 | + def test_abstract_socket_is_used(self): |
1579 | + bogus_abstract_path = '/tmp/dbus-host-socket' |
1580 | + bogus_host_address = 'unix:abstract=' + bogus_abstract_path |
1581 | + |
1582 | + bogus_host_socket = launcher.translate_to_real_address(bogus_host_address) |
1583 | + |
1584 | + self.assertThat(bogus_host_socket, Equals('\0' + bogus_abstract_path)) |
1585 | + |
1586 | + def test_connection_acceptance(self): |
1587 | + """Verify that when at attempt is made to connect to a session bridge it is accepted. |
1588 | + |
1589 | + This test has to perform some ear-wiggling and hop-dart behaviour to |
1590 | + make sure it waits until the event loop has called the accept() method |
1591 | + before it tries to check to see if the accept() method has been called |
1592 | + (hence the thread barrier). Also, because the real socket accept |
1593 | + operation does not get performed, the mock accept() call can be |
1594 | + executed several times by the server before we get around to actually |
1595 | + checking up on it and we only care that it's called at least once, hence |
1596 | + the 'called_once' flag. |
1597 | + """ |
1598 | + barrier = Barrier(2, timeout=0.5) |
1599 | + called_once = False |
1600 | + real_session_address = launcher.translate_to_real_address(self._fake_session_socket) |
1601 | + |
1602 | + def fake_accept(*args, called_once=called_once): |
1603 | + if not called_once: |
1604 | + called_once = True |
1605 | + barrier.wait() |
1606 | + sock = socket(AF_UNIX, SOCK_STREAM) |
1607 | + return (sock, 'xyzzy') |
1608 | + |
1609 | + with patch('libertine.launcher.session.socket.accept', side_effect=fake_accept) as mock_accept: |
1610 | + with SessionEventLoopRunning(self._session): |
1611 | + sock = socket(AF_UNIX, SOCK_STREAM) |
1612 | + sock.connect(real_session_address) |
1613 | + with suppress(BrokenBarrierError): |
1614 | + barrier.wait() |
1615 | + self.assertThat(mock_accept.called, Equals(True), "accept() not called") |
1616 | + |
1617 | + def test_bridge_socket_relay(self): |
1618 | + """Test that the whole socket bridge relay functionality works. |
1619 | + |
1620 | + Sure, this is not really a unit test but a black-box functional test. |
1621 | + """ |
1622 | + echo_server = EchoServer() |
1623 | + real_session_address = launcher.translate_to_real_address(self._fake_session_socket) |
1624 | + |
1625 | + with SessionEventLoopRunning(self._session): |
1626 | + sock = socket(AF_UNIX, SOCK_STREAM) |
1627 | + sock.connect(real_session_address) |
1628 | + sock.sendall(bytes('ping', 'ascii')) |
1629 | + response = str(sock.recv(1024), 'ascii') |
1630 | + self.assertThat(response, Contains('ping')) |
1631 | + |
1632 | + |
1633 | +class TestLauncherSessionTask(TestCase): |
1634 | + """Verify how a Session handles Tasks.""" |
1635 | + |
1636 | + @patch('test_launcher.LibertineContainer') |
1637 | + def setUp(self, mock_container): |
1638 | + super().setUp() |
1639 | + |
1640 | + # Monkey-patch the task object created by the LaunchServiceTask class. |
1641 | + # This is a little awkward by it's the way Python works. |
1642 | + p = patch('libertine.launcher.session.LaunchServiceTask') |
1643 | + mock_service_task_class = p.start() |
1644 | + self._mock_service_task = mock_service_task_class.return_value |
1645 | + self._mock_service_task.wait = MagicMock(return_value=True) |
1646 | + self.addCleanup(p.stop) |
1647 | + |
1648 | + fake_datum = [self.getUniqueString()] |
1649 | + task = launcher.TaskConfig(launcher.TaskType.LAUNCH_SERVICE, fake_datum) |
1650 | + |
1651 | + config = MagicMock(spec=launcher.Config, socket_bridges=[], prelaunch_tasks=[task], host_environ={}) |
1652 | + self._session = launcher.Session(config, mock_container) |
1653 | + |
1654 | + def test_session_starts_prelaunch_task(self): |
1655 | + """Test that a session starts a pre-launch task.""" |
1656 | + self.assertThat(self._mock_service_task.start.called, |
1657 | + Equals(True), |
1658 | + message="task.start() not called") |
1659 | + |
1660 | + def test_session_stops_prelaunch_task(self): |
1661 | + """Test that a session starts a pre-launch task.""" |
1662 | + with SessionEventLoopRunning(self._session) as event_loop: |
1663 | + event_loop.stop() |
1664 | + self.assertThat(self._mock_service_task.stop.called, Equals(True)) |
1665 | + |
1666 | + def test_session_handles_dying_prelaunch_task(self): |
1667 | + """Test that a session starts a pre-launch task.""" |
1668 | + with SessionEventLoopRunning(self._session) as event_loop: |
1669 | + os.kill(os.getpid(), signal.SIGCHLD) |
1670 | + event_loop.stop() |
1671 | + self.assertThat(self._mock_service_task.wait.called, Equals(True)) |
1672 | + |
1673 | + |
1674 | +class TestLauncherContainerBehavior(TestCase): |
1675 | + """Verify some expected behaviour when it comes to running the contained application.""" |
1676 | + |
1677 | + def setUp(self): |
1678 | + super().setUp() |
1679 | + |
1680 | + self._mock_config = MagicMock(spec=launcher.Config, |
1681 | + socket_bridges=[], |
1682 | + session_environ={}) |
1683 | + |
1684 | + # Need to fake the ContainersConfig used internally by |
1685 | + # LibertineContainer... that whole outfit needs to be refactored for |
1686 | + # better testing next. |
1687 | + mock_containers_config = MagicMock() |
1688 | + mock_containers_config.get_container_type = MagicMock(return_value='mock') |
1689 | + |
1690 | + self._container_proxy = LibertineContainer(container_id='xyzzy', |
1691 | + containers_config=mock_containers_config) |
1692 | + |
1693 | + libertine_mock_patcher = patch.object(self._container_proxy, "container", autospec=True) |
1694 | + self._mock_container = libertine_mock_patcher.start() |
1695 | + self.addCleanup(libertine_mock_patcher.stop) |
1696 | + |
1697 | + def test_start_application(self): |
1698 | + """Test the start_application() function of the session API.""" |
1699 | + self._mock_config.exec_line = ["/bin/echo", "sis", "boom", "bah"] |
1700 | + |
1701 | + with launcher.Session(self._mock_config, self._container_proxy) as session: |
1702 | + with SessionEventLoopRunning(session): |
1703 | + session.start_application() |
1704 | + |
1705 | + #self.assertThat(self._mock_container.connect.called, Equals(True)) |
1706 | + #self.assertThat(self._mock_container.disconnect.called, Equals(True)) |
1707 | + self.assertThat(self._mock_container.start_application.called, Equals(True)) |
1708 | + self.assertThat(self._mock_container.start_application.call_args[0][0], |
1709 | + Equals(self._mock_config.exec_line)) |
1710 | + self.assertThat(self._mock_container.finish_application.called, Equals(True)) |
1711 | + |
1712 | + def test_environment_gets_set(self): |
1713 | + """Verify that the configured variables are set in the contained execution environments.""" |
1714 | + fake_value = _generate_unique_string() |
1715 | + fake_var = _generate_unique_string(prefix='V') |
1716 | + self._mock_config.session_environ[fake_var] = fake_value |
1717 | + self._mock_config.exec_line = ["/dev/null"] |
1718 | + |
1719 | + with launcher.Session(self._mock_config, self._container_proxy) as session: |
1720 | + with SessionEventLoopRunning(session): |
1721 | + session.start_application() |
1722 | + |
1723 | + self.assertThat(self._mock_container.start_application.call_args[0][1], Contains(fake_var)) |
1724 | |
1725 | === added file 'tests/unit/test_launcher_with_dbus.py' |
1726 | --- tests/unit/test_launcher_with_dbus.py 1970-01-01 00:00:00 +0000 |
1727 | +++ tests/unit/test_launcher_with_dbus.py 2016-10-25 17:37:59 +0000 |
1728 | @@ -0,0 +1,103 @@ |
1729 | +# Copyright 2016 Canonical Ltd. |
1730 | +# |
1731 | +# This program is free software: you can redistribute it and/or modify it |
1732 | +# under the terms of the GNU General Public License version 3, as published |
1733 | +# by the Free Software Foundation. |
1734 | +# |
1735 | +# This program is distributed in the hope that it will be useful, but |
1736 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
1737 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1738 | +# PURPOSE. See the GNU General Public License for more details. |
1739 | +# |
1740 | +# You should have received a copy of the GNU General Public License along |
1741 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
1742 | + |
1743 | +import dbus |
1744 | +import dbusmock |
1745 | +import os |
1746 | +import subprocess |
1747 | + |
1748 | +from libertine import launcher |
1749 | + |
1750 | +from testtools import TestCase |
1751 | +from testtools.matchers import Equals, Contains |
1752 | +from unittest.mock import patch |
1753 | + |
1754 | + |
1755 | + |
1756 | +class MockDBusServer(object): |
1757 | + """ |
1758 | + A mock object that proxies a mock D-Bus server, for testing things that do |
1759 | + D-Bus operations in the background. |
1760 | + """ |
1761 | + |
1762 | + def __init__(self, maliit_server_address): |
1763 | + dbusmock.DBusTestCase.start_session_bus() |
1764 | + self.maliit_server_address = maliit_server_address |
1765 | + self._dbus_connection = dbusmock.DBusTestCase.get_dbus(system_bus=False) |
1766 | + |
1767 | + def setUp(self): |
1768 | + self._dbus_mock_server = dbusmock.DBusTestCase.spawn_server( |
1769 | + 'org.maliit.server', |
1770 | + '/org/maliit/server/address', |
1771 | + 'org.maliit.Server.Address', |
1772 | + system_bus=False, |
1773 | + stdout=subprocess.PIPE) |
1774 | + self._mock_interface = self._get_mock_interface() |
1775 | + self._mock_interface.AddProperty('', 'address', self.maliit_server_address) |
1776 | + |
1777 | + def cleanUp(self): |
1778 | + self._dbus_mock_server.terminate() |
1779 | + self._dbus_mock_server.wait() |
1780 | + |
1781 | + def getDetails(self): |
1782 | + return { } |
1783 | + |
1784 | + def _get_mock_interface(self): |
1785 | + return dbus.Interface( |
1786 | + self._dbus_connection.get_object( |
1787 | + 'org.maliit.server', |
1788 | + '/org/maliit/server/address', |
1789 | + 'org.maliit.Server.Address'), |
1790 | + dbusmock.MOCK_IFACE) |
1791 | + |
1792 | + |
1793 | +class TestLauncherConfigUsingDBus(TestCase): |
1794 | + """ |
1795 | + Verifies the defined behaviour of the Libertine Launcher Config class with |
1796 | + reference to a D-Bus server. |
1797 | + """ |
1798 | + def setUp(self): |
1799 | + """ |
1800 | + Need to save and restore the os.environ for each test in case they |
1801 | + change things. That's a global that propagates between tests. |
1802 | + """ |
1803 | + super().setUp() |
1804 | + fake_maliit_host_address = 'unix:abstract=/tmp/maliit-host-socket' |
1805 | + self._dbus_server = self.useFixture(MockDBusServer(maliit_server_address=fake_maliit_host_address)) |
1806 | + self._cli = [ self.getUniqueString(), self.getUniqueString() ] |
1807 | + |
1808 | + def test_maliit_socket_bridge_from_dbus(self): |
1809 | + """ |
1810 | + Make sure the Maliit socket bridge gets configured. |
1811 | + |
1812 | + The Maliit service advertizes it's socket on the D-Bus, although the |
1813 | + environment variable, if present, overrides that so to test the D-Bus |
1814 | + advert the environment variable must be clear. |
1815 | + """ |
1816 | + maliit_bridge = None |
1817 | + with patch.dict('os.environ'): |
1818 | + if 'MALIIT_SERVER_ADDRESS' in os.environ: |
1819 | + del os.environ['MALIIT_SERVER_ADDRESS'] |
1820 | + |
1821 | + config = launcher.Config(self._cli[:]) |
1822 | + |
1823 | + for bridge in config.socket_bridges: |
1824 | + if bridge.env_var == 'MALIIT_SERVER_ADDRESS': |
1825 | + maliit_bridge = bridge |
1826 | + |
1827 | + self.assertIsNotNone(maliit_bridge) |
1828 | + self.assertThat(maliit_bridge.host_address, Equals(self._dbus_server.maliit_server_address)) |
1829 | + self.assertThat(maliit_bridge.session_address, Contains('maliit')) |
1830 | + self.assertThat(maliit_bridge.session_address, Contains(config.id)) |
1831 | + |
1832 | |
1833 | === renamed file 'tests/unit/libertine_container_manager_tests.py' => 'tests/unit/test_libertine_container_manager.py' |
1834 | === added file 'tests/unit/test_libertine_gir.py' |
1835 | --- tests/unit/test_libertine_gir.py 1970-01-01 00:00:00 +0000 |
1836 | +++ tests/unit/test_libertine_gir.py 2016-10-25 17:37:59 +0000 |
1837 | @@ -0,0 +1,67 @@ |
1838 | +# Copyright 2015 Canonical Ltd. |
1839 | +# |
1840 | +# This program is free software: you can redistribute it and/or modify it |
1841 | +# under the terms of the GNU General Public License version 3, as published |
1842 | +# by the Free Software Foundation. |
1843 | +# |
1844 | +# This program is distributed in the hope that it will be useful, but |
1845 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
1846 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1847 | +# PURPOSE. See the GNU General Public License for more details. |
1848 | +# |
1849 | +# You should have received a copy of the GNU General Public License along |
1850 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
1851 | + |
1852 | +import os |
1853 | +from testtools import TestCase |
1854 | +from testtools.matchers import Equals |
1855 | +from gi.repository import Libertine |
1856 | +from unittest import skip |
1857 | +from unittest.mock import patch |
1858 | + |
1859 | + |
1860 | +class TestLibertineGir(TestCase): |
1861 | + |
1862 | + def setUp(self): |
1863 | + super(TestLibertineGir, self).setUp() |
1864 | + self.cmake_source_dir = os.environ['LIBERTINE_DATA_DIR'] |
1865 | + |
1866 | + def test_list_containers(self): |
1867 | + with patch.dict('os.environ', {'XDG_DATA_HOME': self.cmake_source_dir + '/libertine-config'}): |
1868 | + containers = Libertine.list_containers() |
1869 | + |
1870 | + self.assertThat(containers[0], Equals('wily')) |
1871 | + self.assertThat(containers[1], Equals('wily-2')) |
1872 | + |
1873 | + @skip("need to work around cached globals in glib") |
1874 | + def test_container_path(self): |
1875 | + container_id = 'wily' |
1876 | + with patch.dict('os.environ', {'XDG_CACHE_HOME': self.cmake_source_dir + '/libertine-data'}): |
1877 | + container_path = Libertine.container_path(container_id) |
1878 | + |
1879 | + self.assertThat(container_path, Equals(self.cmake_source_dir + '/libertine-data/libertine-container/wily/rootfs')) |
1880 | + |
1881 | + def test_container_home_path(self): |
1882 | + container_id = 'wily' |
1883 | + with patch.dict('os.environ', {'XDG_DATA_HOME': self.cmake_source_dir + '/libertine-home'}): |
1884 | + container_home_path = Libertine.container_home_path(container_id) |
1885 | + |
1886 | + self.assertThat(container_home_path, Equals(self.cmake_source_dir + '/libertine-home/libertine-container/user-data/wily')) |
1887 | + |
1888 | + def test_container_name(self): |
1889 | + container_id = 'wily' |
1890 | + with patch.dict('os.environ', {'XDG_DATA_HOME': self.cmake_source_dir + '/libertine-config'}): |
1891 | + container_name = Libertine.container_name(container_id) |
1892 | + |
1893 | + self.assertThat(container_name, Equals("Ubuntu 'Wily Werewolf'")) |
1894 | + |
1895 | + container_id = 'wily-2' |
1896 | + container_name = Libertine.container_name(container_id) |
1897 | + |
1898 | + self.assertThat(container_name, Equals("Ubuntu 'Wily Werewolf' (2)")) |
1899 | + |
1900 | + def test_list_apps_for_container(self): |
1901 | + with patch.dict('os.environ', {'XDG_DATA_HOME': self.cmake_source_dir + '/libertine-config'}): |
1902 | + apps = Libertine.list_apps_for_container('wily') |
1903 | + |
1904 | + self.assertThat(len(apps), Equals(0)) |
1905 | |
1906 | === renamed file 'tests/unit/libertine_logger_tests.py' => 'tests/unit/test_logger.py' |
1907 | --- tests/unit/libertine_logger_tests.py 2016-08-09 23:34:22 +0000 |
1908 | +++ tests/unit/test_logger.py 2016-10-25 17:37:59 +0000 |
1909 | @@ -12,38 +12,34 @@ |
1910 | # You should have received a copy of the GNU General Public License along |
1911 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
1912 | |
1913 | +import logging |
1914 | import os |
1915 | -import shutil |
1916 | |
1917 | import libertine.utils |
1918 | |
1919 | from testtools import TestCase |
1920 | from testtools.matchers import Equals, NotEquals |
1921 | +from unittest.mock import patch |
1922 | + |
1923 | |
1924 | class TestLogger(TestCase): |
1925 | - def setUp(self): |
1926 | - super(TestLogger, self).setUp() |
1927 | - os.environ['LIBERTINE_DEBUG'] = '1' |
1928 | - self.addCleanup(self.cleanup) |
1929 | - |
1930 | - def cleanup(self): |
1931 | - if os.getenv('LIBERTINE_DEBUG', None) is not None: |
1932 | - del os.environ['LIBERTINE_DEBUG'] |
1933 | |
1934 | def test_logger_off_by_default(self): |
1935 | - # Need to turn off for this test only! |
1936 | - self.cleanup() |
1937 | - os.unsetenv('LIBERTINE_DEBUG') |
1938 | - l = libertine.utils.get_logger() |
1939 | - self.assertTrue(l.disabled) |
1940 | + with patch.dict('os.environ'): |
1941 | + if 'LIBERTINE_DEBUG' in os.environ: |
1942 | + del os.environ['LIBERTINE_DEBUG'] |
1943 | + l = libertine.utils.get_logger() |
1944 | + self.assertThat(l.getEffectiveLevel(), Equals(logging.WARNING)) |
1945 | |
1946 | def test_logger_on_with_env_var(self): |
1947 | - l = libertine.utils.get_logger() |
1948 | - self.assertFalse(l.disabled) |
1949 | + with patch.dict('os.environ', {'LIBERTINE_DEBUG': '1'}): |
1950 | + l = libertine.utils.get_logger() |
1951 | + self.assertThat(l.getEffectiveLevel(), Equals(logging.DEBUG)) |
1952 | |
1953 | def test_logger_only_inits_once(self): |
1954 | - l1 = libertine.utils.get_logger() |
1955 | - l2 = libertine.utils.get_logger() |
1956 | - l3 = libertine.utils.get_logger() |
1957 | - self.assertThat(len(l3.handlers), Equals(1)) |
1958 | + with patch.dict('os.environ', {'LIBERTINE_DEBUG': '1'}): |
1959 | + l1 = libertine.utils.get_logger() |
1960 | + l2 = libertine.utils.get_logger() |
1961 | + l3 = libertine.utils.get_logger() |
1962 | + self.assertThat(len(l3.handlers), Equals(1)) |
1963 | |
1964 | |
1965 | === renamed file 'tests/unit/libertine_session_bridge_tests.py' => 'tests/unit/test_session_bridge.py' |
1966 | --- tests/unit/libertine_session_bridge_tests.py 2016-08-09 18:18:14 +0000 |
1967 | +++ tests/unit/test_session_bridge.py 2016-10-25 17:37:59 +0000 |
1968 | @@ -23,6 +23,7 @@ |
1969 | |
1970 | from testtools import TestCase |
1971 | from testtools.matchers import Equals, NotEquals |
1972 | +from unittest import skip |
1973 | |
1974 | import time |
1975 | import threading |
1976 | @@ -73,6 +74,7 @@ |
1977 | self.assertTrue(os.path.exists(self.session_path)) |
1978 | |
1979 | |
1980 | + @skip("to be replaced by launcher.Session tests") |
1981 | def test_data_read_from_host_to_session(self): |
1982 | """ |
1983 | This test shows we are able to proxy data from a host socket to a session client. |
1984 | |
1985 | === renamed file 'tests/unit/libertine_socket_tests.py' => 'tests/unit/test_sockets.py' |
1986 | === modified file 'tools/libertine-launch' |
1987 | --- tools/libertine-launch 2016-10-25 15:24:08 +0000 |
1988 | +++ tools/libertine-launch 2016-10-25 17:37:59 +0000 |
1989 | @@ -1,7 +1,7 @@ |
1990 | #!/usr/bin/python3 |
1991 | # -*- coding: utf-8 -*- |
1992 | |
1993 | -# Copyright (C) 2015 Canonical Ltd. |
1994 | +# Copyright (C) 2015-2016 Canonical Ltd. |
1995 | # Author: Christopher Townsend <christopher.townsend@canonical.com> |
1996 | |
1997 | # This program is free software: you can redistribute it and/or modify |
1998 | @@ -16,144 +16,20 @@ |
1999 | # You should have received a copy of the GNU General Public License |
2000 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
2001 | |
2002 | -import argparse |
2003 | -import dbus |
2004 | import os |
2005 | -import random |
2006 | -import string |
2007 | -import libertine.utils |
2008 | -import shlex |
2009 | -import time |
2010 | -from libertine import LibertineApplication, HostSessionSocketPair |
2011 | - |
2012 | - |
2013 | -def get_host_maliit_socket(): |
2014 | - address_bus_name = 'org.maliit.server' |
2015 | - address_object_path = '/org/maliit/server/address' |
2016 | - address_interface = 'org.maliit.Server.Address' |
2017 | - address_property = 'address' |
2018 | - address = '' |
2019 | - |
2020 | - try: |
2021 | - session_bus = dbus.SessionBus() |
2022 | - maliit_object = session_bus.get_object('org.maliit.server', '/org/maliit/server/address') |
2023 | - |
2024 | - interface = dbus.Interface(maliit_object, dbus.PROPERTIES_IFACE) |
2025 | - address = interface.Get('org.maliit.Server.Address', 'address') |
2026 | - |
2027 | - partition_key = 'unix:abstract=' |
2028 | - address = address.split(',')[0] |
2029 | - address = address.partition(partition_key)[2] |
2030 | - address = "\0%s" % address |
2031 | - except: |
2032 | - pass |
2033 | - |
2034 | - return address |
2035 | - |
2036 | - |
2037 | -def get_host_dbus_socket(): |
2038 | - host_dbus_socket = '' |
2039 | - |
2040 | - with open(os.path.join(libertine.utils.get_user_runtime_dir(), 'dbus-session'), 'r') as fd: |
2041 | - dbus_session_str = fd.read() |
2042 | - |
2043 | - fd.close() |
2044 | - |
2045 | - dbus_session_split = dbus_session_str.rsplit('=', 1) |
2046 | - if len(dbus_session_split) > 1: |
2047 | - host_dbus_socket = dbus_session_split[1].rstrip('\n') |
2048 | - # We need to add a \0 to the start of an abstract socket path to connect to it |
2049 | - if dbus_session_str.find('abstract') >= 0: |
2050 | - host_dbus_socket = "\0%s" % host_dbus_socket |
2051 | - |
2052 | - return host_dbus_socket |
2053 | - |
2054 | - |
2055 | -def get_session_socket_path(session_socket_name): |
2056 | - unique_id = ''.join(random.choice(string.ascii_lowercase + string.digits) for i in range(8)) |
2057 | - |
2058 | - if not os.path.exists(libertine.utils.get_libertine_runtime_dir()): |
2059 | - os.makedirs(libertine.utils.get_libertine_runtime_dir()) |
2060 | - |
2061 | - session_socket_path = ( |
2062 | - os.path.join(libertine.utils.get_libertine_runtime_dir(), session_socket_name + '-' + unique_id)) |
2063 | - |
2064 | - return session_socket_path |
2065 | - |
2066 | - |
2067 | -def get_dbus_session_socket_path(): |
2068 | - return get_session_socket_path('host_dbus_session') |
2069 | - |
2070 | - |
2071 | -def get_maliit_session_socket_path(): |
2072 | - return get_session_socket_path('host_maliit_session') |
2073 | - |
2074 | - |
2075 | -def set_env_socket_path(session_socket_path, session_env): |
2076 | - os.environ[session_env] = "unix:path=" + session_socket_path |
2077 | - |
2078 | - |
2079 | -def set_dbus_env_socket_path(socket_path): |
2080 | - set_env_socket_path(socket_path, 'DBUS_SESSION_BUS_ADDRESS') |
2081 | - |
2082 | - |
2083 | -def set_maliit_env_socket_path(socket_path): |
2084 | - set_env_socket_path(socket_path, 'MALIIT_SERVER_ADDRESS') |
2085 | - |
2086 | - |
2087 | -def detect_session_bridge_socket(session_socket_path): |
2088 | - retries = 0 |
2089 | - |
2090 | - while not os.path.exists(session_socket_path): |
2091 | - if retries >= 10: |
2092 | - raise RuntimeError("Timeout waiting for Libertine Dbus session bridge socket") |
2093 | - |
2094 | - print("Libertine Dbus session bridge socket is not ready. Waiting...") |
2095 | - retries += 1 |
2096 | - time.sleep(.5) |
2097 | - |
2098 | + |
2099 | +from libertine import launcher, LibertineContainer |
2100 | + |
2101 | +def main(): |
2102 | + config = launcher.Config() |
2103 | + |
2104 | + if config.container_id: |
2105 | + container = LibertineContainer(container_id=config.container_id) |
2106 | + with launcher.Session(config, container) as session: |
2107 | + session.start_application() |
2108 | + session.run() |
2109 | + else: |
2110 | + os.execvp(config.exec_line[0], config.exec_line[0:]) |
2111 | |
2112 | if __name__ == '__main__': |
2113 | - arg_parser = argparse.ArgumentParser(description='Launch an application natively or in a Libertine container') |
2114 | - arg_parser.add_argument('-i', '--id', |
2115 | - help=("Container identifier when launching containerized apps")) |
2116 | - arg_parser.add_argument('app_exec_line', nargs=argparse.REMAINDER, |
2117 | - help='exec line') |
2118 | - args = arg_parser.parse_args() |
2119 | - |
2120 | - if args.id: |
2121 | - la = LibertineApplication(args.id, args.app_exec_line) |
2122 | - |
2123 | - # remove problematic environment variables |
2124 | - for e in ['QT_QPA_PLATFORM', 'LD_LIBRARY_PATH', 'FAKECHROOT_BASE', 'FAKECHROOT_CMD_SUBST']: |
2125 | - if e in os.environ: |
2126 | - del os.environ[e] |
2127 | - |
2128 | - socket_paths = [] |
2129 | - |
2130 | - maliit_host_path = get_host_maliit_socket() |
2131 | - |
2132 | - # Maliit needs to check with the real session dbus, so it needs to go before setting |
2133 | - # the DBUS_SESSION_ADDRESS to the session socket path |
2134 | - if maliit_host_path: |
2135 | - maliit_session_path = get_maliit_session_socket_path() |
2136 | - socket_paths.append(HostSessionSocketPair(maliit_host_path, maliit_session_path)) |
2137 | - set_maliit_env_socket_path(maliit_session_path) |
2138 | - |
2139 | - dbus_host_path = get_host_dbus_socket() |
2140 | - |
2141 | - if dbus_host_path: |
2142 | - dbus_session_path = get_dbus_session_socket_path() |
2143 | - socket_paths.append(HostSessionSocketPair(dbus_host_path, dbus_session_path)) |
2144 | - set_dbus_env_socket_path(dbus_session_path) |
2145 | - |
2146 | - la.launch_session_bridge(socket_paths) |
2147 | - |
2148 | - # should detect the maliit socket, but dont know if its around or not here. |
2149 | - detect_session_bridge_socket(dbus_session_path) |
2150 | - |
2151 | - la.launch_pasted() |
2152 | - |
2153 | - la.launch_application() |
2154 | - else: |
2155 | - os.execvp(args.app_exec_line[0], args.app_exec_line[0:]) |
2156 | + main() |
FAILED: Continuous integration, rev:320 /jenkins. canonical. com/libertine/ job/lp- libertine- ci/135/ /jenkins. canonical. com/libertine/ job/build/ 362/console /jenkins. canonical. com/libertine/ job/lp- generic- update- mp/276/ console /jenkins. canonical. com/libertine/ job/build- 0-fetch/ 364 /jenkins. canonical. com/libertine/ job/build- 1-sourcepkg/ release= vivid+overlay/ 347 /jenkins. canonical. com/libertine/ job/build- 1-sourcepkg/ release= xenial+ overlay/ 347 /jenkins. canonical. com/libertine/ job/build- 1-sourcepkg/ release= yakkety/ 347 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= vivid+overlay/ 346/console /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= xenial+ overlay/ 346/console /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= yakkety/ 346/console /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= vivid+overlay/ 346/console /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= xenial+ overlay/ 346/console /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= yakkety/ 346/console
https:/
Executed test runs:
FAILURE: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild: /jenkins. canonical. com/libertine/ job/lp- libertine- ci/135/ rebuild
https:/