Merge ~sylvain-pineau/checkbox-ng:revert-remote-api-bump into checkbox-ng:master
- Git
- lp:~sylvain-pineau/checkbox-ng
- revert-remote-api-bump
- Merge into master
Proposed by
Sylvain Pineau
Status: | Merged |
---|---|
Approved by: | Sylvain Pineau |
Approved revision: | 31d5d7665e41a275e0d2070c2b2ad7b6372238c2 |
Merged at revision: | 040e4860e11bf84b9a4764f741833c9b60fe4c4d |
Proposed branch: | ~sylvain-pineau/checkbox-ng:revert-remote-api-bump |
Merge into: | checkbox-ng:master |
Diff against target: |
4516 lines (+1663/-718) 37 files modified
checkbox_ng/config.py (+38/-45) checkbox_ng/launcher/check_config.py (+30/-27) checkbox_ng/launcher/checkbox_cli.py (+2/-0) checkbox_ng/launcher/master.py (+28/-16) checkbox_ng/launcher/stages.py (+69/-88) checkbox_ng/launcher/subcommands.py (+39/-42) dev/null (+0/-130) plainbox/impl/applogic.py (+18/-0) plainbox/impl/commands/__init__.py (+11/-0) plainbox/impl/ctrl.py (+1/-0) plainbox/impl/launcher.py (+258/-0) plainbox/impl/runner.py (+1/-0) plainbox/impl/secure/test_config.py (+608/-0) plainbox/impl/session/assistant.py (+32/-11) plainbox/impl/session/remote_assistant.py (+53/-27) plainbox/impl/test_launcher.py (+136/-0) plainbox/impl/unit/unit.py (+2/-1) plainbox/vendor/rpyc/__init__.py (+1/-1) plainbox/vendor/rpyc/core/async_.py (+10/-11) plainbox/vendor/rpyc/core/brine.py (+43/-10) plainbox/vendor/rpyc/core/consts.py (+0/-3) plainbox/vendor/rpyc/core/netref.py (+38/-30) plainbox/vendor/rpyc/core/protocol.py (+22/-21) plainbox/vendor/rpyc/core/service.py (+6/-8) plainbox/vendor/rpyc/core/stream.py (+3/-4) plainbox/vendor/rpyc/core/vinegar.py (+20/-4) plainbox/vendor/rpyc/lib/__init__.py (+4/-4) plainbox/vendor/rpyc/lib/compat.py (+0/-1) plainbox/vendor/rpyc/utils/authenticators.py (+1/-1) plainbox/vendor/rpyc/utils/classic.py (+13/-19) plainbox/vendor/rpyc/utils/factory.py (+32/-46) plainbox/vendor/rpyc/utils/helpers.py (+5/-5) plainbox/vendor/rpyc/utils/registry.py (+32/-82) plainbox/vendor/rpyc/utils/server.py (+34/-34) plainbox/vendor/rpyc/utils/teleportation.py (+69/-32) plainbox/vendor/rpyc/utils/zerodeploy.py (+2/-13) plainbox/vendor/rpyc/version.py (+2/-2) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Sylvain Pineau (community) | Approve | ||
Maciej Kisielewski (community) | Approve | ||
Review via email: mp+419476@code.launchpad.net |
Commit message
Description of the change
Revert the remote api branch
To post a comment you must log in.
Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote : | # |
self-approved
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/checkbox_ng/config.py b/checkbox_ng/config.py | |||
2 | index a8e8ad0..5a4ba11 100644 | |||
3 | --- a/checkbox_ng/config.py | |||
4 | +++ b/checkbox_ng/config.py | |||
5 | @@ -23,62 +23,55 @@ | |||
6 | 23 | ===================================================== | 23 | ===================================================== |
7 | 24 | """ | 24 | """ |
8 | 25 | import gettext | 25 | import gettext |
9 | 26 | import itertools | ||
10 | 26 | import logging | 27 | import logging |
11 | 27 | import os | 28 | import os |
12 | 28 | 29 | ||
14 | 29 | from plainbox.impl.config import Configuration | 30 | from plainbox.impl.launcher import DefaultLauncherDefinition |
15 | 31 | from plainbox.impl.launcher import LauncherDefinition | ||
16 | 30 | 32 | ||
17 | 31 | 33 | ||
18 | 32 | _ = gettext.gettext | 34 | _ = gettext.gettext |
19 | 33 | 35 | ||
20 | 34 | _logger = logging.getLogger("config") | 36 | _logger = logging.getLogger("config") |
21 | 35 | 37 | ||
22 | 36 | |||
23 | 37 | SEARCH_DIRS = [ | ||
24 | 38 | '$SNAP_DATA', | ||
25 | 39 | '/etc/xdg/', | ||
26 | 40 | '~/.config/', | ||
27 | 41 | ] | ||
28 | 42 | |||
29 | 43 | |||
30 | 44 | def expand_all(path): | 38 | def expand_all(path): |
31 | 45 | """Expand both: envvars and ~ in `path`.""" | ||
32 | 46 | return os.path.expandvars(os.path.expanduser(path)) | 39 | return os.path.expandvars(os.path.expanduser(path)) |
33 | 47 | 40 | ||
53 | 48 | 41 | def load_configs(launcher_file=None): | |
54 | 49 | def load_configs(launcher_file=None, cfg=None): | 42 | # launcher can override the default name of config files to look for |
55 | 50 | """ | 43 | # so first we need to establish the filename to look for |
56 | 51 | Read a chain of configs/launchers. | 44 | configs = [] |
57 | 52 | 45 | config_filename = 'checkbox.conf' | |
58 | 53 | In theory there can be a very long list of configs that are linked by | 46 | launcher = DefaultLauncherDefinition() |
59 | 54 | specifying config_filename in each. Each time this happen we need to | 47 | if launcher_file: |
60 | 55 | consider the new one and override all the values contained therein. | 48 | configs.append(launcher_file) |
61 | 56 | And after this chain is exhausted the values in the launcher should | 49 | generic_launcher = LauncherDefinition() |
62 | 57 | take precedence over the previously read. | 50 | if not os.path.exists(launcher_file): |
63 | 58 | Warning: some recursion ahead. | 51 | _logger.error(_( |
64 | 59 | """ | 52 | "Unable to load launcher '%s'. File not found!"), |
65 | 60 | if not cfg: | 53 | launcher_file) |
66 | 61 | cfg = Configuration() | 54 | raise SystemExit(1) |
67 | 62 | previous_cfg_name = cfg.get_value('config', 'config_filename') | 55 | generic_launcher.read(launcher_file) |
68 | 63 | if os.path.isabs(expand_all(previous_cfg_name)): | 56 | config_filename = os.path.expandvars(os.path.expanduser( |
69 | 64 | cfg.update_from_another( | 57 | generic_launcher.config_filename)) |
70 | 65 | Configuration.from_path(expand_all(previous_cfg_name)), | 58 | launcher = generic_launcher.get_concrete_launcher() |
71 | 66 | 'config file: {}'.format(previous_cfg_name)) | 59 | if os.path.isabs(config_filename): |
72 | 60 | configs.append(config_filename) | ||
73 | 67 | else: | 61 | else: |
76 | 68 | for sdir in SEARCH_DIRS: | 62 | search_dirs = [ |
77 | 69 | config = expand_all(os.path.join(sdir, previous_cfg_name)) | 63 | '$SNAP_DATA', |
78 | 64 | '/etc/xdg/', | ||
79 | 65 | '~/.config/', | ||
80 | 66 | ] | ||
81 | 67 | for d in search_dirs: | ||
82 | 68 | config = expand_all(os.path.join(d, config_filename)) | ||
83 | 70 | if os.path.exists(config): | 69 | if os.path.exists(config): |
98 | 71 | cfg.update_from_another( | 70 | configs.append(config) |
99 | 72 | Configuration.from_path(config), | 71 | launcher.read(configs) |
100 | 73 | 'config file: {}'.format(config)) | 72 | if launcher.problem_list: |
101 | 74 | else: | 73 | _logger.error(_("Unable to start launcher because of errors:")) |
102 | 75 | _logger.info( | 74 | for problem in launcher.problem_list: |
103 | 76 | "Referenced config file doesn't exist: %s", config) | 75 | _logger.error("%s", str(problem)) |
104 | 77 | new_cfg_filename = cfg.get_value('config', 'config_filename') | 76 | raise SystemExit(1) |
105 | 78 | if new_cfg_filename != previous_cfg_name: | 77 | return launcher |
92 | 79 | load_configs(launcher_file, cfg) | ||
93 | 80 | if launcher_file: | ||
94 | 81 | cfg.update_from_another( | ||
95 | 82 | Configuration.from_path(launcher_file), | ||
96 | 83 | 'Launcher file: {}'.format(launcher_file)) | ||
97 | 84 | return cfg | ||
106 | diff --git a/checkbox_ng/launcher/check_config.py b/checkbox_ng/launcher/check_config.py | |||
107 | index 0ac3088..359d3ca 100644 | |||
108 | --- a/checkbox_ng/launcher/check_config.py | |||
109 | +++ b/checkbox_ng/launcher/check_config.py | |||
110 | @@ -1,6 +1,6 @@ | |||
111 | 1 | # This file is part of Checkbox. | 1 | # This file is part of Checkbox. |
112 | 2 | # | 2 | # |
114 | 3 | # Copyright 2018-2021 Canonical Ltd. | 3 | # Copyright 2018-2019 Canonical Ltd. |
115 | 4 | # Written by: | 4 | # Written by: |
116 | 5 | # Maciej Kisielewski <maciej.kisielewski@canonical.com> | 5 | # Maciej Kisielewski <maciej.kisielewski@canonical.com> |
117 | 6 | # | 6 | # |
118 | @@ -15,38 +15,41 @@ | |||
119 | 15 | # | 15 | # |
120 | 16 | # You should have received a copy of the GNU General Public License | 16 | # You should have received a copy of the GNU General Public License |
121 | 17 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | 17 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
123 | 18 | """This module contains the implementation of the `check-config` subcmd.""" | 18 | from plainbox.impl.secure.config import ValidationError |
124 | 19 | from plainbox.i18n import gettext as _ | ||
125 | 19 | 20 | ||
126 | 20 | from checkbox_ng.config import load_configs | 21 | from checkbox_ng.config import load_configs |
127 | 21 | 22 | ||
128 | 22 | 23 | ||
129 | 23 | class CheckConfig(): | 24 | class CheckConfig(): |
134 | 24 | """Implementation of the `check-config` sub-command.""" | 25 | def invoked(self, ctx): |
131 | 25 | @staticmethod | ||
132 | 26 | def invoked(_): | ||
133 | 27 | """Function that's run with `check-config` invocation.""" | ||
135 | 28 | config = load_configs() | 26 | config = load_configs() |
152 | 29 | print("Configuration files:") | 27 | print(_("Configuration files:")) |
153 | 30 | for source in config.sources: | 28 | for filename in config.filename_list: |
154 | 31 | print(" - {}".format(source)) | 29 | print(" - {}".format(filename)) |
155 | 32 | for sect_name, section in config.sections.items(): | 30 | for variable in config.Meta.variable_list: |
156 | 33 | print(" [{0}]".format(sect_name)) | 31 | print(" [{0}]".format(variable.section)) |
157 | 34 | for var_name in section.keys(): | 32 | print(" {0}={1}".format( |
158 | 35 | value = config.get_value(sect_name, var_name) | 33 | variable.name, |
159 | 36 | if isinstance(value, list): | 34 | variable.__get__(config, config.__class__))) |
160 | 37 | value = ', '.join(value) | 35 | for section in config.Meta.section_list: |
161 | 38 | origin = config.get_origin(sect_name, var_name) | 36 | print(" [{0}]".format(section.name)) |
162 | 39 | origin = "From {}".format(origin) if origin else "(Default)" | 37 | section_value = section.__get__(config, config.__class__) |
163 | 40 | key_val = "{}={}".format(var_name, value) | 38 | if section_value: |
164 | 41 | print(" {0: <34} {1}".format(key_val, origin)) | 39 | for key, value in sorted(section_value.items()): |
165 | 42 | problems = config.get_problems() | 40 | print(" {0}={1}".format(key, value)) |
166 | 43 | if not problems: | 41 | if config.problem_list: |
167 | 44 | print("No problems with config(s) found!") | 42 | print(_("Problems:")) |
168 | 43 | for problem in config.problem_list: | ||
169 | 44 | if isinstance(problem, ValidationError): | ||
170 | 45 | print(_(" - variable {0}: {1}").format( | ||
171 | 46 | problem.variable.name, problem.message)) | ||
172 | 47 | else: | ||
173 | 48 | print(" - {0}".format(problem.message)) | ||
174 | 49 | return 1 | ||
175 | 50 | else: | ||
176 | 51 | print(_("No validation problems found")) | ||
177 | 45 | return 0 | 52 | return 0 |
178 | 46 | print('Problems:') | ||
179 | 47 | for problem in problems: | ||
180 | 48 | print('- ', problem) | ||
181 | 49 | return 1 | ||
182 | 50 | 53 | ||
183 | 51 | def register_arguments(self, parser): | 54 | def register_arguments(self, parser): |
185 | 52 | """Register extra args for this subcmd. No extra args ATM.""" | 55 | pass |
186 | diff --git a/checkbox_ng/launcher/checkbox_cli.py b/checkbox_ng/launcher/checkbox_cli.py | |||
187 | index 9740eb8..b69da17 100644 | |||
188 | --- a/checkbox_ng/launcher/checkbox_cli.py | |||
189 | +++ b/checkbox_ng/launcher/checkbox_cli.py | |||
190 | @@ -27,6 +27,8 @@ import subprocess | |||
191 | 27 | import sys | 27 | import sys |
192 | 28 | 28 | ||
193 | 29 | from plainbox.impl.jobcache import ResourceJobCache | 29 | from plainbox.impl.jobcache import ResourceJobCache |
194 | 30 | from plainbox.impl.launcher import DefaultLauncherDefinition | ||
195 | 31 | from plainbox.impl.launcher import LauncherDefinition | ||
196 | 30 | from plainbox.impl.session.assistant import SessionAssistant | 32 | from plainbox.impl.session.assistant import SessionAssistant |
197 | 31 | 33 | ||
198 | 32 | from checkbox_ng.config import load_configs | 34 | from checkbox_ng.config import load_configs |
199 | diff --git a/checkbox_ng/launcher/master.py b/checkbox_ng/launcher/master.py | |||
200 | index 6f28444..730fc02 100644 | |||
201 | --- a/checkbox_ng/launcher/master.py | |||
202 | +++ b/checkbox_ng/launcher/master.py | |||
203 | @@ -37,7 +37,8 @@ from functools import partial | |||
204 | 37 | from tempfile import SpooledTemporaryFile | 37 | from tempfile import SpooledTemporaryFile |
205 | 38 | 38 | ||
206 | 39 | from plainbox.impl.color import Colorizer | 39 | from plainbox.impl.color import Colorizer |
208 | 40 | from plainbox.impl.config import Configuration | 40 | from plainbox.impl.launcher import DefaultLauncherDefinition |
209 | 41 | from plainbox.impl.secure.config import Unset | ||
210 | 41 | from plainbox.impl.session.remote_assistant import RemoteSessionAssistant | 42 | from plainbox.impl.session.remote_assistant import RemoteSessionAssistant |
211 | 42 | from plainbox.vendor import rpyc | 43 | from plainbox.vendor import rpyc |
212 | 43 | from checkbox_ng.urwid_ui import TestPlanBrowser | 44 | from checkbox_ng.urwid_ui import TestPlanBrowser |
213 | @@ -110,7 +111,7 @@ class RemoteMaster(ReportsStage, MainLoopStage): | |||
214 | 110 | 111 | ||
215 | 111 | @property | 112 | @property |
216 | 112 | def is_interactive(self): | 113 | def is_interactive(self): |
218 | 113 | return (self.launcher.get_value('ui', 'type') == 'interactive' and | 114 | return (self.launcher.ui_type == 'interactive' and |
219 | 114 | sys.stdin.isatty() and sys.stdout.isatty()) | 115 | sys.stdin.isatty() and sys.stdout.isatty()) |
220 | 115 | 116 | ||
221 | 116 | @property | 117 | @property |
222 | @@ -128,7 +129,7 @@ class RemoteMaster(ReportsStage, MainLoopStage): | |||
223 | 128 | self._is_bootstrapping = False | 129 | self._is_bootstrapping = False |
224 | 129 | self._target_host = ctx.args.host | 130 | self._target_host = ctx.args.host |
225 | 130 | self._normal_user = '' | 131 | self._normal_user = '' |
227 | 131 | self.launcher = Configuration() | 132 | self.launcher = DefaultLauncherDefinition() |
228 | 132 | if ctx.args.launcher: | 133 | if ctx.args.launcher: |
229 | 133 | expanded_path = os.path.expanduser(ctx.args.launcher) | 134 | expanded_path = os.path.expanduser(ctx.args.launcher) |
230 | 134 | if not os.path.exists(expanded_path): | 135 | if not os.path.exists(expanded_path): |
231 | @@ -136,8 +137,7 @@ class RemoteMaster(ReportsStage, MainLoopStage): | |||
232 | 136 | expanded_path)) | 137 | expanded_path)) |
233 | 137 | with open(expanded_path, 'rt') as f: | 138 | with open(expanded_path, 'rt') as f: |
234 | 138 | self._launcher_text = f.read() | 139 | self._launcher_text = f.read() |
237 | 139 | self.launcher = Configuration.from_text( | 140 | self.launcher.read_string(self._launcher_text) |
236 | 140 | self._launcher_text, 'Remote:{}'.format(expanded_path)) | ||
238 | 141 | if ctx.args.user: | 141 | if ctx.args.user: |
239 | 142 | self._normal_user = ctx.args.user | 142 | self._normal_user = ctx.args.user |
240 | 143 | timeout = 600 | 143 | timeout = 600 |
241 | @@ -185,9 +185,21 @@ class RemoteMaster(ReportsStage, MainLoopStage): | |||
242 | 185 | nonlocal keep_running | 185 | nonlocal keep_running |
243 | 186 | keep_running = False | 186 | keep_running = False |
244 | 187 | server_msg = msg | 187 | server_msg = msg |
246 | 188 | conn.root.register_master_blaster(quitter) | 188 | with contextlib.suppress(AttributeError): |
247 | 189 | # TODO: REMOTE_API | ||
248 | 190 | # when bumping the remote api make this bit obligatory | ||
249 | 191 | # i.e. remove the suppressing | ||
250 | 192 | conn.root.register_master_blaster(quitter) | ||
251 | 189 | self._sa = conn.root.get_sa() | 193 | self._sa = conn.root.get_sa() |
252 | 190 | self.sa.conn = conn | 194 | self.sa.conn = conn |
253 | 195 | # TODO: REMOTE API RAPI: Remove this API on the next RAPI bump | ||
254 | 196 | # the check and bailout is not needed if the slave as up to | ||
255 | 197 | # date as this master, so after bumping RAPI we can assume | ||
256 | 198 | # that slave is always passwordless | ||
257 | 199 | if not self.sa.passwordless_sudo: | ||
258 | 200 | raise SystemExit( | ||
259 | 201 | _("This version of Checkbox requires the service" | ||
260 | 202 | " to be run as root")) | ||
261 | 191 | try: | 203 | try: |
262 | 192 | slave_api_version = self.sa.get_remote_api_version() | 204 | slave_api_version = self.sa.get_remote_api_version() |
263 | 193 | except AttributeError: | 205 | except AttributeError: |
264 | @@ -255,8 +267,8 @@ class RemoteMaster(ReportsStage, MainLoopStage): | |||
265 | 255 | tps = self.sa.start_session(configuration) | 267 | tps = self.sa.start_session(configuration) |
266 | 256 | except RuntimeError as exc: | 268 | except RuntimeError as exc: |
267 | 257 | raise SystemExit(exc.args[0]) from exc | 269 | raise SystemExit(exc.args[0]) from exc |
270 | 258 | if self.launcher.get_value('test plan', 'forced'): | 270 | if self.launcher.test_plan_forced: |
271 | 259 | self.select_tp(self.launcher.get_value('test plan', 'unit')) | 271 | self.select_tp(self.launcher.test_plan_default_selection) |
272 | 260 | self.select_jobs(self.jobs) | 272 | self.select_jobs(self.jobs) |
273 | 261 | else: | 273 | else: |
274 | 262 | self.interactively_choose_tp(tps) | 274 | self.interactively_choose_tp(tps) |
275 | @@ -299,8 +311,8 @@ class RemoteMaster(ReportsStage, MainLoopStage): | |||
276 | 299 | return val.lower() in ('y', 'yes', 't', 'true', 'on', '1') | 311 | return val.lower() in ('y', 'yes', 't', 'true', 'on', '1') |
277 | 300 | 312 | ||
278 | 301 | def select_jobs(self, all_jobs): | 313 | def select_jobs(self, all_jobs): |
281 | 302 | if self.launcher.get_value('test selection', 'forced'): | 314 | if self.launcher.test_selection_forced: |
282 | 303 | if self.launcher.manifest: | 315 | if self.launcher.manifest is not Unset: |
283 | 304 | self.sa.save_manifest( | 316 | self.sa.save_manifest( |
284 | 305 | {manifest_id: | 317 | {manifest_id: |
285 | 306 | self._strtobool(self.launcher.manifest[manifest_id]) for | 318 | self._strtobool(self.launcher.manifest[manifest_id]) for |
286 | @@ -340,7 +352,7 @@ class RemoteMaster(ReportsStage, MainLoopStage): | |||
287 | 340 | Returns True if the remote should keep running. | 352 | Returns True if the remote should keep running. |
288 | 341 | And False if it should quit. | 353 | And False if it should quit. |
289 | 342 | """ | 354 | """ |
291 | 343 | if self.launcher.get_value('ui', 'type') == 'silent': | 355 | if self.launcher.ui_type == 'silent': |
292 | 344 | self._sa.terminate() | 356 | self._sa.terminate() |
293 | 345 | return False | 357 | return False |
294 | 346 | response = interrupt_dialog(self._target_host) | 358 | response = interrupt_dialog(self._target_host) |
295 | @@ -360,7 +372,7 @@ class RemoteMaster(ReportsStage, MainLoopStage): | |||
296 | 360 | 372 | ||
297 | 361 | def finish_session(self): | 373 | def finish_session(self): |
298 | 362 | print(self.C.header("Results")) | 374 | print(self.C.header("Results")) |
300 | 363 | if self.launcher.get_value('launcher', 'local_submission'): | 375 | if self.launcher.local_submission: |
301 | 364 | # Disable SIGINT while we save local results | 376 | # Disable SIGINT while we save local results |
302 | 365 | with contextlib.ExitStack() as stack: | 377 | with contextlib.ExitStack() as stack: |
303 | 366 | tmp_sig = signal.signal(signal.SIGINT, signal.SIG_IGN) | 378 | tmp_sig = signal.signal(signal.SIGINT, signal.SIG_IGN) |
304 | @@ -378,7 +390,7 @@ class RemoteMaster(ReportsStage, MainLoopStage): | |||
305 | 378 | self.run_jobs() | 390 | self.run_jobs() |
306 | 379 | 391 | ||
307 | 380 | def _handle_last_job_after_resume(self, resumed_session_info): | 392 | def _handle_last_job_after_resume(self, resumed_session_info): |
309 | 381 | if self.launcher.get_value('ui', 'type') == 'silent': | 393 | if self.launcher.ui_type == 'silent': |
310 | 382 | time.sleep(20) | 394 | time.sleep(20) |
311 | 383 | else: | 395 | else: |
312 | 384 | resume_dialog(10) | 396 | resume_dialog(10) |
313 | @@ -408,11 +420,11 @@ class RemoteMaster(ReportsStage, MainLoopStage): | |||
314 | 408 | self._run_jobs(jobs_repr, total_num) | 420 | self._run_jobs(jobs_repr, total_num) |
315 | 409 | rerun_candidates = self.sa.get_rerun_candidates('manual') | 421 | rerun_candidates = self.sa.get_rerun_candidates('manual') |
316 | 410 | if rerun_candidates: | 422 | if rerun_candidates: |
318 | 411 | if self.launcher.get_value('ui', 'type') == 'interactive': | 423 | if self.launcher.ui_type == 'interactive': |
319 | 412 | while True: | 424 | while True: |
320 | 413 | if not self._maybe_manual_rerun_jobs(): | 425 | if not self._maybe_manual_rerun_jobs(): |
321 | 414 | break | 426 | break |
323 | 415 | if self.launcher.get_value('ui', 'auto_retry'): | 427 | if self.launcher.auto_retry: |
324 | 416 | while True: | 428 | while True: |
325 | 417 | if not self._maybe_auto_rerun_jobs(): | 429 | if not self._maybe_auto_rerun_jobs(): |
326 | 418 | break | 430 | break |
327 | @@ -498,7 +510,7 @@ class RemoteMaster(ReportsStage, MainLoopStage): | |||
328 | 498 | if not rerun_candidates: | 510 | if not rerun_candidates: |
329 | 499 | return False | 511 | return False |
330 | 500 | # we wait before retrying | 512 | # we wait before retrying |
332 | 501 | delay = self.launcher.get_value('ui', 'delay_before_retry') | 513 | delay = self.launcher.delay_before_retry |
333 | 502 | _logger.info(_("Waiting {} seconds before retrying failed" | 514 | _logger.info(_("Waiting {} seconds before retrying failed" |
334 | 503 | " jobs...".format(delay))) | 515 | " jobs...".format(delay))) |
335 | 504 | time.sleep(delay) | 516 | time.sleep(delay) |
336 | diff --git a/checkbox_ng/launcher/stages.py b/checkbox_ng/launcher/stages.py | |||
337 | index 05566be..126324b 100644 | |||
338 | --- a/checkbox_ng/launcher/stages.py | |||
339 | +++ b/checkbox_ng/launcher/stages.py | |||
340 | @@ -26,11 +26,9 @@ import json | |||
341 | 26 | import logging | 26 | import logging |
342 | 27 | import os | 27 | import os |
343 | 28 | import time | 28 | import time |
344 | 29 | import textwrap | ||
345 | 30 | 29 | ||
346 | 31 | from plainbox.abc import IJobResult | 30 | from plainbox.abc import IJobResult |
347 | 32 | from plainbox.i18n import pgettext as C_ | 31 | from plainbox.i18n import pgettext as C_ |
348 | 33 | from plainbox.impl.config import Configuration | ||
349 | 34 | from plainbox.impl.result import JobResultBuilder | 32 | from plainbox.impl.result import JobResultBuilder |
350 | 35 | from plainbox.impl.result import tr_outcome | 33 | from plainbox.impl.result import tr_outcome |
351 | 36 | from plainbox.impl.transport import InvalidSecureIDError | 34 | from plainbox.impl.transport import InvalidSecureIDError |
352 | @@ -318,44 +316,42 @@ class ReportsStage(CheckboxUiStage): | |||
353 | 318 | self._export_fn = export_fn | 316 | self._export_fn = export_fn |
354 | 319 | 317 | ||
355 | 320 | def _prepare_stock_report(self, report): | 318 | def _prepare_stock_report(self, report): |
358 | 321 | 319 | # this is purposefully not using pythonic dict-keying for better | |
359 | 322 | new_origin = 'stock_reports' | 320 | # readability |
360 | 321 | if not self.sa.config.transports: | ||
361 | 322 | self.sa.config.transports = dict() | ||
362 | 323 | if not self.sa.config.exporters: | ||
363 | 324 | self.sa.config.exporters = dict() | ||
364 | 325 | if not self.sa.config.reports: | ||
365 | 326 | self.sa.config.reports = dict() | ||
366 | 323 | if report == 'text': | 327 | if report == 'text': |
379 | 324 | additional_config = Configuration.from_text(textwrap.dedent(""" | 328 | self.sa.config.exporters['text'] = { |
380 | 325 | [exporter:text] | 329 | 'unit': 'com.canonical.plainbox::text'} |
381 | 326 | unit = com.canonical.plainbox::text | 330 | self.sa.config.transports['stdout'] = { |
382 | 327 | [transport:stdout] | 331 | 'type': 'stream', 'stream': 'stdout'} |
383 | 328 | stream = stdout | 332 | # '1_' prefix ensures ordering amongst other stock reports. This |
384 | 329 | type = stream | 333 | # report name does not appear anywhere (because of forced: yes) |
385 | 330 | [report:1_text_to_screen] | 334 | self.sa.config.reports['1_text_to_screen'] = { |
386 | 331 | exporter = text | 335 | 'transport': 'stdout', 'exporter': 'text', 'forced': 'yes'} |
375 | 332 | forced = yes | ||
376 | 333 | transport = stdout | ||
377 | 334 | """), new_origin) | ||
378 | 335 | self.sa.config.update_from_another(additional_config, new_origin) | ||
387 | 336 | elif report == 'certification': | 336 | elif report == 'certification': |
398 | 337 | additional_config = Configuration.from_text(textwrap.dedent(""" | 337 | self.sa.config.exporters['tar'] = { |
399 | 338 | [exporter:tar] | 338 | 'unit': 'com.canonical.plainbox::tar'} |
400 | 339 | unit = com.canonical.plainbox::tar | 339 | self.sa.config.transports['c3'] = { |
401 | 340 | [transport:c3] | 340 | 'type': 'submission-service', |
402 | 341 | type = submission-service | 341 | 'secure_id': self.sa.config.transports.get('c3', {}).get( |
403 | 342 | [report:upload to certification] | 342 | 'secure_id', None)} |
404 | 343 | exporter = tar | 343 | self.sa.config.reports['upload to certification'] = { |
405 | 344 | transport = c3 | 344 | 'transport': 'c3', 'exporter': 'tar'} |
396 | 345 | """), new_origin) | ||
397 | 346 | self.sa.config.update_from_another(additional_config, new_origin) | ||
406 | 347 | elif report == 'certification-staging': | 345 | elif report == 'certification-staging': |
418 | 348 | additional_config = Configuration.from_text(textwrap.dedent(""" | 346 | self.sa.config.exporters['tar'] = { |
419 | 349 | [exporter:tar] | 347 | 'unit': 'com.canonical.plainbox::tar'} |
420 | 350 | unit = com.canonical.plainbox::tar | 348 | self.sa.config.transports['c3-staging'] = { |
421 | 351 | [transport:c3] | 349 | 'type': 'submission-service', |
422 | 352 | staging = yes | 350 | 'secure_id': self.sa.config.transports.get('c3', {}).get( |
423 | 353 | type = submission-service | 351 | 'secure_id', None), |
424 | 354 | [report:upload to certification-staging] | 352 | 'staging': 'yes'} |
425 | 355 | exporter = tar | 353 | self.sa.config.reports['upload to certification-staging'] = { |
426 | 356 | transport = c3 | 354 | 'transport': 'c3-staging', 'exporter': 'tar'} |
416 | 357 | """), new_origin) | ||
417 | 358 | self.sa.config.update_from_another(additional_config, new_origin) | ||
427 | 359 | elif report == 'submission_files': | 355 | elif report == 'submission_files': |
428 | 360 | # LP:1585326 maintain isoformat but removing ':' chars that cause | 356 | # LP:1585326 maintain isoformat but removing ':' chars that cause |
429 | 361 | # issues when copying files. | 357 | # issues when copying files. |
430 | @@ -368,21 +364,21 @@ class ReportsStage(CheckboxUiStage): | |||
431 | 368 | ('tar', '.tar.xz')]: | 364 | ('tar', '.tar.xz')]: |
432 | 369 | path = os.path.join(self.base_dir, ''.join( | 365 | path = os.path.join(self.base_dir, ''.join( |
433 | 370 | ['submission_', timestamp, file_ext])) | 366 | ['submission_', timestamp, file_ext])) |
449 | 371 | template = textwrap.dedent(""" | 367 | self.sa.config.transports['{}_file'.format(exporter)] = { |
450 | 372 | [transport:{exporter}_file] | 368 | 'type': 'file', |
451 | 373 | path = {path} | 369 | 'path': path} |
452 | 374 | type = file | 370 | if exporter not in self.sa.config.exporters: |
453 | 375 | [exporter:{exporter}] | 371 | self.sa.config.exporters[exporter] = { |
454 | 376 | unit = com.canonical.plainbox::{exporter} | 372 | 'unit': 'com.canonical.plainbox::{}'.format( |
455 | 377 | [report:2_{exporter}_file] | 373 | exporter)} |
456 | 378 | exporter = {exporter} | 374 | if not self.sa.config.exporters[exporter].get('unit'): |
457 | 379 | forced = yes | 375 | unit = 'com.canonical.plainbox::{}'.format(exporter) |
458 | 380 | transport = {exporter}_file | 376 | self.sa.config.exporters[exporter]['unit'] = unit |
459 | 381 | """) | 377 | self.sa.config.reports['2_{}_file'.format(exporter)] = { |
460 | 382 | additional_config = Configuration.from_text( | 378 | 'transport': '{}_file'.format(exporter), |
461 | 383 | template.format(exporter=exporter, path=path), new_origin) | 379 | 'exporter': '{}'.format(exporter), |
462 | 384 | self.sa.config.update_from_another( | 380 | 'forced': 'yes' |
463 | 385 | additional_config, new_origin) | 381 | } |
464 | 386 | 382 | ||
465 | 387 | def _prepare_transports(self): | 383 | def _prepare_transports(self): |
466 | 388 | self.base_dir = os.path.join( | 384 | self.base_dir = os.path.join( |
467 | @@ -398,9 +394,7 @@ class ReportsStage(CheckboxUiStage): | |||
468 | 398 | # depending on the type of transport we need to pick variable that | 394 | # depending on the type of transport we need to pick variable that |
469 | 399 | # serves as the 'where' param for the transport. In case of | 395 | # serves as the 'where' param for the transport. In case of |
470 | 400 | # certification site the URL is supplied here | 396 | # certification site the URL is supplied here |
474 | 401 | transport_cfg = self.sa.config.get_parametric_sections( | 397 | tr_type = self.sa.config.transports[transport]['type'] |
472 | 402 | 'transport')[transport] | ||
473 | 403 | tr_type = transport_cfg['type'] | ||
475 | 404 | if tr_type not in self._available_transports: | 398 | if tr_type not in self._available_transports: |
476 | 405 | _logger.error(_("Unrecognized type '%s' of transport '%s'"), | 399 | _logger.error(_("Unrecognized type '%s' of transport '%s'"), |
477 | 406 | tr_type, transport) | 400 | tr_type, transport) |
478 | @@ -408,11 +402,14 @@ class ReportsStage(CheckboxUiStage): | |||
479 | 408 | cls = self._available_transports[tr_type] | 402 | cls = self._available_transports[tr_type] |
480 | 409 | if tr_type == 'file': | 403 | if tr_type == 'file': |
481 | 410 | self.transports[transport] = cls( | 404 | self.transports[transport] = cls( |
483 | 411 | os.path.expanduser(transport_cfg['path'])) | 405 | os.path.expanduser( |
484 | 406 | self.sa.config.transports[transport]['path'])) | ||
485 | 412 | elif tr_type == 'stream': | 407 | elif tr_type == 'stream': |
487 | 413 | self.transports[transport] = cls(transport_cfg['stream']) | 408 | self.transports[transport] = cls( |
488 | 409 | self.sa.config.transports[transport]['stream']) | ||
489 | 414 | elif tr_type == 'submission-service': | 410 | elif tr_type == 'submission-service': |
491 | 415 | secure_id = transport_cfg.get('secure_id', None) | 411 | secure_id = self.sa.config.transports[transport].get( |
492 | 412 | 'secure_id', None) | ||
493 | 416 | if self.is_interactive: | 413 | if self.is_interactive: |
494 | 417 | new_description = input(self.C.BLUE(_( | 414 | new_description = input(self.C.BLUE(_( |
495 | 418 | 'Enter submission description (press Enter to skip): '))) | 415 | 'Enter submission description (press Enter to skip): '))) |
496 | @@ -428,7 +425,7 @@ class ReportsStage(CheckboxUiStage): | |||
497 | 428 | options = "secure_id={}".format(secure_id) | 425 | options = "secure_id={}".format(secure_id) |
498 | 429 | else: | 426 | else: |
499 | 430 | options = "" | 427 | options = "" |
501 | 431 | if transport_cfg.get('staging', False): | 428 | if self.sa.config.transports[transport].get('staging', False): |
502 | 432 | url = ('https://certification.staging.canonical.com/' | 429 | url = ('https://certification.staging.canonical.com/' |
503 | 433 | 'api/v1/submission/{}/'.format(secure_id)) | 430 | 'api/v1/submission/{}/'.format(secure_id)) |
504 | 434 | elif os.getenv('C3_URL'): | 431 | elif os.getenv('C3_URL'): |
505 | @@ -440,15 +437,14 @@ class ReportsStage(CheckboxUiStage): | |||
506 | 440 | self.transports[transport] = cls(url, options) | 437 | self.transports[transport] = cls(url, options) |
507 | 441 | 438 | ||
508 | 442 | def _export_results(self): | 439 | def _export_results(self): |
512 | 443 | stock_reports = self.sa.config.get_value('launcher', 'stock_reports') | 440 | if 'none' not in self.sa.config.stock_reports: |
513 | 444 | if 'none' not in stock_reports: | 441 | for report in self.sa.config.stock_reports: |
511 | 445 | for report in stock_reports: | ||
514 | 446 | if report in ['certification', 'certification-staging']: | 442 | if report in ['certification', 'certification-staging']: |
515 | 447 | # skip stock c3 report if secure_id is not given from | 443 | # skip stock c3 report if secure_id is not given from |
516 | 448 | # config files or launchers, and the UI is non-interactive | 444 | # config files or launchers, and the UI is non-interactive |
517 | 449 | # (silent) | 445 | # (silent) |
520 | 450 | if ('transport:c3' not in self.sa.config.sections.keys() | 446 | if ('c3' not in self.sa.config.transports and |
521 | 451 | and not self.is_interactive): | 447 | not self.is_interactive): |
522 | 452 | continue | 448 | continue |
523 | 453 | # don't generate stock c3 reports if sideloaded providers | 449 | # don't generate stock c3 reports if sideloaded providers |
524 | 454 | # were in use, something that should only be done during | 450 | # were in use, something that should only be done during |
525 | @@ -461,9 +457,7 @@ class ReportsStage(CheckboxUiStage): | |||
526 | 461 | # reports are stored in an ordinary dict(), so sorting them ensures | 457 | # reports are stored in an ordinary dict(), so sorting them ensures |
527 | 462 | # the same order of submitting them between runs, and if they | 458 | # the same order of submitting them between runs, and if they |
528 | 463 | # share common prefix, they are next to each other | 459 | # share common prefix, they are next to each other |
532 | 464 | for name, params in sorted( | 460 | for name, params in sorted(self.sa.config.reports.items()): |
530 | 465 | self.sa.config.get_parametric_sections('report').items()): | ||
531 | 466 | |||
533 | 467 | # don't generate stock c3 reports if sideloaded providers | 461 | # don't generate stock c3 reports if sideloaded providers |
534 | 468 | # were in use, something that should only be done during | 462 | # were in use, something that should only be done during |
535 | 469 | # development | 463 | # development |
536 | @@ -482,10 +476,8 @@ class ReportsStage(CheckboxUiStage): | |||
537 | 482 | cmd = 'y' | 476 | cmd = 'y' |
538 | 483 | if cmd == 'n': | 477 | if cmd == 'n': |
539 | 484 | continue | 478 | continue |
544 | 485 | all_exporters = self.sa.config.get_parametric_sections('exporter') | 479 | exporter_id = self.sa.config.exporters[params['exporter']]['unit'] |
545 | 486 | exporter_id = self.sa.config.get_parametric_sections('exporter')[ | 480 | exporter_options = self.sa.config.exporters[ |
542 | 487 | params['exporter']]['unit'] | ||
543 | 488 | exp_options = self.sa.config.get_parametric_sections('exporter')[ | ||
546 | 489 | params['exporter']].get('options', '').split() | 481 | params['exporter']].get('options', '').split() |
547 | 490 | done_sending = False | 482 | done_sending = False |
548 | 491 | while not done_sending: | 483 | while not done_sending: |
549 | @@ -497,7 +489,7 @@ class ReportsStage(CheckboxUiStage): | |||
550 | 497 | else: | 489 | else: |
551 | 498 | try: | 490 | try: |
552 | 499 | result = self.sa.export_to_transport( | 491 | result = self.sa.export_to_transport( |
554 | 500 | exporter_id, transport, exp_options) | 492 | exporter_id, transport, exporter_options) |
555 | 501 | except ExporterError as exc: | 493 | except ExporterError as exc: |
556 | 502 | _logger.warning( | 494 | _logger.warning( |
557 | 503 | _("Problem occured when preparing %s report:" | 495 | _("Problem occured when preparing %s report:" |
558 | @@ -525,14 +517,14 @@ class ReportsStage(CheckboxUiStage): | |||
559 | 525 | done_sending = True | 517 | done_sending = True |
560 | 526 | continue | 518 | continue |
561 | 527 | if self._retry_dialog(): | 519 | if self._retry_dialog(): |
564 | 528 | self.sa.config.sections['transports']['c3'].pop( | 520 | self.sa.config.transports['c3'].pop('secure_id') |
563 | 529 | 'secure_id') | ||
565 | 530 | continue | 521 | continue |
567 | 531 | except Exception as exc: | 522 | except Exception: |
568 | 532 | _logger.error( | 523 | _logger.error( |
569 | 533 | _("Problem with a '%s' report using '%s' exporter " | 524 | _("Problem with a '%s' report using '%s' exporter " |
572 | 534 | "sent to '%s' transport. Reason %s"), | 525 | "sent to '%s' transport."), |
573 | 535 | name, exporter_id, transport.url, exc) | 526 | name, exporter_id, transport.url) |
574 | 527 | self._reset_auto_submission_retries() | ||
575 | 536 | done_sending = True | 528 | done_sending = True |
576 | 537 | 529 | ||
577 | 538 | def _retry_dialog(self): | 530 | def _retry_dialog(self): |
578 | @@ -554,14 +546,3 @@ class ReportsStage(CheckboxUiStage): | |||
579 | 554 | return True | 546 | return True |
580 | 555 | 547 | ||
581 | 556 | return False | 548 | return False |
582 | 557 | |||
583 | 558 | template = textwrap.dedent(""" | ||
584 | 559 | [transport:{exporter}_file] | ||
585 | 560 | type = file | ||
586 | 561 | path = {path} | ||
587 | 562 | [exporter:{exporter}] | ||
588 | 563 | unit = com.canonical.plainbox::{exporter} | ||
589 | 564 | [report:2_{exporter}_file] | ||
590 | 565 | transport = {exporter}_file | ||
591 | 566 | exporter = {exporter} | ||
592 | 567 | forced = yes""") | ||
593 | diff --git a/checkbox_ng/launcher/subcommands.py b/checkbox_ng/launcher/subcommands.py | |||
594 | index 1d3c278..69f4a4a 100644 | |||
595 | --- a/checkbox_ng/launcher/subcommands.py | |||
596 | +++ b/checkbox_ng/launcher/subcommands.py | |||
597 | @@ -42,6 +42,7 @@ from plainbox.impl.execution import UnifiedRunner | |||
598 | 42 | from plainbox.impl.highlevel import Explorer | 42 | from plainbox.impl.highlevel import Explorer |
599 | 43 | from plainbox.impl.result import MemoryJobResult | 43 | from plainbox.impl.result import MemoryJobResult |
600 | 44 | from plainbox.impl.runner import slugify | 44 | from plainbox.impl.runner import slugify |
601 | 45 | from plainbox.impl.secure.config import Unset | ||
602 | 45 | from plainbox.impl.secure.sudo_broker import sudo_password_provider | 46 | from plainbox.impl.secure.sudo_broker import sudo_password_provider |
603 | 46 | from plainbox.impl.session.assistant import SA_RESTARTABLE | 47 | from plainbox.impl.session.assistant import SA_RESTARTABLE |
604 | 47 | from plainbox.impl.session.restart import detect_restart_strategy | 48 | from plainbox.impl.session.restart import detect_restart_strategy |
605 | @@ -167,10 +168,10 @@ class Launcher(MainLoopStage, ReportsStage): | |||
606 | 167 | return self._C | 168 | return self._C |
607 | 168 | 169 | ||
608 | 169 | def get_sa_api_version(self): | 170 | def get_sa_api_version(self): |
610 | 170 | return '0.99' | 171 | return self.launcher.api_version |
611 | 171 | 172 | ||
612 | 172 | def get_sa_api_flags(self): | 173 | def get_sa_api_flags(self): |
614 | 173 | return [SA_RESTARTABLE] | 174 | return self.launcher.api_flags |
615 | 174 | 175 | ||
616 | 175 | def invoked(self, ctx): | 176 | def invoked(self, ctx): |
617 | 176 | if ctx.args.version: | 177 | if ctx.args.version: |
618 | @@ -183,12 +184,12 @@ class Launcher(MainLoopStage, ReportsStage): | |||
619 | 183 | # exited by now, so validation passed | 184 | # exited by now, so validation passed |
620 | 184 | print(_("Launcher seems valid.")) | 185 | print(_("Launcher seems valid.")) |
621 | 185 | return | 186 | return |
623 | 186 | self.configuration = load_configs(ctx.args.launcher) | 187 | self.launcher = load_configs(ctx.args.launcher) |
624 | 187 | logging_level = { | 188 | logging_level = { |
625 | 188 | 'normal': logging.WARNING, | 189 | 'normal': logging.WARNING, |
626 | 189 | 'verbose': logging.INFO, | 190 | 'verbose': logging.INFO, |
627 | 190 | 'debug': logging.DEBUG, | 191 | 'debug': logging.DEBUG, |
629 | 191 | }[self.configuration.get_value('ui', 'verbosity')] | 192 | }[self.launcher.verbosity] |
630 | 192 | if not ctx.args.verbose and not ctx.args.debug: | 193 | if not ctx.args.verbose and not ctx.args.debug: |
631 | 193 | # Command line args take precendence | 194 | # Command line args take precendence |
632 | 194 | logging.basicConfig(level=logging_level) | 195 | logging.basicConfig(level=logging_level) |
633 | @@ -199,28 +200,25 @@ class Launcher(MainLoopStage, ReportsStage): | |||
634 | 199 | # replace the previously built SA with the defaults | 200 | # replace the previously built SA with the defaults |
635 | 200 | self._configure_restart(ctx) | 201 | self._configure_restart(ctx) |
636 | 201 | self._prepare_transports() | 202 | self._prepare_transports() |
638 | 202 | ctx.sa.use_alternate_configuration(self.configuration) | 203 | ctx.sa.use_alternate_configuration(self.launcher) |
639 | 203 | if not self._maybe_resume_session(): | 204 | if not self._maybe_resume_session(): |
640 | 204 | self._start_new_session() | 205 | self._start_new_session() |
641 | 205 | self._pick_jobs_to_run() | 206 | self._pick_jobs_to_run() |
642 | 206 | if not self.ctx.sa.get_static_todo_list(): | 207 | if not self.ctx.sa.get_static_todo_list(): |
643 | 207 | return 0 | 208 | return 0 |
646 | 208 | if 'submission_files' in self.configuration.get_value( | 209 | if 'submission_files' in self.launcher.stock_reports: |
645 | 209 | 'launcher', 'stock_reports'): | ||
647 | 210 | print("Reports will be saved to: {}".format(self.base_dir)) | 210 | print("Reports will be saved to: {}".format(self.base_dir)) |
648 | 211 | # we initialize the nb of attempts for all the selected jobs... | 211 | # we initialize the nb of attempts for all the selected jobs... |
649 | 212 | for job_id in self.ctx.sa.get_dynamic_todo_list(): | 212 | for job_id in self.ctx.sa.get_dynamic_todo_list(): |
650 | 213 | job_state = self.ctx.sa.get_job_state(job_id) | 213 | job_state = self.ctx.sa.get_job_state(job_id) |
653 | 214 | job_state.attempts = self.configuration.get_value( | 214 | job_state.attempts = self.launcher.max_attempts |
652 | 215 | 'ui', 'max_attempts') | ||
654 | 216 | # ... before running them | 215 | # ... before running them |
655 | 217 | self._run_jobs(self.ctx.sa.get_dynamic_todo_list()) | 216 | self._run_jobs(self.ctx.sa.get_dynamic_todo_list()) |
658 | 218 | if self.is_interactive and not self.configuration.get_value( | 217 | if self.is_interactive and not self.launcher.auto_retry: |
657 | 219 | 'ui', 'auto_retry'): | ||
659 | 220 | while True: | 218 | while True: |
660 | 221 | if not self._maybe_rerun_jobs(): | 219 | if not self._maybe_rerun_jobs(): |
661 | 222 | break | 220 | break |
663 | 223 | elif self.configuration.get_value('ui', 'auto_retry'): | 221 | elif self.launcher.auto_retry: |
664 | 224 | while True: | 222 | while True: |
665 | 225 | if not self._maybe_auto_rerun_jobs(): | 223 | if not self._maybe_auto_rerun_jobs(): |
666 | 226 | break | 224 | break |
667 | @@ -237,18 +235,23 @@ class Launcher(MainLoopStage, ReportsStage): | |||
668 | 237 | 235 | ||
669 | 238 | We can then interact with the user when we encounter OUTCOME_UNDECIDED. | 236 | We can then interact with the user when we encounter OUTCOME_UNDECIDED. |
670 | 239 | """ | 237 | """ |
673 | 240 | return (self.configuration.get_value('ui', 'type') == 'interactive' | 238 | return (self.launcher.ui_type == 'interactive' and |
674 | 241 | and sys.stdin.isatty() and sys.stdout.isatty()) | 239 | sys.stdin.isatty() and sys.stdout.isatty()) |
675 | 242 | 240 | ||
676 | 243 | def _configure_restart(self, ctx): | 241 | def _configure_restart(self, ctx): |
677 | 244 | if SA_RESTARTABLE not in self.get_sa_api_flags(): | 242 | if SA_RESTARTABLE not in self.get_sa_api_flags(): |
678 | 245 | return | 243 | return |
680 | 246 | if self.configuration.get_value('restart', 'strategy'): | 244 | if self.launcher.restart_strategy: |
681 | 247 | try: | 245 | try: |
682 | 248 | cls = get_strategy_by_name( | 246 | cls = get_strategy_by_name( |
685 | 249 | self.configuration.get_value('restart', 'strategy')) | 247 | self.launcher.restart_strategy) |
686 | 250 | strategy = cls(**self.configuration.get_strategy_kwargs()) | 248 | kwargs = copy.deepcopy(self.launcher.restart) |
687 | 249 | # [restart] section has the kwargs for the strategy initializer | ||
688 | 250 | # and the 'strategy' which is not one, let's pop it | ||
689 | 251 | kwargs.pop('strategy') | ||
690 | 252 | strategy = cls(**kwargs) | ||
691 | 251 | ctx.sa.use_alternate_restart_strategy(strategy) | 253 | ctx.sa.use_alternate_restart_strategy(strategy) |
692 | 254 | |||
693 | 252 | except KeyError: | 255 | except KeyError: |
694 | 253 | _logger.warning(_('Unknown restart strategy: %s', ( | 256 | _logger.warning(_('Unknown restart strategy: %s', ( |
695 | 254 | self.launcher.restart_strategy))) | 257 | self.launcher.restart_strategy))) |
696 | @@ -278,7 +281,7 @@ class Launcher(MainLoopStage, ReportsStage): | |||
697 | 278 | respawn_cmd += os.path.abspath(ctx.args.launcher) + ' ' | 281 | respawn_cmd += os.path.abspath(ctx.args.launcher) + ' ' |
698 | 279 | respawn_cmd += '--resume {}' # interpolate with session_id | 282 | respawn_cmd += '--resume {}' # interpolate with session_id |
699 | 280 | ctx.sa.configure_application_restart( | 283 | ctx.sa.configure_application_restart( |
701 | 281 | lambda session_id: [respawn_cmd.format(session_id)], 'local') | 284 | lambda session_id: [respawn_cmd.format(session_id)]) |
702 | 282 | 285 | ||
703 | 283 | def _maybe_resume_session(self): | 286 | def _maybe_resume_session(self): |
704 | 284 | resume_candidates = list(self.ctx.sa.get_resumable_sessions()) | 287 | resume_candidates = list(self.ctx.sa.get_resumable_sessions()) |
705 | @@ -335,21 +338,18 @@ class Launcher(MainLoopStage, ReportsStage): | |||
706 | 335 | 338 | ||
707 | 336 | def _start_new_session(self): | 339 | def _start_new_session(self): |
708 | 337 | print(_("Preparing...")) | 340 | print(_("Preparing...")) |
715 | 338 | title = self.ctx.args.title or self.configuration.get_value( | 341 | title = self.ctx.args.title or self.launcher.session_title |
716 | 339 | 'launcher', 'session_title') | 342 | title = title or self.launcher.app_id |
717 | 340 | title = title or self.configuration.get_value('launcher', 'app_id') | 343 | if self.launcher.app_version: |
718 | 341 | if self.configuration.get_value('launcher', 'app_version'): | 344 | title += ' {}'.format(self.launcher.app_version) |
713 | 342 | title += ' {}'.format(self.configuration.get_value( | ||
714 | 343 | 'launcher', 'app_version')) | ||
719 | 344 | runner_kwargs = { | 345 | runner_kwargs = { |
722 | 345 | 'normal_user_provider': lambda: self.configuration.get_value( | 346 | 'normal_user_provider': lambda: self.launcher.normal_user, |
721 | 346 | 'daemon', 'normal_user'), | ||
723 | 347 | 'password_provider': sudo_password_provider.get_sudo_password, | 347 | 'password_provider': sudo_password_provider.get_sudo_password, |
724 | 348 | 'stdin': None, | 348 | 'stdin': None, |
725 | 349 | } | 349 | } |
726 | 350 | self.ctx.sa.start_new_session(title, UnifiedRunner, runner_kwargs) | 350 | self.ctx.sa.start_new_session(title, UnifiedRunner, runner_kwargs) |
729 | 351 | if self.configuration.get_value('test plan', 'forced'): | 351 | if self.launcher.test_plan_forced: |
730 | 352 | tp_id = self.configuration.get_value('test plan', 'unit') | 352 | tp_id = self.launcher.test_plan_default_selection |
731 | 353 | if tp_id not in self.ctx.sa.get_test_plans(): | 353 | if tp_id not in self.ctx.sa.get_test_plans(): |
732 | 354 | _logger.error(_( | 354 | _logger.error(_( |
733 | 355 | 'The test plan "%s" is not available!'), tp_id) | 355 | 'The test plan "%s" is not available!'), tp_id) |
734 | @@ -365,8 +365,7 @@ class Launcher(MainLoopStage, ReportsStage): | |||
735 | 365 | if tp_id is None: | 365 | if tp_id is None: |
736 | 366 | raise SystemExit(_("No test plan selected.")) | 366 | raise SystemExit(_("No test plan selected.")) |
737 | 367 | self.ctx.sa.select_test_plan(tp_id) | 367 | self.ctx.sa.select_test_plan(tp_id) |
740 | 368 | description = self.ctx.args.message or self.configuration.get_value( | 368 | description = self.ctx.args.message or self.launcher.session_desc |
739 | 369 | 'launcher', 'session_desc') | ||
741 | 370 | self.ctx.sa.update_app_blob(json.dumps( | 369 | self.ctx.sa.update_app_blob(json.dumps( |
742 | 371 | {'testplan_id': tp_id, | 370 | {'testplan_id': tp_id, |
743 | 372 | 'description': description}).encode("UTF-8")) | 371 | 'description': description}).encode("UTF-8")) |
744 | @@ -381,7 +380,7 @@ class Launcher(MainLoopStage, ReportsStage): | |||
745 | 381 | def _interactively_pick_test_plan(self): | 380 | def _interactively_pick_test_plan(self): |
746 | 382 | test_plan_ids = self.ctx.sa.get_test_plans() | 381 | test_plan_ids = self.ctx.sa.get_test_plans() |
747 | 383 | filtered_tp_ids = set() | 382 | filtered_tp_ids = set() |
749 | 384 | for filter in self.configuration.get_value('test plan', 'filter'): | 383 | for filter in self.launcher.test_plan_filters: |
750 | 385 | filtered_tp_ids.update(fnmatch.filter(test_plan_ids, filter)) | 384 | filtered_tp_ids.update(fnmatch.filter(test_plan_ids, filter)) |
751 | 386 | tp_info_list = self._generate_tp_infos(filtered_tp_ids) | 385 | tp_info_list = self._generate_tp_infos(filtered_tp_ids) |
752 | 387 | if not tp_info_list: | 386 | if not tp_info_list: |
753 | @@ -389,20 +388,19 @@ class Launcher(MainLoopStage, ReportsStage): | |||
754 | 389 | return | 388 | return |
755 | 390 | selected_tp = TestPlanBrowser( | 389 | selected_tp = TestPlanBrowser( |
756 | 391 | _("Select test plan"), tp_info_list, | 390 | _("Select test plan"), tp_info_list, |
758 | 392 | self.configuration.get_value('test plan', 'unit')).run() | 391 | self.launcher.test_plan_default_selection).run() |
759 | 393 | return selected_tp | 392 | return selected_tp |
760 | 394 | 393 | ||
761 | 395 | def _strtobool(self, val): | 394 | def _strtobool(self, val): |
762 | 396 | return val.lower() in ('y', 'yes', 't', 'true', 'on', '1') | 395 | return val.lower() in ('y', 'yes', 't', 'true', 'on', '1') |
763 | 397 | 396 | ||
764 | 398 | def _pick_jobs_to_run(self): | 397 | def _pick_jobs_to_run(self): |
767 | 399 | if self.configuration.get_value('test selection', 'forced'): | 398 | if self.launcher.test_selection_forced: |
768 | 400 | if self.configuration.manifest: | 399 | if self.launcher.manifest is not Unset: |
769 | 401 | self.ctx.sa.save_manifest( | 400 | self.ctx.sa.save_manifest( |
770 | 402 | {manifest_id: | 401 | {manifest_id: |
774 | 403 | self._strtobool( | 402 | self._strtobool(self.launcher.manifest[manifest_id]) for |
775 | 404 | self.configuration.manifest[manifest_id]) for | 403 | manifest_id in self.launcher.manifest} |
773 | 405 | manifest_id in self.configuration.manifest} | ||
776 | 406 | ) | 404 | ) |
777 | 407 | # by default all tests are selected; so we're done here | 405 | # by default all tests are selected; so we're done here |
778 | 408 | return | 406 | return |
779 | @@ -494,7 +492,7 @@ class Launcher(MainLoopStage, ReportsStage): | |||
780 | 494 | if not rerun_candidates: | 492 | if not rerun_candidates: |
781 | 495 | return False | 493 | return False |
782 | 496 | # we wait before retrying | 494 | # we wait before retrying |
784 | 497 | delay = self.configuration.get_value('ui', 'delay_before_retry') | 495 | delay = self.launcher.delay_before_retry |
785 | 498 | _logger.info(_("Waiting {} seconds before retrying failed" | 496 | _logger.info(_("Waiting {} seconds before retrying failed" |
786 | 499 | " jobs...".format(delay))) | 497 | " jobs...".format(delay))) |
787 | 500 | time.sleep(delay) | 498 | time.sleep(delay) |
788 | @@ -527,11 +525,10 @@ class Launcher(MainLoopStage, ReportsStage): | |||
789 | 527 | def considering_job(self, job, job_state): | 525 | def considering_job(self, job, job_state): |
790 | 528 | pass | 526 | pass |
791 | 529 | show_out = True | 527 | show_out = True |
794 | 530 | output = self.configuration.get_value('ui', 'output') | 528 | if self.launcher.output == 'hide-resource-and-attachment': |
793 | 531 | if output == 'hide-resource-and-attachment': | ||
795 | 532 | if job.plugin in ('local', 'resource', 'attachment'): | 529 | if job.plugin in ('local', 'resource', 'attachment'): |
796 | 533 | show_out = False | 530 | show_out = False |
798 | 534 | elif output in ['hide', 'hide-automated']: | 531 | elif self.launcher.output in ['hide', 'hide-automated']: |
799 | 535 | if job.plugin in ('shell', 'local', 'resource', 'attachment'): | 532 | if job.plugin in ('shell', 'local', 'resource', 'attachment'): |
800 | 536 | show_out = False | 533 | show_out = False |
801 | 537 | if 'suppress-output' in job.get_flag_set(): | 534 | if 'suppress-output' in job.get_flag_set(): |
802 | @@ -751,7 +748,7 @@ class Run(MainLoopStage): | |||
803 | 751 | respawn_cmd = sys.argv[0] # entry-point to checkbox | 748 | respawn_cmd = sys.argv[0] # entry-point to checkbox |
804 | 752 | respawn_cmd += ' --resume {}' # interpolate with session_id | 749 | respawn_cmd += ' --resume {}' # interpolate with session_id |
805 | 753 | self.sa.configure_application_restart( | 750 | self.sa.configure_application_restart( |
807 | 754 | lambda session_id: [respawn_cmd.format(session_id)], 'local') | 751 | lambda session_id: [respawn_cmd.format(session_id)]) |
808 | 755 | 752 | ||
809 | 756 | 753 | ||
810 | 757 | class List(): | 754 | class List(): |
811 | diff --git a/plainbox/impl/applogic.py b/plainbox/impl/applogic.py | |||
812 | index 84f9906..1d90dfd 100644 | |||
813 | --- a/plainbox/impl/applogic.py | |||
814 | +++ b/plainbox/impl/applogic.py | |||
815 | @@ -31,6 +31,7 @@ import os | |||
816 | 31 | from plainbox.abc import IJobResult | 31 | from plainbox.abc import IJobResult |
817 | 32 | from plainbox.i18n import gettext as _ | 32 | from plainbox.i18n import gettext as _ |
818 | 33 | from plainbox.impl.result import MemoryJobResult | 33 | from plainbox.impl.result import MemoryJobResult |
819 | 34 | from plainbox.impl.secure import config | ||
820 | 34 | from plainbox.impl.secure.qualifiers import select_jobs | 35 | from plainbox.impl.secure.qualifiers import select_jobs |
821 | 35 | from plainbox.impl.session import SessionManager | 36 | from plainbox.impl.session import SessionManager |
822 | 36 | from plainbox.impl.session.jobs import InhibitionCause | 37 | from plainbox.impl.session.jobs import InhibitionCause |
823 | @@ -79,6 +80,23 @@ def run_job_if_possible(session, runner, config, job, update=True, ui=None): | |||
824 | 79 | return job_state, job_result | 80 | return job_state, job_result |
825 | 80 | 81 | ||
826 | 81 | 82 | ||
827 | 83 | class PlainBoxConfig(config.Config): | ||
828 | 84 | """ | ||
829 | 85 | Configuration for PlainBox itself | ||
830 | 86 | """ | ||
831 | 87 | |||
832 | 88 | environment = config.Section( | ||
833 | 89 | help_text=_("Environment variables for scripts and jobs")) | ||
834 | 90 | |||
835 | 91 | class Meta: | ||
836 | 92 | |||
837 | 93 | # TODO: properly depend on xdg and use real code that also handles | ||
838 | 94 | # XDG_CONFIG_HOME. | ||
839 | 95 | filename_list = [ | ||
840 | 96 | '/etc/xdg/plainbox.conf', | ||
841 | 97 | os.path.expanduser('~/.config/plainbox.conf')] | ||
842 | 98 | |||
843 | 99 | |||
844 | 82 | def get_all_exporter_names(): | 100 | def get_all_exporter_names(): |
845 | 83 | """ | 101 | """ |
846 | 84 | Get the identifiers (names) of all the supported session state exporters. | 102 | Get the identifiers (names) of all the supported session state exporters. |
847 | diff --git a/plainbox/impl/commands/__init__.py b/plainbox/impl/commands/__init__.py | |||
848 | index efdb1f6..5a26853 100644 | |||
849 | --- a/plainbox/impl/commands/__init__.py | |||
850 | +++ b/plainbox/impl/commands/__init__.py | |||
851 | @@ -47,6 +47,7 @@ class PlainBoxToolBase(ToolBase): | |||
852 | 47 | 1. :meth:`get_exec_name()` -- to know how the command will be called | 47 | 1. :meth:`get_exec_name()` -- to know how the command will be called |
853 | 48 | 2. :meth:`get_exec_version()` -- to know how the version of the tool | 48 | 2. :meth:`get_exec_version()` -- to know how the version of the tool |
854 | 49 | 3. :meth:`add_subcommands()` -- to add some actual commands to execute | 49 | 3. :meth:`add_subcommands()` -- to add some actual commands to execute |
855 | 50 | 4. :meth:`get_config_cls()` -- to know which config to use | ||
856 | 50 | 51 | ||
857 | 51 | This class has some complex control flow to support important and | 52 | This class has some complex control flow to support important and |
858 | 52 | interesting use cases. There are some concerns to people that subclass this | 53 | interesting use cases. There are some concerns to people that subclass this |
859 | @@ -68,6 +69,16 @@ class PlainBoxToolBase(ToolBase): | |||
860 | 68 | known yet. | 69 | known yet. |
861 | 69 | """ | 70 | """ |
862 | 70 | 71 | ||
863 | 72 | @classmethod | ||
864 | 73 | @abc.abstractmethod | ||
865 | 74 | def get_config_cls(cls): | ||
866 | 75 | """ | ||
867 | 76 | Get the Config class that is used by this implementation. | ||
868 | 77 | |||
869 | 78 | This can be overridden by subclasses to use a different config class | ||
870 | 79 | that is suitable for the particular application. | ||
871 | 80 | """ | ||
872 | 81 | |||
873 | 71 | def _load_config(self): | 82 | def _load_config(self): |
874 | 72 | return self.get_config_cls().get() | 83 | return self.get_config_cls().get() |
875 | 73 | 84 | ||
876 | diff --git a/plainbox/impl/config.py b/plainbox/impl/config.py | |||
877 | 74 | deleted file mode 100644 | 85 | deleted file mode 100644 |
878 | index 044862d..0000000 | |||
879 | --- a/plainbox/impl/config.py | |||
880 | +++ /dev/null | |||
881 | @@ -1,398 +0,0 @@ | |||
882 | 1 | # This file is part of Checkbox. | ||
883 | 2 | # | ||
884 | 3 | # Copyright 2021-2022 Canonical Ltd. | ||
885 | 4 | # Written by: | ||
886 | 5 | # Maciej Kisielewski <maciej.kisielewski@canonical.com> | ||
887 | 6 | # | ||
888 | 7 | # Checkbox is free software: you can redistribute it and/or modify | ||
889 | 8 | # it under the terms of the GNU General Public License version 3, | ||
890 | 9 | # as published by the Free Software Foundation. | ||
891 | 10 | # | ||
892 | 11 | # Checkbox is distributed in the hope that it will be useful, | ||
893 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
894 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
895 | 14 | # GNU General Public License for more details. | ||
896 | 15 | # | ||
897 | 16 | # You should have received a copy of the GNU General Public License | ||
898 | 17 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
899 | 18 | """ | ||
900 | 19 | This module defines class for handling Checkbox configs. | ||
901 | 20 | |||
902 | 21 | If we ever need to add validators to config variables, the addition should be | ||
903 | 22 | done in VarSpec (the fourth 'field'). | ||
904 | 23 | """ | ||
905 | 24 | import copy | ||
906 | 25 | import io | ||
907 | 26 | import logging | ||
908 | 27 | import os | ||
909 | 28 | import shlex | ||
910 | 29 | |||
911 | 30 | from configparser import ConfigParser | ||
912 | 31 | from collections import namedtuple, OrderedDict | ||
913 | 32 | |||
914 | 33 | logger = logging.getLogger(__name__) | ||
915 | 34 | |||
916 | 35 | |||
917 | 36 | class Configuration: | ||
918 | 37 | """ | ||
919 | 38 | Checkbox configuration storing objects. | ||
920 | 39 | |||
921 | 40 | Checkbox configs store various information on how to run the Checkbox. | ||
922 | 41 | For instance what reports to generate, should the session be interactive, | ||
923 | 42 | and many others. Look at CONFIG_SPEC for details. | ||
924 | 43 | """ | ||
925 | 44 | def __init__(self, source=None): | ||
926 | 45 | """Create a new configuration object filled with default values.""" | ||
927 | 46 | self.sections = OrderedDict() | ||
928 | 47 | self._origins = dict() | ||
929 | 48 | self._problems = [] | ||
930 | 49 | # sources is similar to origins, but instead of keeping an info on | ||
931 | 50 | # each variable, we note what configs got read in general | ||
932 | 51 | self._sources = [source] if source else [] | ||
933 | 52 | for section, contents in CONFIG_SPEC: | ||
934 | 53 | if isinstance(contents, ParametricSection): | ||
935 | 54 | # we don't know what the actual section name will be, | ||
936 | 55 | # so let's wait with the creation until we know the full name | ||
937 | 56 | continue | ||
938 | 57 | if isinstance(contents, DynamicSection): | ||
939 | 58 | self.sections[section] = DynamicSection() | ||
940 | 59 | else: | ||
941 | 60 | self.sections[section] = OrderedDict() | ||
942 | 61 | self._origins[section] = dict() | ||
943 | 62 | for name, spec in sorted(contents.items()): | ||
944 | 63 | self.sections[section][name] = spec.default | ||
945 | 64 | self._origins[section][name] = '' | ||
946 | 65 | |||
947 | 66 | @property | ||
948 | 67 | def environment(self): | ||
949 | 68 | """Return contents of the environment section.""" | ||
950 | 69 | return self.sections['environment'] | ||
951 | 70 | |||
952 | 71 | @property | ||
953 | 72 | def manifest(self): | ||
954 | 73 | """Return contents of the manifest section.""" | ||
955 | 74 | return self.sections['manifest'] | ||
956 | 75 | |||
957 | 76 | @property | ||
958 | 77 | def sources(self): | ||
959 | 78 | """Return list of sources for this configuration.""" | ||
960 | 79 | return self._sources | ||
961 | 80 | |||
962 | 81 | def get_strategy_kwargs(self): | ||
963 | 82 | """Return custom restart strategy parameters.""" | ||
964 | 83 | kwargs = copy.deepcopy(self.sections['restart']) | ||
965 | 84 | # [restart] section has the kwargs for the strategy initializer | ||
966 | 85 | # and the 'strategy' which is not one, let's pop it | ||
967 | 86 | kwargs.pop('strategy') | ||
968 | 87 | return kwargs | ||
969 | 88 | |||
970 | 89 | def notice_problem(self, problem): | ||
971 | 90 | """ Record and log problem encountered when building configuration.""" | ||
972 | 91 | self._problems.append(problem) | ||
973 | 92 | logger.warning(problem) | ||
974 | 93 | |||
975 | 94 | def get_problems(self): | ||
976 | 95 | """Return a list of problem as strings.""" | ||
977 | 96 | return self._problems | ||
978 | 97 | |||
979 | 98 | def get_value(self, section, name): | ||
980 | 99 | """Return a value of given `name` from given `section`,""" | ||
981 | 100 | return self.sections[section][name] | ||
982 | 101 | |||
983 | 102 | def get_origin(self, section, name): | ||
984 | 103 | """Return origin of the value.""" | ||
985 | 104 | return self._origins[section][name] | ||
986 | 105 | |||
987 | 106 | def update_from_another(self, configuration, origin): | ||
988 | 107 | """ | ||
989 | 108 | Update this configuration with values from `configuration`. | ||
990 | 109 | |||
991 | 110 | Only the values that are not defaults from 'configuration` are taken | ||
992 | 111 | into account. | ||
993 | 112 | """ | ||
994 | 113 | for section, variables in configuration.sections.items(): | ||
995 | 114 | for name in variables.keys(): | ||
996 | 115 | new_origin = configuration.get_origin(section, name) | ||
997 | 116 | if new_origin: | ||
998 | 117 | if ':' in section and section not in self.sections.keys(): | ||
999 | 118 | self.sections[section] = OrderedDict() | ||
1000 | 119 | self._origins[section] = dict() | ||
1001 | 120 | self.sections[section][name] = configuration.get_value( | ||
1002 | 121 | section, name) | ||
1003 | 122 | self._origins[section][name] = origin or new_origin | ||
1004 | 123 | self._sources += configuration.sources | ||
1005 | 124 | self._problems += configuration.get_problems() | ||
1006 | 125 | |||
1007 | 126 | def dyn_set_value(self, section, name, value, origin): | ||
1008 | 127 | """Set a value of a var from a dynamic section.""" | ||
1009 | 128 | if section == 'environment': | ||
1010 | 129 | name = name.upper() | ||
1011 | 130 | self.sections[section][name] = value | ||
1012 | 131 | self._origins[section][name] = origin | ||
1013 | 132 | |||
1014 | 133 | def set_value(self, section, name, value, origin, parser): | ||
1015 | 134 | """Set a new value for variable and update its origin.""" | ||
1016 | 135 | # we are kind off guaranteed that section will be found in the spec | ||
1017 | 136 | # but let's make linters happy | ||
1018 | 137 | if section in self._DYNAMIC_SECTIONS: | ||
1019 | 138 | self.dyn_set_value(section, name, value, origin) | ||
1020 | 139 | return | ||
1021 | 140 | parametrized = False | ||
1022 | 141 | if ':' in section: | ||
1023 | 142 | parametrized = True | ||
1024 | 143 | prefix, _ = section.split(':') | ||
1025 | 144 | if parametrized: | ||
1026 | 145 | # TODO: do the check here for typing | ||
1027 | 146 | pass | ||
1028 | 147 | |||
1029 | 148 | index = -1 | ||
1030 | 149 | for i, (sect_name, spec) in enumerate(CONFIG_SPEC): | ||
1031 | 150 | if sect_name == section: | ||
1032 | 151 | index = i | ||
1033 | 152 | if isinstance(spec, ParametricSection): | ||
1034 | 153 | if parametrized and sect_name == prefix: | ||
1035 | 154 | if name not in spec: | ||
1036 | 155 | problem = ( | ||
1037 | 156 | "Unexpected variable '{}' in section [{}] " | ||
1038 | 157 | "Origin: {}").format(name, section, origin) | ||
1039 | 158 | self.notice_problem(problem) | ||
1040 | 159 | return | ||
1041 | 160 | index = i | ||
1042 | 161 | if index == -1: | ||
1043 | 162 | # this should happen only for parametric sections | ||
1044 | 163 | problem = "Unexpected section [{}]. Origin: {}".format( | ||
1045 | 164 | section, origin) | ||
1046 | 165 | self.notice_problem(problem) | ||
1047 | 166 | return | ||
1048 | 167 | |||
1049 | 168 | assert index > -1 | ||
1050 | 169 | kind = CONFIG_SPEC[index][1][name].kind | ||
1051 | 170 | try: | ||
1052 | 171 | if kind == list: | ||
1053 | 172 | value = shlex.split(value.replace(',', ' ')) | ||
1054 | 173 | elif kind == bool: | ||
1055 | 174 | value = parser.getboolean(section, name) | ||
1056 | 175 | elif kind == float: | ||
1057 | 176 | value = parser.getfloat(section, name) | ||
1058 | 177 | elif kind == int: | ||
1059 | 178 | value = parser.getint(section, name) | ||
1060 | 179 | else: | ||
1061 | 180 | value = kind(value) | ||
1062 | 181 | if parametrized: | ||
1063 | 182 | # we couldn't have known the param names eariler (in __init__) | ||
1064 | 183 | # but now we do know them, so let's create the dict to hold | ||
1065 | 184 | # the values | ||
1066 | 185 | if section not in self.sections.keys(): | ||
1067 | 186 | self.sections[section] = OrderedDict() | ||
1068 | 187 | self._origins[section] = dict() | ||
1069 | 188 | self.sections[section][name] = value | ||
1070 | 189 | self._origins[section][name] = origin | ||
1071 | 190 | except TypeError: | ||
1072 | 191 | problem = ( | ||
1073 | 192 | "Problem with setting field {} in section [{}] " | ||
1074 | 193 | "'{}' cannot be used as {}. Origin: {}").format( | ||
1075 | 194 | name, section, value, kind, origin) | ||
1076 | 195 | self.notice_problem(problem) | ||
1077 | 196 | |||
1078 | 197 | def get_parametric_sections(self, prefix): | ||
1079 | 198 | """ | ||
1080 | 199 | Return a dict of parametrised section that share the same prefix. | ||
1081 | 200 | |||
1082 | 201 | The resulting dict is keyed by the parameter, the values are dicts | ||
1083 | 202 | with the declared variables. | ||
1084 | 203 | |||
1085 | 204 | E.g. | ||
1086 | 205 | If there's two sections: [report:myrep] and [report:other] | ||
1087 | 206 | The resulting dict will have two keys: myrep and other. | ||
1088 | 207 | """ | ||
1089 | 208 | result = dict() | ||
1090 | 209 | # check if there is such section declared in the SPEC | ||
1091 | 210 | for sect_name, section in CONFIG_SPEC: | ||
1092 | 211 | if not isinstance(section, ParametricSection): | ||
1093 | 212 | continue | ||
1094 | 213 | if sect_name == prefix: | ||
1095 | 214 | break | ||
1096 | 215 | else: | ||
1097 | 216 | raise ValueError("No such section in the spec ({}".format(prefix)) | ||
1098 | 217 | for sect_name, section in self.sections.items(): | ||
1099 | 218 | sect_prefix, _, sect_param = sect_name.partition(':') | ||
1100 | 219 | if sect_prefix == prefix: | ||
1101 | 220 | result[sect_param] = section | ||
1102 | 221 | return result | ||
1103 | 222 | |||
1104 | 223 | @classmethod | ||
1105 | 224 | def from_text(cls, text, origin): | ||
1106 | 225 | """ | ||
1107 | 226 | Create a new configuration with values from the text. | ||
1108 | 227 | |||
1109 | 228 | Behaves just the same as the from_ini_file method, but accepts string | ||
1110 | 229 | as the param. | ||
1111 | 230 | """ | ||
1112 | 231 | return cls.from_ini_file(io.StringIO(text), origin) | ||
1113 | 232 | |||
1114 | 233 | @classmethod | ||
1115 | 234 | def from_path(cls, path): | ||
1116 | 235 | """Create a new configuration with values stored in a file at path.""" | ||
1117 | 236 | cfg = Configuration() | ||
1118 | 237 | if not os.path.isfile(path): | ||
1119 | 238 | cfg.notice_problem("{} file not found".format(path)) | ||
1120 | 239 | return cfg | ||
1121 | 240 | with open(path, 'rt') as ini_file: | ||
1122 | 241 | return cls.from_ini_file(ini_file, path) | ||
1123 | 242 | |||
1124 | 243 | @classmethod | ||
1125 | 244 | def from_ini_file(cls, ini_file, origin): | ||
1126 | 245 | """ | ||
1127 | 246 | Create a new configuration with values from the ini file. | ||
1128 | 247 | |||
1129 | 248 | ini_file should be a file object. | ||
1130 | 249 | |||
1131 | 250 | This function is designed not to fail (raise), so if some entry in the | ||
1132 | 251 | ini file is misdefined then it should be ignored and the default value | ||
1133 | 252 | should be kept. Each such problem is kept in the self._problems list. | ||
1134 | 253 | """ | ||
1135 | 254 | cfg = Configuration(origin) | ||
1136 | 255 | parser = ConfigParser(delimiters='=') | ||
1137 | 256 | parser.read_string(ini_file.read()) | ||
1138 | 257 | for sect_name, section in parser.items(): | ||
1139 | 258 | if sect_name == 'DEFAULT': | ||
1140 | 259 | for var_name in section: | ||
1141 | 260 | problem = "[DEFAULT] section is not supported" | ||
1142 | 261 | cfg.notice_problem(problem) | ||
1143 | 262 | continue | ||
1144 | 263 | if ':' in sect_name: | ||
1145 | 264 | for var_name, var in section.items(): | ||
1146 | 265 | cfg.set_value(sect_name, var_name, var, origin, parser) | ||
1147 | 266 | continue | ||
1148 | 267 | if sect_name not in cfg.sections: | ||
1149 | 268 | problem = "Unexpected section [{}]. Origin: {}".format( | ||
1150 | 269 | sect_name, origin) | ||
1151 | 270 | cfg.notice_problem(problem) | ||
1152 | 271 | continue | ||
1153 | 272 | for var_name, var in section.items(): | ||
1154 | 273 | is_dyn = sect_name in cls._DYNAMIC_SECTIONS | ||
1155 | 274 | if var_name not in cfg.sections[sect_name] and not is_dyn: | ||
1156 | 275 | problem = ( | ||
1157 | 276 | "Unexpected variable '{}' in section [{}] " | ||
1158 | 277 | "Origin: {}").format(var_name, sect_name, origin) | ||
1159 | 278 | cfg.notice_problem(problem) | ||
1160 | 279 | continue | ||
1161 | 280 | cfg.set_value(sect_name, var_name, var, origin, parser) | ||
1162 | 281 | return cfg | ||
1163 | 282 | |||
1164 | 283 | _DYNAMIC_SECTIONS = ('environment', 'manifest') | ||
1165 | 284 | |||
1166 | 285 | |||
1167 | 286 | VarSpec = namedtuple('VarSpec', ['kind', 'default', 'help']) | ||
1168 | 287 | |||
1169 | 288 | |||
1170 | 289 | class ParametricSection(dict): | ||
1171 | 290 | """ Dict for storing parametric section's contents.""" | ||
1172 | 291 | |||
1173 | 292 | |||
1174 | 293 | class DynamicSection(dict): | ||
1175 | 294 | """ | ||
1176 | 295 | Dict for storing dynamic section's contents. | ||
1177 | 296 | |||
1178 | 297 | This is an extra type to record the fact that this is a different section | ||
1179 | 298 | compared to the predefined ones. It works and isn't very complex, but | ||
1180 | 299 | a different way of storing this information might be more elegant. | ||
1181 | 300 | """ | ||
1182 | 301 | |||
1183 | 302 | |||
1184 | 303 | # in order to maintain the section order the CONFIG_SPEC is a list of pairs, | ||
1185 | 304 | # where the first value is the name of the section and the other is a dict | ||
1186 | 305 | # of variable specs. | ||
1187 | 306 | CONFIG_SPEC = [ | ||
1188 | 307 | ('config', { | ||
1189 | 308 | 'config_filename': VarSpec( | ||
1190 | 309 | str, 'checkbox.conf', | ||
1191 | 310 | 'Name of the configuration file to look for.'), | ||
1192 | 311 | }), | ||
1193 | 312 | ('launcher', { | ||
1194 | 313 | 'launcher_version': VarSpec( | ||
1195 | 314 | int, 1, "Version of launcher to use"), | ||
1196 | 315 | 'app_id': VarSpec( | ||
1197 | 316 | str, 'checkbox-cli', "Identifier of the application"), | ||
1198 | 317 | 'app_version': VarSpec( | ||
1199 | 318 | str, '', "Version of the application"), | ||
1200 | 319 | 'stock_reports': VarSpec( | ||
1201 | 320 | list, ['text', 'certification', 'submission_files'], | ||
1202 | 321 | "List of stock reports to use"), | ||
1203 | 322 | 'local_submission': VarSpec( | ||
1204 | 323 | bool, True, ("Send/generate submission report locally when using " | ||
1205 | 324 | "checkbox remote")), | ||
1206 | 325 | 'session_title': VarSpec( | ||
1207 | 326 | str, 'session title', | ||
1208 | 327 | ("A title to be applied to the sessions created using this " | ||
1209 | 328 | "launcher that can be used in report generation")), | ||
1210 | 329 | 'session_desc': VarSpec( | ||
1211 | 330 | str, '', ("A string that can be applied to sessions created using " | ||
1212 | 331 | "this launcher. Useful for storing some contextual " | ||
1213 | 332 | "infomation about the session")), | ||
1214 | 333 | }), | ||
1215 | 334 | ('test plan', { | ||
1216 | 335 | 'filter': VarSpec( | ||
1217 | 336 | list, ['*'], | ||
1218 | 337 | "Constrain interactive choice to test plans matching this glob"), | ||
1219 | 338 | 'unit': VarSpec(str, '', "Select this test plan by default."), | ||
1220 | 339 | 'forced': VarSpec( | ||
1221 | 340 | bool, False, "Don't allow the user to change test plan."), | ||
1222 | 341 | }), | ||
1223 | 342 | ('test selection', { | ||
1224 | 343 | 'forced': VarSpec( | ||
1225 | 344 | bool, False, "Don't allow the user to alter test selection."), | ||
1226 | 345 | 'exclude': VarSpec( | ||
1227 | 346 | list, [], "Exclude test matching patterns from running."), | ||
1228 | 347 | }), | ||
1229 | 348 | ('ui', { | ||
1230 | 349 | 'type': VarSpec(str, 'interactive', "Type of user interface to use."), | ||
1231 | 350 | 'output': VarSpec(str, 'show', "Silence or restrict command output."), | ||
1232 | 351 | 'dont_suppress_output': VarSpec( | ||
1233 | 352 | bool, False, | ||
1234 | 353 | "Don't suppress the output of certain job plugin types."), | ||
1235 | 354 | 'verbosity': VarSpec(str, 'normal', "Verbosity level."), | ||
1236 | 355 | 'auto_retry': VarSpec( | ||
1237 | 356 | bool, False, | ||
1238 | 357 | "Automatically retry failed jobs at the end of the session."), | ||
1239 | 358 | 'max_attempts': VarSpec( | ||
1240 | 359 | int, 3, | ||
1241 | 360 | "Number of attempts to run a job when in auto-retry mode."), | ||
1242 | 361 | 'delay_before_retry': VarSpec( | ||
1243 | 362 | int, 1, ("Delay (in seconds) before " | ||
1244 | 363 | "retrying failed jobs in auto-retry mode.")), | ||
1245 | 364 | }), | ||
1246 | 365 | ('daemon', { | ||
1247 | 366 | 'normal_user': VarSpec( | ||
1248 | 367 | str, '', "Username to use for jobs that don't specify user."), | ||
1249 | 368 | }), | ||
1250 | 369 | ('restart', { | ||
1251 | 370 | 'strategy': VarSpec(str, '', "Use alternative restart strategy."), | ||
1252 | 371 | }), | ||
1253 | 372 | ('report', ParametricSection({ | ||
1254 | 373 | 'exporter': VarSpec( | ||
1255 | 374 | str, '', "Name of the exporter to use"), | ||
1256 | 375 | 'transport': VarSpec( | ||
1257 | 376 | str, '', "Name of the transport to use"), | ||
1258 | 377 | 'forced': VarSpec( | ||
1259 | 378 | bool, False, "Don't ask the user if they want the report."), | ||
1260 | 379 | })), | ||
1261 | 380 | ('transport', ParametricSection({ | ||
1262 | 381 | 'type': VarSpec( | ||
1263 | 382 | str, '', "Type of transport to use."), | ||
1264 | 383 | 'stream': VarSpec( | ||
1265 | 384 | str, 'stdout', "Stream to use - stdout or stderr."), | ||
1266 | 385 | 'path': VarSpec( | ||
1267 | 386 | str, '', "Path to where the report should be saved to."), | ||
1268 | 387 | 'secure_id': VarSpec( | ||
1269 | 388 | str, '', "Secure ID to use."), | ||
1270 | 389 | 'staging': VarSpec( | ||
1271 | 390 | bool, False, "Pushes to staging C3 instead of normal C3."), | ||
1272 | 391 | })), | ||
1273 | 392 | ('exporter', ParametricSection({ | ||
1274 | 393 | 'unit': VarSpec(str, '', "ID of the exporter to use."), | ||
1275 | 394 | 'options': VarSpec(list, [], "Flags to forward to the exporter."), | ||
1276 | 395 | })), | ||
1277 | 396 | ('environment', DynamicSection()), | ||
1278 | 397 | ('manifest', DynamicSection()), | ||
1279 | 398 | ] | ||
1280 | diff --git a/plainbox/impl/ctrl.py b/plainbox/impl/ctrl.py | |||
1281 | index 2242114..5facd64 100644 | |||
1282 | --- a/plainbox/impl/ctrl.py | |||
1283 | +++ b/plainbox/impl/ctrl.py | |||
1284 | @@ -60,6 +60,7 @@ from plainbox.impl.resource import ExpressionCannotEvaluateError | |||
1285 | 60 | from plainbox.impl.resource import ExpressionFailedError | 60 | from plainbox.impl.resource import ExpressionFailedError |
1286 | 61 | from plainbox.impl.resource import ResourceProgramError | 61 | from plainbox.impl.resource import ResourceProgramError |
1287 | 62 | from plainbox.impl.resource import Resource | 62 | from plainbox.impl.resource import Resource |
1288 | 63 | from plainbox.impl.secure.config import Unset | ||
1289 | 63 | from plainbox.impl.secure.origin import JobOutputTextSource | 64 | from plainbox.impl.secure.origin import JobOutputTextSource |
1290 | 64 | from plainbox.impl.secure.providers.v1 import Provider1 | 65 | from plainbox.impl.secure.providers.v1 import Provider1 |
1291 | 65 | from plainbox.impl.secure.rfc822 import RFC822SyntaxError | 66 | from plainbox.impl.secure.rfc822 import RFC822SyntaxError |
1292 | diff --git a/plainbox/impl/launcher.py b/plainbox/impl/launcher.py | |||
1293 | 66 | new file mode 100644 | 67 | new file mode 100644 |
1294 | index 0000000..16b3059 | |||
1295 | --- /dev/null | |||
1296 | +++ b/plainbox/impl/launcher.py | |||
1297 | @@ -0,0 +1,258 @@ | |||
1298 | 1 | # This file is part of Checkbox. | ||
1299 | 2 | # | ||
1300 | 3 | # Copyright 2014-2016 Canonical Ltd. | ||
1301 | 4 | # Written by: | ||
1302 | 5 | # Zygmunt Krynicki <zygmunt.krynicki@canonical.com> | ||
1303 | 6 | # Maciej Kisielewski <maciej.kisielewski@canonical.com> | ||
1304 | 7 | # | ||
1305 | 8 | # Checkbox is free software: you can redistribute it and/or modify | ||
1306 | 9 | # it under the terms of the GNU General Public License version 3, | ||
1307 | 10 | # as published by the Free Software Foundation. | ||
1308 | 11 | # | ||
1309 | 12 | # Checkbox is distributed in the hope that it will be useful, | ||
1310 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1311 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1312 | 15 | # GNU General Public License for more details. | ||
1313 | 16 | # | ||
1314 | 17 | # You should have received a copy of the GNU General Public License | ||
1315 | 18 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
1316 | 19 | |||
1317 | 20 | """ | ||
1318 | 21 | :mod:`plainbox.impl.launcher` -- launcher definition | ||
1319 | 22 | ================================================== | ||
1320 | 23 | """ | ||
1321 | 24 | |||
1322 | 25 | from gettext import gettext as _ | ||
1323 | 26 | import logging | ||
1324 | 27 | |||
1325 | 28 | from plainbox.impl.applogic import PlainBoxConfig | ||
1326 | 29 | from plainbox.impl.secure import config | ||
1327 | 30 | from plainbox.impl.session.assistant import SA_RESTARTABLE | ||
1328 | 31 | from plainbox.impl.session.assistant import get_all_sa_flags | ||
1329 | 32 | from plainbox.impl.session.assistant import get_known_sa_api_versions | ||
1330 | 33 | from plainbox.impl.transport import get_all_transports | ||
1331 | 34 | from plainbox.impl.transport import SECURE_ID_PATTERN | ||
1332 | 35 | |||
1333 | 36 | |||
1334 | 37 | logger = logging.getLogger("plainbox.launcher") | ||
1335 | 38 | |||
1336 | 39 | |||
1337 | 40 | class LauncherDefinition(PlainBoxConfig): | ||
1338 | 41 | """ | ||
1339 | 42 | Launcher definition. | ||
1340 | 43 | |||
1341 | 44 | Launchers are small executables using one of the available user interfaces | ||
1342 | 45 | as the interpreter. This class contains all the available options that can | ||
1343 | 46 | be set inside the launcher, that will affect the user interface at runtime. | ||
1344 | 47 | This generic launcher definition class helps to pick concrete version of | ||
1345 | 48 | the launcher definition. | ||
1346 | 49 | """ | ||
1347 | 50 | launcher_version = config.Variable( | ||
1348 | 51 | section="launcher", | ||
1349 | 52 | help_text=_("Version of launcher to use")) | ||
1350 | 53 | |||
1351 | 54 | config_filename = config.Variable( | ||
1352 | 55 | section="config", | ||
1353 | 56 | default="checkbox.conf", | ||
1354 | 57 | help_text=_("Name of custom configuration file")) | ||
1355 | 58 | |||
1356 | 59 | def get_concrete_launcher(self): | ||
1357 | 60 | """Create appropriate LauncherDefinition instance. | ||
1358 | 61 | |||
1359 | 62 | Depending on the value of launcher_version variable appropriate | ||
1360 | 63 | LauncherDefinition class is chosen and its instance returned. | ||
1361 | 64 | |||
1362 | 65 | :returns: LauncherDefinition instance | ||
1363 | 66 | :raises KeyError: for unknown launcher_version values | ||
1364 | 67 | """ | ||
1365 | 68 | return {'1': LauncherDefinition1}[self.launcher_version]() | ||
1366 | 69 | |||
1367 | 70 | |||
1368 | 71 | class LauncherDefinition1(LauncherDefinition): | ||
1369 | 72 | """ | ||
1370 | 73 | Definition for launchers version 1. | ||
1371 | 74 | |||
1372 | 75 | As specced in https://goo.gl/qJYtPX | ||
1373 | 76 | """ | ||
1374 | 77 | |||
1375 | 78 | def __init__(self): | ||
1376 | 79 | super().__init__() | ||
1377 | 80 | |||
1378 | 81 | launcher_version = config.Variable( | ||
1379 | 82 | section="launcher", | ||
1380 | 83 | default='1', | ||
1381 | 84 | help_text=_("Version of launcher to use")) | ||
1382 | 85 | |||
1383 | 86 | app_id = config.Variable( | ||
1384 | 87 | section='launcher', | ||
1385 | 88 | default='checkbox-cli', | ||
1386 | 89 | help_text=_('Identifier of the application')) | ||
1387 | 90 | |||
1388 | 91 | app_version = config.Variable( | ||
1389 | 92 | section='launcher', | ||
1390 | 93 | help_text=_('Version of the application')) | ||
1391 | 94 | |||
1392 | 95 | api_flags = config.Variable( | ||
1393 | 96 | section='launcher', | ||
1394 | 97 | kind=list, | ||
1395 | 98 | default=[SA_RESTARTABLE], | ||
1396 | 99 | validator_list=[config.SubsetValidator(get_all_sa_flags())], | ||
1397 | 100 | help_text=_('List of feature-flags the application requires')) | ||
1398 | 101 | |||
1399 | 102 | api_version = config.Variable( | ||
1400 | 103 | section='launcher', | ||
1401 | 104 | default='0.99', | ||
1402 | 105 | validator_list=[config.ChoiceValidator( | ||
1403 | 106 | get_known_sa_api_versions())], | ||
1404 | 107 | help_text=_('Version of API the launcher uses')) | ||
1405 | 108 | |||
1406 | 109 | stock_reports = config.Variable( | ||
1407 | 110 | section='launcher', | ||
1408 | 111 | kind=list, | ||
1409 | 112 | validator_list=[ | ||
1410 | 113 | config.SubsetValidator({ | ||
1411 | 114 | 'text', 'certification', 'certification-staging', | ||
1412 | 115 | 'submission_files', 'none'}), | ||
1413 | 116 | config.OneOrTheOtherValidator( | ||
1414 | 117 | {'none'}, {'text', 'certification', 'certification-staging', | ||
1415 | 118 | 'submission_files'}), | ||
1416 | 119 | ], | ||
1417 | 120 | default=['text', 'certification', 'submission_files'], | ||
1418 | 121 | help_text=_('List of stock reports to use')) | ||
1419 | 122 | |||
1420 | 123 | local_submission = config.Variable( | ||
1421 | 124 | section='launcher', | ||
1422 | 125 | kind=bool, | ||
1423 | 126 | default=True, | ||
1424 | 127 | help_text=_("Send/generate submission report locally when using " | ||
1425 | 128 | "checkbox remote")) | ||
1426 | 129 | |||
1427 | 130 | session_title = config.Variable( | ||
1428 | 131 | section='launcher', | ||
1429 | 132 | default='session title', | ||
1430 | 133 | help_text=_("A title to be applied to the sessions created using " | ||
1431 | 134 | "this launcher that can be used in report generation")) | ||
1432 | 135 | |||
1433 | 136 | session_desc = config.Variable( | ||
1434 | 137 | section='launcher', | ||
1435 | 138 | default='', | ||
1436 | 139 | help_text=_("A string that can be applied to sessions created using " | ||
1437 | 140 | "this launcher. Useful for storing some contextual " | ||
1438 | 141 | "infomation about the session")) | ||
1439 | 142 | |||
1440 | 143 | test_plan_filters = config.Variable( | ||
1441 | 144 | section='test plan', | ||
1442 | 145 | name='filter', | ||
1443 | 146 | default=['*'], | ||
1444 | 147 | kind=list, | ||
1445 | 148 | help_text=_('Constrain interactive choice to test plans matching this' | ||
1446 | 149 | 'glob')) | ||
1447 | 150 | |||
1448 | 151 | test_plan_default_selection = config.Variable( | ||
1449 | 152 | section='test plan', | ||
1450 | 153 | name='unit', | ||
1451 | 154 | help_text=_('Select this test plan by default.')) | ||
1452 | 155 | |||
1453 | 156 | test_plan_forced = config.Variable( | ||
1454 | 157 | section='test plan', | ||
1455 | 158 | name='forced', | ||
1456 | 159 | kind=bool, | ||
1457 | 160 | default=False, | ||
1458 | 161 | help_text=_("Don't allow the user to change test plan.")) | ||
1459 | 162 | |||
1460 | 163 | test_selection_forced = config.Variable( | ||
1461 | 164 | section='test selection', | ||
1462 | 165 | name='forced', | ||
1463 | 166 | kind=bool, | ||
1464 | 167 | default=False, | ||
1465 | 168 | help_text=_("Don't allow the user to alter test selection.")) | ||
1466 | 169 | |||
1467 | 170 | test_exclude = config.Variable( | ||
1468 | 171 | section='test selection', | ||
1469 | 172 | name='exclude', | ||
1470 | 173 | default=[], | ||
1471 | 174 | kind=list, | ||
1472 | 175 | help_text=_("Exclude test matching the patterns from running")) | ||
1473 | 176 | |||
1474 | 177 | ui_type = config.Variable( | ||
1475 | 178 | section='ui', | ||
1476 | 179 | name='type', | ||
1477 | 180 | default='interactive', | ||
1478 | 181 | validator_list=[config.ChoiceValidator( | ||
1479 | 182 | ['interactive', 'silent'])], | ||
1480 | 183 | help_text=_('Type of stock user interface to use.')) | ||
1481 | 184 | |||
1482 | 185 | output = config.Variable( | ||
1483 | 186 | section='ui', | ||
1484 | 187 | default='show', | ||
1485 | 188 | validator_list=[config.ChoiceValidator( | ||
1486 | 189 | ['show', 'hide', 'hide-resource-and-attachment', | ||
1487 | 190 | 'hide-automated'])], | ||
1488 | 191 | help_text=_('Silence or restrict command output')) | ||
1489 | 192 | |||
1490 | 193 | dont_suppress_output = config.Variable( | ||
1491 | 194 | section="ui", kind=bool, default=False, | ||
1492 | 195 | help_text=_("Don't suppress the output of certain job plugin types.")) | ||
1493 | 196 | |||
1494 | 197 | verbosity = config.Variable( | ||
1495 | 198 | section="ui", validator_list=[config.ChoiceValidator( | ||
1496 | 199 | ['normal', 'verbose', 'debug'])], help_text=_('Verbosity level'), | ||
1497 | 200 | default='normal') | ||
1498 | 201 | |||
1499 | 202 | auto_retry = config.Variable( | ||
1500 | 203 | section='ui', | ||
1501 | 204 | kind=bool, | ||
1502 | 205 | default=False, | ||
1503 | 206 | help_text=_("Automatically retry failed jobs at the end" | ||
1504 | 207 | " of the session.")) | ||
1505 | 208 | |||
1506 | 209 | max_attempts = config.Variable( | ||
1507 | 210 | section='ui', | ||
1508 | 211 | kind=int, | ||
1509 | 212 | default=3, | ||
1510 | 213 | help_text=_("Number of attempts to run a job when in auto-retry mode.")) | ||
1511 | 214 | |||
1512 | 215 | delay_before_retry = config.Variable( | ||
1513 | 216 | section='ui', | ||
1514 | 217 | kind=int, | ||
1515 | 218 | default=1, | ||
1516 | 219 | help_text=_("Delay (in seconds) before retrying failed jobs in" | ||
1517 | 220 | " auto-retry mode.")) | ||
1518 | 221 | |||
1519 | 222 | normal_user = config.Variable( | ||
1520 | 223 | section='daemon', | ||
1521 | 224 | kind=str, | ||
1522 | 225 | default='', | ||
1523 | 226 | help_text=_("Username to use for jobs that don't specify user")) | ||
1524 | 227 | |||
1525 | 228 | restart_strategy = config.Variable( | ||
1526 | 229 | section='restart', | ||
1527 | 230 | name='strategy', | ||
1528 | 231 | help_text=_('Use alternative restart strategy')) | ||
1529 | 232 | |||
1530 | 233 | restart = config.Section( | ||
1531 | 234 | help_text=_('Restart strategy parameters')) | ||
1532 | 235 | |||
1533 | 236 | reports = config.ParametricSection( | ||
1534 | 237 | name='report', | ||
1535 | 238 | help_text=_('Report declaration')) | ||
1536 | 239 | |||
1537 | 240 | exporters = config.ParametricSection( | ||
1538 | 241 | name='exporter', | ||
1539 | 242 | help_text=_('Exporter declaration')) | ||
1540 | 243 | |||
1541 | 244 | transports = config.ParametricSection( | ||
1542 | 245 | name='transport', | ||
1543 | 246 | help_text=_('Transport declaration')) | ||
1544 | 247 | |||
1545 | 248 | environment = config.Section( | ||
1546 | 249 | help_text=_('Environment variables to use')) | ||
1547 | 250 | |||
1548 | 251 | daemon = config.Section( | ||
1549 | 252 | name='daemon', | ||
1550 | 253 | help_text=_('Daemon-specific configuration')) | ||
1551 | 254 | |||
1552 | 255 | manifest = config.Section( | ||
1553 | 256 | help_text=_('Manifest entries to use')) | ||
1554 | 257 | |||
1555 | 258 | DefaultLauncherDefinition = LauncherDefinition1 | ||
1556 | diff --git a/plainbox/impl/runner.py b/plainbox/impl/runner.py | |||
1557 | index fae15fb..f46a99f 100644 | |||
1558 | --- a/plainbox/impl/runner.py | |||
1559 | +++ b/plainbox/impl/runner.py | |||
1560 | @@ -50,6 +50,7 @@ from plainbox.i18n import gettext as _ | |||
1561 | 50 | from plainbox.impl.result import IOLogRecord | 50 | from plainbox.impl.result import IOLogRecord |
1562 | 51 | from plainbox.impl.result import IOLogRecordWriter | 51 | from plainbox.impl.result import IOLogRecordWriter |
1563 | 52 | from plainbox.impl.result import JobResultBuilder | 52 | from plainbox.impl.result import JobResultBuilder |
1564 | 53 | from plainbox.impl.secure.config import Unset | ||
1565 | 53 | from plainbox.vendor import extcmd | 54 | from plainbox.vendor import extcmd |
1566 | 54 | from plainbox.vendor import morris | 55 | from plainbox.vendor import morris |
1567 | 55 | 56 | ||
1568 | diff --git a/plainbox/impl/secure/test_config.py b/plainbox/impl/secure/test_config.py | |||
1569 | 56 | new file mode 100644 | 57 | new file mode 100644 |
1570 | index 0000000..1efab12 | |||
1571 | --- /dev/null | |||
1572 | +++ b/plainbox/impl/secure/test_config.py | |||
1573 | @@ -0,0 +1,608 @@ | |||
1574 | 1 | # This file is part of Checkbox. | ||
1575 | 2 | # | ||
1576 | 3 | # Copyright 2013, 2014 Canonical Ltd. | ||
1577 | 4 | # Written by: | ||
1578 | 5 | # Zygmunt Krynicki <zygmunt.krynicki@canonical.com> | ||
1579 | 6 | # | ||
1580 | 7 | # Checkbox is free software: you can redistribute it and/or modify | ||
1581 | 8 | # it under the terms of the GNU General Public License version 3, | ||
1582 | 9 | # as published by the Free Software Foundation. | ||
1583 | 10 | |||
1584 | 11 | # | ||
1585 | 12 | # Checkbox is distributed in the hope that it will be useful, | ||
1586 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1587 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1588 | 15 | # GNU General Public License for more details. | ||
1589 | 16 | # | ||
1590 | 17 | # You should have received a copy of the GNU General Public License | ||
1591 | 18 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
1592 | 19 | |||
1593 | 20 | """ | ||
1594 | 21 | plainbox.impl.secure.test_config | ||
1595 | 22 | ================================ | ||
1596 | 23 | |||
1597 | 24 | Test definitions for plainbox.impl.secure.config module | ||
1598 | 25 | """ | ||
1599 | 26 | from io import StringIO | ||
1600 | 27 | from unittest import TestCase | ||
1601 | 28 | import configparser | ||
1602 | 29 | |||
1603 | 30 | from plainbox.impl.secure.config import ChoiceValidator | ||
1604 | 31 | from plainbox.impl.secure.config import ConfigMetaData | ||
1605 | 32 | from plainbox.impl.secure.config import KindValidator | ||
1606 | 33 | from plainbox.impl.secure.config import NotEmptyValidator | ||
1607 | 34 | from plainbox.impl.secure.config import NotUnsetValidator | ||
1608 | 35 | from plainbox.impl.secure.config import OneOrTheOtherValidator | ||
1609 | 36 | from plainbox.impl.secure.config import PatternValidator | ||
1610 | 37 | from plainbox.impl.secure.config import ParametricSection | ||
1611 | 38 | from plainbox.impl.secure.config import PlainBoxConfigParser, Config | ||
1612 | 39 | from plainbox.impl.secure.config import ValidationError | ||
1613 | 40 | from plainbox.impl.secure.config import Variable, Section, Unset | ||
1614 | 41 | from plainbox.impl.secure.config import understands_Unset | ||
1615 | 42 | from plainbox.vendor import mock | ||
1616 | 43 | |||
1617 | 44 | |||
1618 | 45 | class UnsetTests(TestCase): | ||
1619 | 46 | |||
1620 | 47 | def test_str(self): | ||
1621 | 48 | self.assertEqual(str(Unset), "unset") | ||
1622 | 49 | |||
1623 | 50 | def test_repr(self): | ||
1624 | 51 | self.assertEqual(repr(Unset), "Unset") | ||
1625 | 52 | |||
1626 | 53 | def test_bool(self): | ||
1627 | 54 | self.assertEqual(bool(Unset), False) | ||
1628 | 55 | |||
1629 | 56 | |||
1630 | 57 | class understands_Unset_Tests(TestCase): | ||
1631 | 58 | |||
1632 | 59 | def test_func(self): | ||
1633 | 60 | @understands_Unset | ||
1634 | 61 | def func(): | ||
1635 | 62 | pass | ||
1636 | 63 | |||
1637 | 64 | self.assertTrue(hasattr(func, 'understands_Unset')) | ||
1638 | 65 | self.assertTrue(getattr(func, 'understands_Unset')) | ||
1639 | 66 | |||
1640 | 67 | def test_cls(self): | ||
1641 | 68 | @understands_Unset | ||
1642 | 69 | class cls: | ||
1643 | 70 | pass | ||
1644 | 71 | |||
1645 | 72 | self.assertTrue(hasattr(cls, 'understands_Unset')) | ||
1646 | 73 | self.assertTrue(getattr(cls, 'understands_Unset')) | ||
1647 | 74 | |||
1648 | 75 | |||
1649 | 76 | class VariableTests(TestCase): | ||
1650 | 77 | |||
1651 | 78 | def test_name(self): | ||
1652 | 79 | v1 = Variable() | ||
1653 | 80 | self.assertIsNone(v1.name) | ||
1654 | 81 | v2 = Variable('var') | ||
1655 | 82 | self.assertEqual(v2.name, 'var') | ||
1656 | 83 | v3 = Variable(name='var') | ||
1657 | 84 | self.assertEqual(v3.name, 'var') | ||
1658 | 85 | |||
1659 | 86 | def test_section(self): | ||
1660 | 87 | v1 = Variable() | ||
1661 | 88 | self.assertEqual(v1.section, 'DEFAULT') | ||
1662 | 89 | v2 = Variable(section='foo') | ||
1663 | 90 | self.assertEqual(v2.section, 'foo') | ||
1664 | 91 | |||
1665 | 92 | def test_kind(self): | ||
1666 | 93 | v1 = Variable(kind=bool) | ||
1667 | 94 | self.assertIs(v1.kind, bool) | ||
1668 | 95 | v2 = Variable(kind=int) | ||
1669 | 96 | self.assertIs(v2.kind, int) | ||
1670 | 97 | v3 = Variable(kind=float) | ||
1671 | 98 | self.assertIs(v3.kind, float) | ||
1672 | 99 | v4 = Variable(kind=str) | ||
1673 | 100 | self.assertIs(v4.kind, str) | ||
1674 | 101 | v5 = Variable() | ||
1675 | 102 | self.assertIs(v5.kind, str) | ||
1676 | 103 | v6 = Variable(kind=list) | ||
1677 | 104 | self.assertIs(v6.kind, list) | ||
1678 | 105 | with self.assertRaises(ValueError): | ||
1679 | 106 | Variable(kind=dict) | ||
1680 | 107 | |||
1681 | 108 | def test_validator_list__default(self): | ||
1682 | 109 | """ | ||
1683 | 110 | verify that each Variable has a validator_list and that by default, | ||
1684 | 111 | that list contains a KindValidator as the first element | ||
1685 | 112 | """ | ||
1686 | 113 | self.assertEqual(Variable().validator_list, [KindValidator]) | ||
1687 | 114 | |||
1688 | 115 | def test_validator_list__explicit(self): | ||
1689 | 116 | """ | ||
1690 | 117 | verify that each Variable has a validator_list and that, if | ||
1691 | 118 | customized, the list contains the custom validators, preceded by | ||
1692 | 119 | the implicit KindValidator object | ||
1693 | 120 | """ | ||
1694 | 121 | def DummyValidator(variable, new_value): | ||
1695 | 122 | """ Dummy validator for the test below""" | ||
1696 | 123 | pass | ||
1697 | 124 | var = Variable(validator_list=[DummyValidator]) | ||
1698 | 125 | self.assertEqual(var.validator_list, [KindValidator, DummyValidator]) | ||
1699 | 126 | |||
1700 | 127 | def test_validator_list__with_NotUnsetValidator(self): | ||
1701 | 128 | """ | ||
1702 | 129 | verify that each Variable has a validator_list and that, if | ||
1703 | 130 | customized, and if using NotUnsetValidator it will take precedence | ||
1704 | 131 | over all other validators, including the implicit KindValidator | ||
1705 | 132 | """ | ||
1706 | 133 | var = Variable(validator_list=[NotUnsetValidator()]) | ||
1707 | 134 | self.assertEqual( | ||
1708 | 135 | var.validator_list, [NotUnsetValidator(), KindValidator]) | ||
1709 | 136 | |||
1710 | 137 | |||
1711 | 138 | class SectionTests(TestCase): | ||
1712 | 139 | |||
1713 | 140 | def test_name(self): | ||
1714 | 141 | s1 = Section() | ||
1715 | 142 | self.assertIsNone(s1.name) | ||
1716 | 143 | s2 = Section('sec') | ||
1717 | 144 | self.assertEqual(s2.name, 'sec') | ||
1718 | 145 | s3 = Variable(name='sec') | ||
1719 | 146 | self.assertEqual(s3.name, 'sec') | ||
1720 | 147 | |||
1721 | 148 | |||
1722 | 149 | class ConfigTests(TestCase): | ||
1723 | 150 | |||
1724 | 151 | def test_Meta_present(self): | ||
1725 | 152 | class TestConfig(Config): | ||
1726 | 153 | pass | ||
1727 | 154 | self.assertTrue(hasattr(TestConfig, 'Meta')) | ||
1728 | 155 | |||
1729 | 156 | def test_Meta_base_cls(self): | ||
1730 | 157 | class TestConfig(Config): | ||
1731 | 158 | pass | ||
1732 | 159 | self.assertTrue(issubclass(TestConfig.Meta, ConfigMetaData)) | ||
1733 | 160 | |||
1734 | 161 | class HelperMeta: | ||
1735 | 162 | pass | ||
1736 | 163 | |||
1737 | 164 | class TestConfigWMeta(Config): | ||
1738 | 165 | Meta = HelperMeta | ||
1739 | 166 | self.assertTrue(issubclass(TestConfigWMeta.Meta, ConfigMetaData)) | ||
1740 | 167 | self.assertTrue(issubclass(TestConfigWMeta.Meta, HelperMeta)) | ||
1741 | 168 | |||
1742 | 169 | def test_Meta_variable_list(self): | ||
1743 | 170 | class TestConfig(Config): | ||
1744 | 171 | v1 = Variable() | ||
1745 | 172 | v2 = Variable() | ||
1746 | 173 | self.assertEqual( | ||
1747 | 174 | TestConfig.Meta.variable_list, | ||
1748 | 175 | [TestConfig.v1, TestConfig.v2]) | ||
1749 | 176 | |||
1750 | 177 | def test_variable_smoke(self): | ||
1751 | 178 | class TestConfig(Config): | ||
1752 | 179 | v = Variable() | ||
1753 | 180 | conf = TestConfig() | ||
1754 | 181 | self.assertIs(conf.v, Unset) | ||
1755 | 182 | conf.v = "value" | ||
1756 | 183 | self.assertEqual(conf.v, "value") | ||
1757 | 184 | del conf.v | ||
1758 | 185 | self.assertIs(conf.v, Unset) | ||
1759 | 186 | |||
1760 | 187 | def _get_featureful_config(self): | ||
1761 | 188 | # define a featureful config class | ||
1762 | 189 | class TestConfig(Config): | ||
1763 | 190 | v1 = Variable() | ||
1764 | 191 | v2 = Variable(section="v23_section") | ||
1765 | 192 | v3 = Variable(section="v23_section") | ||
1766 | 193 | v_unset = Variable() | ||
1767 | 194 | v_bool = Variable(section="type_section", kind=bool) | ||
1768 | 195 | v_int = Variable(section="type_section", kind=int) | ||
1769 | 196 | v_float = Variable(section="type_section", kind=float) | ||
1770 | 197 | v_list = Variable(section="type_section", kind=list) | ||
1771 | 198 | v_str = Variable(section="type_section", kind=str) | ||
1772 | 199 | s = Section() | ||
1773 | 200 | ps = ParametricSection() | ||
1774 | 201 | conf = TestConfig() | ||
1775 | 202 | # assign value to each variable, except v3_unset | ||
1776 | 203 | conf.v1 = "v1 value" | ||
1777 | 204 | conf.v2 = "v2 value" | ||
1778 | 205 | conf.v3 = "v3 value" | ||
1779 | 206 | conf.v_bool = True | ||
1780 | 207 | conf.v_int = -7 | ||
1781 | 208 | conf.v_float = 1.5 | ||
1782 | 209 | conf.v_str = "hi" | ||
1783 | 210 | conf.v_list = ['foo', 'bar'] | ||
1784 | 211 | # assign value to the section | ||
1785 | 212 | conf.s = {"a": 1, "b": 2} | ||
1786 | 213 | conf.ps = {"foo": {"c": 3, "d": 4}} | ||
1787 | 214 | return conf | ||
1788 | 215 | |||
1789 | 216 | def test_get_parser_obj(self): | ||
1790 | 217 | """ | ||
1791 | 218 | verify that Config.get_parser_obj() properly writes all the data to the | ||
1792 | 219 | ConfigParser object. | ||
1793 | 220 | """ | ||
1794 | 221 | conf = self._get_featureful_config() | ||
1795 | 222 | parser = conf.get_parser_obj() | ||
1796 | 223 | # verify that section and section-less variables work | ||
1797 | 224 | self.assertEqual(parser.get("DEFAULT", "v1"), "v1 value") | ||
1798 | 225 | self.assertEqual(parser.get("v23_section", "v2"), "v2 value") | ||
1799 | 226 | self.assertEqual(parser.get("v23_section", "v3"), "v3 value") | ||
1800 | 227 | # verify that unset variable is not getting set to anything | ||
1801 | 228 | with self.assertRaises(configparser.Error): | ||
1802 | 229 | parser.get("DEFAULT", "v_unset") | ||
1803 | 230 | # verify that various types got converted correctly and still resolve | ||
1804 | 231 | # to correct typed values | ||
1805 | 232 | self.assertEqual(parser.get("type_section", "v_bool"), "True") | ||
1806 | 233 | self.assertEqual(parser.getboolean("type_section", "v_bool"), True) | ||
1807 | 234 | self.assertEqual(parser.get("type_section", "v_int"), "-7") | ||
1808 | 235 | self.assertEqual(parser.getint("type_section", "v_int"), -7) | ||
1809 | 236 | self.assertEqual(parser.get("type_section", "v_float"), "1.5") | ||
1810 | 237 | self.assertEqual(parser.getfloat("type_section", "v_float"), 1.5) | ||
1811 | 238 | self.assertEqual(parser.get("type_section", "v_str"), "hi") | ||
1812 | 239 | # verify that section work okay | ||
1813 | 240 | self.assertEqual(parser.get("s", "a"), "1") | ||
1814 | 241 | self.assertEqual(parser.get("s", "b"), "2") | ||
1815 | 242 | # verify that parametric section works okay | ||
1816 | 243 | self.assertEqual(parser.get("ps:foo", "c"), "3") | ||
1817 | 244 | self.assertEqual(parser.get("ps:foo", "d"), "4") | ||
1818 | 245 | |||
1819 | 246 | def test_write(self): | ||
1820 | 247 | """ | ||
1821 | 248 | verify that Config.write() works | ||
1822 | 249 | """ | ||
1823 | 250 | conf = self._get_featureful_config() | ||
1824 | 251 | with StringIO() as stream: | ||
1825 | 252 | conf.write(stream) | ||
1826 | 253 | self.assertEqual(stream.getvalue(), ( | ||
1827 | 254 | "[DEFAULT]\n" | ||
1828 | 255 | "v1 = v1 value\n" | ||
1829 | 256 | "\n" | ||
1830 | 257 | "[v23_section]\n" | ||
1831 | 258 | "v2 = v2 value\n" | ||
1832 | 259 | "v3 = v3 value\n" | ||
1833 | 260 | "\n" | ||
1834 | 261 | "[type_section]\n" | ||
1835 | 262 | "v_bool = True\n" | ||
1836 | 263 | "v_float = 1.5\n" | ||
1837 | 264 | "v_int = -7\n" | ||
1838 | 265 | "v_list = foo, bar\n" | ||
1839 | 266 | "v_str = hi\n" | ||
1840 | 267 | "\n" | ||
1841 | 268 | "[s]\n" | ||
1842 | 269 | "a = 1\n" | ||
1843 | 270 | "b = 2\n" | ||
1844 | 271 | "\n" | ||
1845 | 272 | "[ps:foo]\n" | ||
1846 | 273 | "c = 3\n" | ||
1847 | 274 | "d = 4\n" | ||
1848 | 275 | "\n")) | ||
1849 | 276 | |||
1850 | 277 | def test_section_smoke(self): | ||
1851 | 278 | class TestConfig(Config): | ||
1852 | 279 | s = Section() | ||
1853 | 280 | conf = TestConfig() | ||
1854 | 281 | self.assertIs(conf.s, Unset) | ||
1855 | 282 | with self.assertRaises(TypeError): | ||
1856 | 283 | conf.s['key'] = "key-value" | ||
1857 | 284 | conf.s = {} | ||
1858 | 285 | self.assertEqual(conf.s, {}) | ||
1859 | 286 | conf.s['key'] = "key-value" | ||
1860 | 287 | self.assertEqual(conf.s['key'], "key-value") | ||
1861 | 288 | del conf.s | ||
1862 | 289 | self.assertIs(conf.s, Unset) | ||
1863 | 290 | |||
1864 | 291 | def test_read_string(self): | ||
1865 | 292 | class TestConfig(Config): | ||
1866 | 293 | v = Variable() | ||
1867 | 294 | conf = TestConfig() | ||
1868 | 295 | conf.read_string( | ||
1869 | 296 | "[DEFAULT]\n" | ||
1870 | 297 | "v = 1") | ||
1871 | 298 | self.assertEqual(conf.v, "1") | ||
1872 | 299 | self.assertEqual(len(conf.problem_list), 0) | ||
1873 | 300 | |||
1874 | 301 | def test_read_list_with_spaces(self): | ||
1875 | 302 | class TestConfig(Config): | ||
1876 | 303 | l = Variable(kind=list) | ||
1877 | 304 | conf = TestConfig() | ||
1878 | 305 | conf.read_string('[DEFAULT]\nl = foo bar') | ||
1879 | 306 | self.assertEqual(conf.l, ['foo', 'bar']) | ||
1880 | 307 | self.assertEqual(len(conf.problem_list), 0) | ||
1881 | 308 | |||
1882 | 309 | def test_read_list_with_commas(self): | ||
1883 | 310 | class TestConfig(Config): | ||
1884 | 311 | l = Variable(kind=list) | ||
1885 | 312 | conf = TestConfig() | ||
1886 | 313 | conf.read_string('[DEFAULT]\nl = foo,bar') | ||
1887 | 314 | self.assertEqual(conf.l, ['foo', 'bar']) | ||
1888 | 315 | self.assertEqual(len(conf.problem_list), 0) | ||
1889 | 316 | |||
1890 | 317 | def test_read_list_quoted_strings(self): | ||
1891 | 318 | class TestConfig(Config): | ||
1892 | 319 | l = Variable(kind=list) | ||
1893 | 320 | conf = TestConfig() | ||
1894 | 321 | conf.read_string('[DEFAULT]\nl = foo "bar baz"') | ||
1895 | 322 | self.assertEqual(conf.l, ['foo', 'bar baz']) | ||
1896 | 323 | self.assertEqual(len(conf.problem_list), 0) | ||
1897 | 324 | |||
1898 | 325 | def test_read_string_calls_validate_whole(self): | ||
1899 | 326 | """ | ||
1900 | 327 | verify that Config.read_string() calls validate_whole()" | ||
1901 | 328 | """ | ||
1902 | 329 | conf = Config() | ||
1903 | 330 | with mock.patch.object(conf, 'validate_whole') as mocked_validate: | ||
1904 | 331 | conf.read_string('') | ||
1905 | 332 | mocked_validate.assert_called_once_with() | ||
1906 | 333 | |||
1907 | 334 | def test_read_calls_validate_whole(self): | ||
1908 | 335 | """ | ||
1909 | 336 | verify that Config.read() calls validate_whole()" | ||
1910 | 337 | """ | ||
1911 | 338 | conf = Config() | ||
1912 | 339 | with mock.patch.object(conf, 'validate_whole') as mocked_validate: | ||
1913 | 340 | conf.read([]) | ||
1914 | 341 | mocked_validate.assert_called_once_with() | ||
1915 | 342 | |||
1916 | 343 | def test_read__handles_errors_from_validate_whole(self): | ||
1917 | 344 | """ | ||
1918 | 345 | verify that Config.read() collects errors from validate_whole()". | ||
1919 | 346 | """ | ||
1920 | 347 | class TestConfig(Config): | ||
1921 | 348 | v = Variable() | ||
1922 | 349 | |||
1923 | 350 | def validate_whole(self): | ||
1924 | 351 | raise ValidationError(TestConfig.v, self.v, "v is evil") | ||
1925 | 352 | conf = TestConfig() | ||
1926 | 353 | conf.read([]) | ||
1927 | 354 | self.assertEqual(len(conf.problem_list), 1) | ||
1928 | 355 | self.assertEqual(conf.problem_list[0].variable, TestConfig.v) | ||
1929 | 356 | self.assertEqual(conf.problem_list[0].new_value, Unset) | ||
1930 | 357 | self.assertEqual(conf.problem_list[0].message, "v is evil") | ||
1931 | 358 | |||
1932 | 359 | def test_read_string__does_not_ignore_nonmentioned_variables(self): | ||
1933 | 360 | class TestConfig(Config): | ||
1934 | 361 | v = Variable(validator_list=[NotUnsetValidator()]) | ||
1935 | 362 | conf = TestConfig() | ||
1936 | 363 | conf.read_string("") | ||
1937 | 364 | # Because Unset is the default, sadly | ||
1938 | 365 | self.assertEqual(conf.v, Unset) | ||
1939 | 366 | # But there was a problem noticed | ||
1940 | 367 | self.assertEqual(len(conf.problem_list), 1) | ||
1941 | 368 | self.assertEqual(conf.problem_list[0].variable, TestConfig.v) | ||
1942 | 369 | self.assertEqual(conf.problem_list[0].new_value, Unset) | ||
1943 | 370 | self.assertEqual(conf.problem_list[0].message, | ||
1944 | 371 | "must be set to something") | ||
1945 | 372 | |||
1946 | 373 | def test_read_string__handles_errors_from_validate_whole(self): | ||
1947 | 374 | """ | ||
1948 | 375 | verify that Config.read_strig() collects errors from validate_whole()". | ||
1949 | 376 | """ | ||
1950 | 377 | class TestConfig(Config): | ||
1951 | 378 | v = Variable() | ||
1952 | 379 | |||
1953 | 380 | def validate_whole(self): | ||
1954 | 381 | raise ValidationError(TestConfig.v, self.v, "v is evil") | ||
1955 | 382 | conf = TestConfig() | ||
1956 | 383 | conf.read_string("") | ||
1957 | 384 | self.assertEqual(len(conf.problem_list), 1) | ||
1958 | 385 | self.assertEqual(conf.problem_list[0].variable, TestConfig.v) | ||
1959 | 386 | self.assertEqual(conf.problem_list[0].new_value, Unset) | ||
1960 | 387 | self.assertEqual(conf.problem_list[0].message, "v is evil") | ||
1961 | 388 | |||
1962 | 389 | |||
1963 | 390 | class ConfigMetaDataTests(TestCase): | ||
1964 | 391 | |||
1965 | 392 | def test_filename_list(self): | ||
1966 | 393 | self.assertEqual(ConfigMetaData.filename_list, []) | ||
1967 | 394 | |||
1968 | 395 | def test_variable_list(self): | ||
1969 | 396 | self.assertEqual(ConfigMetaData.variable_list, []) | ||
1970 | 397 | |||
1971 | 398 | def test_section_list(self): | ||
1972 | 399 | self.assertEqual(ConfigMetaData.section_list, []) | ||
1973 | 400 | |||
1974 | 401 | def test_parametric_section_list(self): | ||
1975 | 402 | self.assertEqual(ConfigMetaData.parametric_section_list, []) | ||
1976 | 403 | |||
1977 | 404 | |||
1978 | 405 | class PlainBoxConfigParserTest(TestCase): | ||
1979 | 406 | |||
1980 | 407 | def test_parser(self): | ||
1981 | 408 | conf_file = StringIO("[testsection]\nlower = low\nUPPER = up") | ||
1982 | 409 | config = PlainBoxConfigParser() | ||
1983 | 410 | config.read_file(conf_file) | ||
1984 | 411 | |||
1985 | 412 | self.assertEqual(['testsection'], config.sections()) | ||
1986 | 413 | all_keys = list(config['testsection'].keys()) | ||
1987 | 414 | self.assertTrue('lower' in all_keys) | ||
1988 | 415 | self.assertTrue('UPPER' in all_keys) | ||
1989 | 416 | self.assertFalse('upper' in all_keys) | ||
1990 | 417 | |||
1991 | 418 | def test_parametric_sections_parsing(self): | ||
1992 | 419 | class TestConfig(Config): | ||
1993 | 420 | ps = ParametricSection() | ||
1994 | 421 | conf_str = "[ps:foo]\nval = baz\n[ps:bar]\nvar = biz" | ||
1995 | 422 | config = TestConfig() | ||
1996 | 423 | config.read_string(conf_str) | ||
1997 | 424 | self.assertEqual( | ||
1998 | 425 | config.ps, | ||
1999 | 426 | {'foo': {'val': 'baz'}, 'bar': {'var': 'biz'}}) | ||
2000 | 427 | |||
2001 | 428 | |||
2002 | 429 | class KindValidatorTests(TestCase): | ||
2003 | 430 | |||
2004 | 431 | class _Config(Config): | ||
2005 | 432 | var_bool = Variable(kind=bool) | ||
2006 | 433 | var_int = Variable(kind=int) | ||
2007 | 434 | var_float = Variable(kind=float) | ||
2008 | 435 | var_str = Variable(kind=str) | ||
2009 | 436 | |||
2010 | 437 | def test_error_msg(self): | ||
2011 | 438 | """ | ||
2012 | 439 | verify that KindValidator() has correct error message for each type | ||
2013 | 440 | """ | ||
2014 | 441 | bad_value = object() | ||
2015 | 442 | self.assertEqual( | ||
2016 | 443 | KindValidator(self._Config.var_bool, bad_value), | ||
2017 | 444 | "expected a boolean") | ||
2018 | 445 | self.assertEqual( | ||
2019 | 446 | KindValidator(self._Config.var_int, bad_value), | ||
2020 | 447 | "expected an integer") | ||
2021 | 448 | self.assertEqual( | ||
2022 | 449 | KindValidator(self._Config.var_float, bad_value), | ||
2023 | 450 | "expected a floating point number") | ||
2024 | 451 | self.assertEqual( | ||
2025 | 452 | KindValidator(self._Config.var_str, bad_value), | ||
2026 | 453 | "expected a string") | ||
2027 | 454 | |||
2028 | 455 | |||
2029 | 456 | class PatternValidatorTests(TestCase): | ||
2030 | 457 | |||
2031 | 458 | class _Config(Config): | ||
2032 | 459 | var = Variable() | ||
2033 | 460 | |||
2034 | 461 | def test_smoke(self): | ||
2035 | 462 | """ | ||
2036 | 463 | verify that PatternValidator works as intended | ||
2037 | 464 | """ | ||
2038 | 465 | validator = PatternValidator("foo.+") | ||
2039 | 466 | self.assertEqual(validator(self._Config.var, "foobar"), None) | ||
2040 | 467 | self.assertEqual( | ||
2041 | 468 | validator(self._Config.var, "foo"), | ||
2042 | 469 | "does not match pattern: 'foo.+'") | ||
2043 | 470 | |||
2044 | 471 | def test_comparison_works(self): | ||
2045 | 472 | self.assertTrue(PatternValidator('foo') == PatternValidator('foo')) | ||
2046 | 473 | self.assertTrue(PatternValidator('foo') != PatternValidator('bar')) | ||
2047 | 474 | self.assertTrue(PatternValidator('foo') != object()) | ||
2048 | 475 | |||
2049 | 476 | |||
2050 | 477 | class ChoiceValidatorTests(TestCase): | ||
2051 | 478 | |||
2052 | 479 | class _Config(Config): | ||
2053 | 480 | var = Variable() | ||
2054 | 481 | |||
2055 | 482 | def test_smoke(self): | ||
2056 | 483 | """ | ||
2057 | 484 | verify that ChoiceValidator works as intended | ||
2058 | 485 | """ | ||
2059 | 486 | validator = ChoiceValidator(["foo", "bar"]) | ||
2060 | 487 | self.assertEqual(validator(self._Config.var, "foo"), None) | ||
2061 | 488 | self.assertEqual( | ||
2062 | 489 | validator(self._Config.var, "omg"), | ||
2063 | 490 | "var must be one of foo, bar. Got 'omg'") | ||
2064 | 491 | |||
2065 | 492 | def test_comparison_works(self): | ||
2066 | 493 | self.assertTrue(ChoiceValidator(["a"]) == ChoiceValidator(["a"])) | ||
2067 | 494 | self.assertTrue(ChoiceValidator(["a"]) != ChoiceValidator(["b"])) | ||
2068 | 495 | self.assertTrue(ChoiceValidator(["a"]) != object()) | ||
2069 | 496 | |||
2070 | 497 | |||
2071 | 498 | class NotUnsetValidatorTests(TestCase): | ||
2072 | 499 | """ | ||
2073 | 500 | Tests for the NotUnsetValidator class | ||
2074 | 501 | """ | ||
2075 | 502 | |||
2076 | 503 | class _Config(Config): | ||
2077 | 504 | var = Variable() | ||
2078 | 505 | |||
2079 | 506 | def test_understands_Unset(self): | ||
2080 | 507 | """ | ||
2081 | 508 | verify that Unset can be handled at all | ||
2082 | 509 | """ | ||
2083 | 510 | self.assertTrue(getattr(NotUnsetValidator, "understands_Unset")) | ||
2084 | 511 | |||
2085 | 512 | def test_rejects_unset_values(self): | ||
2086 | 513 | """ | ||
2087 | 514 | verify that Unset variables are rejected | ||
2088 | 515 | """ | ||
2089 | 516 | validator = NotUnsetValidator() | ||
2090 | 517 | self.assertEqual( | ||
2091 | 518 | validator(self._Config.var, Unset), "must be set to something") | ||
2092 | 519 | |||
2093 | 520 | def test_accepts_other_values(self): | ||
2094 | 521 | """ | ||
2095 | 522 | verify that other values are accepted | ||
2096 | 523 | """ | ||
2097 | 524 | validator = NotUnsetValidator() | ||
2098 | 525 | self.assertIsNone(validator(self._Config.var, None)) | ||
2099 | 526 | self.assertIsNone(validator(self._Config.var, "string")) | ||
2100 | 527 | self.assertIsNone(validator(self._Config.var, 15)) | ||
2101 | 528 | |||
2102 | 529 | def test_supports_custom_message(self): | ||
2103 | 530 | """ | ||
2104 | 531 | verify that custom message is used | ||
2105 | 532 | """ | ||
2106 | 533 | validator = NotUnsetValidator("value required!") | ||
2107 | 534 | self.assertEqual( | ||
2108 | 535 | validator(self._Config.var, Unset), "value required!") | ||
2109 | 536 | |||
2110 | 537 | def test_comparison_works(self): | ||
2111 | 538 | """ | ||
2112 | 539 | verify that comparison works as expected | ||
2113 | 540 | """ | ||
2114 | 541 | self.assertTrue(NotUnsetValidator() == NotUnsetValidator()) | ||
2115 | 542 | self.assertTrue(NotUnsetValidator("?") == NotUnsetValidator("?")) | ||
2116 | 543 | self.assertTrue(NotUnsetValidator() != NotUnsetValidator("?")) | ||
2117 | 544 | self.assertTrue(NotUnsetValidator() != object()) | ||
2118 | 545 | |||
2119 | 546 | |||
2120 | 547 | class NotEmptyValidatorTests(TestCase): | ||
2121 | 548 | |||
2122 | 549 | class _Config(Config): | ||
2123 | 550 | var = Variable() | ||
2124 | 551 | |||
2125 | 552 | def test_rejects_empty_values(self): | ||
2126 | 553 | validator = NotEmptyValidator() | ||
2127 | 554 | self.assertEqual(validator(self._Config.var, ""), "cannot be empty") | ||
2128 | 555 | |||
2129 | 556 | def test_supports_custom_message(self): | ||
2130 | 557 | validator = NotEmptyValidator("name required!") | ||
2131 | 558 | self.assertEqual(validator(self._Config.var, ""), "name required!") | ||
2132 | 559 | |||
2133 | 560 | def test_isnt_broken(self): | ||
2134 | 561 | validator = NotEmptyValidator() | ||
2135 | 562 | self.assertEqual(validator(self._Config.var, "some value"), None) | ||
2136 | 563 | |||
2137 | 564 | def test_comparison_works(self): | ||
2138 | 565 | self.assertTrue(NotEmptyValidator() == NotEmptyValidator()) | ||
2139 | 566 | self.assertTrue(NotEmptyValidator("?") == NotEmptyValidator("?")) | ||
2140 | 567 | self.assertTrue(NotEmptyValidator() != NotEmptyValidator("?")) | ||
2141 | 568 | self.assertTrue(NotEmptyValidator() != object()) | ||
2142 | 569 | |||
2143 | 570 | |||
2144 | 571 | class OneOrTheOtherValidatorTests(TestCase): | ||
2145 | 572 | |||
2146 | 573 | class _Config(Config): | ||
2147 | 574 | var = Variable("The Name", kind=list) | ||
2148 | 575 | |||
2149 | 576 | def test_pass_validation(self): | ||
2150 | 577 | validator = OneOrTheOtherValidator({'foo'}, {'bar'}) | ||
2151 | 578 | value = ['foo'] | ||
2152 | 579 | self.assertIsNone(validator(self._Config.var, value)) | ||
2153 | 580 | value = ['bar'] | ||
2154 | 581 | self.assertIsNone(validator(self._Config.var, value)) | ||
2155 | 582 | |||
2156 | 583 | def test_fail_validation(self): | ||
2157 | 584 | validator = OneOrTheOtherValidator({'foo'}, {'bar'}) | ||
2158 | 585 | value = ['foo', 'bar'] | ||
2159 | 586 | self.assertEquals( | ||
2160 | 587 | validator(self._Config.var, value), | ||
2161 | 588 | "The Name can only use values from {'foo'} or from {'bar'}") | ||
2162 | 589 | |||
2163 | 590 | def test_pass_empty(self): | ||
2164 | 591 | validator = OneOrTheOtherValidator({'foo'}, {'bar'}) | ||
2165 | 592 | value = [] | ||
2166 | 593 | self.assertIsNone(validator(self._Config.var, value)) | ||
2167 | 594 | |||
2168 | 595 | def test_comparison_works(self): | ||
2169 | 596 | self.assertEqual( | ||
2170 | 597 | OneOrTheOtherValidator({'foo'}, {'bar'}), | ||
2171 | 598 | OneOrTheOtherValidator({'foo'}, {'bar'}) | ||
2172 | 599 | ) | ||
2173 | 600 | self.assertEqual( | ||
2174 | 601 | OneOrTheOtherValidator({1, 2}, {3, 4}), | ||
2175 | 602 | OneOrTheOtherValidator({2, 1}, {4, 3}) | ||
2176 | 603 | ) | ||
2177 | 604 | self.assertNotEqual( | ||
2178 | 605 | OneOrTheOtherValidator({1}, {2}), | ||
2179 | 606 | OneOrTheOtherValidator({1}, {'foo'}) | ||
2180 | 607 | ) | ||
2181 | 608 | self.assertNotEqual(OneOrTheOtherValidator({1}, {2}), object()) | ||
2182 | diff --git a/plainbox/impl/session/assistant.py b/plainbox/impl/session/assistant.py | |||
2183 | index fa852c8..41ec411 100644 | |||
2184 | --- a/plainbox/impl/session/assistant.py | |||
2185 | +++ b/plainbox/impl/session/assistant.py | |||
2186 | @@ -38,15 +38,16 @@ from plainbox.abc import IJobResult | |||
2187 | 38 | from plainbox.abc import IJobRunnerUI | 38 | from plainbox.abc import IJobRunnerUI |
2188 | 39 | from plainbox.abc import ISessionStateTransport | 39 | from plainbox.abc import ISessionStateTransport |
2189 | 40 | from plainbox.i18n import gettext as _ | 40 | from plainbox.i18n import gettext as _ |
2190 | 41 | from plainbox.impl.applogic import PlainBoxConfig | ||
2191 | 41 | from plainbox.impl.decorators import raises | 42 | from plainbox.impl.decorators import raises |
2192 | 42 | from plainbox.impl.developer import UnexpectedMethodCall | 43 | from plainbox.impl.developer import UnexpectedMethodCall |
2193 | 43 | from plainbox.impl.developer import UsageExpectation | 44 | from plainbox.impl.developer import UsageExpectation |
2194 | 44 | from plainbox.impl.execution import UnifiedRunner | 45 | from plainbox.impl.execution import UnifiedRunner |
2195 | 45 | from plainbox.impl.config import Configuration | ||
2196 | 46 | from plainbox.impl.providers import get_providers | 46 | from plainbox.impl.providers import get_providers |
2197 | 47 | from plainbox.impl.result import JobResultBuilder | 47 | from plainbox.impl.result import JobResultBuilder |
2198 | 48 | from plainbox.impl.result import MemoryJobResult | 48 | from plainbox.impl.result import MemoryJobResult |
2199 | 49 | from plainbox.impl.runner import JobRunnerUIDelegate | 49 | from plainbox.impl.runner import JobRunnerUIDelegate |
2200 | 50 | from plainbox.impl.secure.config import Unset | ||
2201 | 50 | from plainbox.impl.secure.origin import Origin | 51 | from plainbox.impl.secure.origin import Origin |
2202 | 51 | from plainbox.impl.secure.qualifiers import select_jobs | 52 | from plainbox.impl.secure.qualifiers import select_jobs |
2203 | 52 | from plainbox.impl.secure.qualifiers import FieldQualifier | 53 | from plainbox.impl.secure.qualifiers import FieldQualifier |
2204 | @@ -161,7 +162,7 @@ class SessionAssistant: | |||
2205 | 161 | self._app_version = app_version | 162 | self._app_version = app_version |
2206 | 162 | self._api_version = api_version | 163 | self._api_version = api_version |
2207 | 163 | self._api_flags = api_flags | 164 | self._api_flags = api_flags |
2209 | 164 | self._config = Configuration() | 165 | self._config = PlainBoxConfig().get() |
2210 | 165 | Unit.config = self._config | 166 | Unit.config = self._config |
2211 | 166 | self._execution_ctrl_list = None # None is "default" | 167 | self._execution_ctrl_list = None # None is "default" |
2212 | 167 | self._ctrl_setup_list = [] | 168 | self._ctrl_setup_list = [] |
2213 | @@ -210,8 +211,7 @@ class SessionAssistant: | |||
2214 | 210 | 211 | ||
2215 | 211 | @raises(UnexpectedMethodCall, LookupError) | 212 | @raises(UnexpectedMethodCall, LookupError) |
2216 | 212 | def configure_application_restart( | 213 | def configure_application_restart( |
2219 | 213 | self, cmd_callback, session_type: | 214 | self, cmd_callback: 'Callable[[str], List[str]]') -> None: |
2218 | 214 | 'Callable[[str], List[str]], str') -> None: | ||
2220 | 215 | """ | 215 | """ |
2221 | 216 | Configure automatic restart capability. | 216 | Configure automatic restart capability. |
2222 | 217 | 217 | ||
2223 | @@ -219,8 +219,6 @@ class SessionAssistant: | |||
2224 | 219 | A callable (function or lambda) that when called with a single | 219 | A callable (function or lambda) that when called with a single |
2225 | 220 | string argument, session_id, returns a list of strings describing | 220 | string argument, session_id, returns a list of strings describing |
2226 | 221 | how to execute the tool in order to restart a particular session. | 221 | how to execute the tool in order to restart a particular session. |
2227 | 222 | :param session_type: | ||
2228 | 223 | Kind of the session we're running. Either 'local' or 'remote' | ||
2229 | 224 | :raises UnexpectedMethodCall: | 222 | :raises UnexpectedMethodCall: |
2230 | 225 | If the call is made at an unexpected time. Do not catch this error. | 223 | If the call is made at an unexpected time. Do not catch this error. |
2231 | 226 | It is a bug in your program. The error message will indicate what | 224 | It is a bug in your program. The error message will indicate what |
2232 | @@ -247,6 +245,25 @@ class SessionAssistant: | |||
2233 | 247 | """ | 245 | """ |
2234 | 248 | UsageExpectation.of(self).enforce() | 246 | UsageExpectation.of(self).enforce() |
2235 | 249 | if self._restart_strategy is None: | 247 | if self._restart_strategy is None: |
2236 | 248 | # 'checkbox-slave' is deprecated, it's here so people can resume | ||
2237 | 249 | # old session, the next if statement can be changed to just checking | ||
2238 | 250 | # for 'remote' type | ||
2239 | 251 | # session_type = 'remote' if self._metadata.title == 'remote' | ||
2240 | 252 | # else 'local' | ||
2241 | 253 | # with the next release or when we do inclusive naming refactor | ||
2242 | 254 | # or roughly after April of 2022 | ||
2243 | 255 | # TODO: REMOTE API RAPI: | ||
2244 | 256 | # this heuristic of guessing session type from the title | ||
2245 | 257 | # should be changed to a proper arg/flag with the Remote API bump | ||
2246 | 258 | remote_types = ('remote', 'checkbox-slave') | ||
2247 | 259 | session_type = 'local' | ||
2248 | 260 | try: | ||
2249 | 261 | app_blob = json.loads(self._metadata.app_blob.decode("UTF-8")) | ||
2250 | 262 | session_type = app_blob['type'] | ||
2251 | 263 | if session_type in remote_types: | ||
2252 | 264 | session_type = 'remote' | ||
2253 | 265 | except (AttributeError, ValueError, KeyError): | ||
2254 | 266 | session_type = 'local' | ||
2255 | 250 | self._restart_strategy = detect_restart_strategy( | 267 | self._restart_strategy = detect_restart_strategy( |
2256 | 251 | self, session_type=session_type) | 268 | self, session_type=session_type) |
2257 | 252 | self._restart_cmd_callback = cmd_callback | 269 | self._restart_cmd_callback = cmd_callback |
2258 | @@ -300,7 +317,8 @@ class SessionAssistant: | |||
2259 | 300 | Use alternate configuration object. | 317 | Use alternate configuration object. |
2260 | 301 | 318 | ||
2261 | 302 | :param config: | 319 | :param config: |
2263 | 303 | A Checkbox configuration object. | 320 | A configuration object that implements a superset of the plainbox |
2264 | 321 | configuration. | ||
2265 | 304 | :raises UnexpectedMethodCall: | 322 | :raises UnexpectedMethodCall: |
2266 | 305 | If the call is made at an unexpected time. Do not catch this error. | 323 | If the call is made at an unexpected time. Do not catch this error. |
2267 | 306 | It is a bug in your program. The error message will indicate what | 324 | It is a bug in your program. The error message will indicate what |
2268 | @@ -313,7 +331,7 @@ class SessionAssistant: | |||
2269 | 313 | UsageExpectation.of(self).enforce() | 331 | UsageExpectation.of(self).enforce() |
2270 | 314 | self._config = config | 332 | self._config = config |
2271 | 315 | self._exclude_qualifiers = [] | 333 | self._exclude_qualifiers = [] |
2273 | 316 | for pattern in self._config.get_value('test selection', 'exclude'): | 334 | for pattern in self._config.test_exclude: |
2274 | 317 | self._exclude_qualifiers.append( | 335 | self._exclude_qualifiers.append( |
2275 | 318 | RegExpJobQualifier(pattern, None, False)) | 336 | RegExpJobQualifier(pattern, None, False)) |
2276 | 319 | Unit.config = config | 337 | Unit.config = config |
2277 | @@ -1201,7 +1219,7 @@ class SessionAssistant: | |||
2278 | 1201 | if os.path.isfile(manifest): | 1219 | if os.path.isfile(manifest): |
2279 | 1202 | with open(manifest, 'rt', encoding='UTF-8') as stream: | 1220 | with open(manifest, 'rt', encoding='UTF-8') as stream: |
2280 | 1203 | manifest_cache = json.load(stream) | 1221 | manifest_cache = json.load(stream) |
2282 | 1204 | if self._config is not None and self._config.manifest: | 1222 | if self._config is not None and self._config.manifest is not Unset: |
2283 | 1205 | for manifest_id in self._config.manifest: | 1223 | for manifest_id in self._config.manifest: |
2284 | 1206 | manifest_cache.update( | 1224 | manifest_cache.update( |
2285 | 1207 | {manifest_id: self._config.manifest[manifest_id]}) | 1225 | {manifest_id: self._config.manifest[manifest_id]}) |
2286 | @@ -1364,8 +1382,11 @@ class SessionAssistant: | |||
2287 | 1364 | f.writelines(self._restart_cmd_callback( | 1382 | f.writelines(self._restart_cmd_callback( |
2288 | 1365 | self.get_session_id())) | 1383 | self.get_session_id())) |
2289 | 1366 | if not native: | 1384 | if not native: |
2292 | 1367 | result = self._runner.run_job(job, job_state, | 1385 | if self._config.environment is Unset: |
2293 | 1368 | self._config.environment, ui) | 1386 | result = self._runner.run_job(job, job_state, ui=ui) |
2294 | 1387 | else: | ||
2295 | 1388 | result = self._runner.run_job(job, job_state, | ||
2296 | 1389 | self._config.environment, ui) | ||
2297 | 1369 | builder = result.get_builder() | 1390 | builder = result.get_builder() |
2298 | 1370 | else: | 1391 | else: |
2299 | 1371 | builder = JobResultBuilder( | 1392 | builder = JobResultBuilder( |
2300 | diff --git a/plainbox/impl/session/remote_assistant.py b/plainbox/impl/session/remote_assistant.py | |||
2301 | index e15961b..e3c6c11 100644 | |||
2302 | --- a/plainbox/impl/session/remote_assistant.py | |||
2303 | +++ b/plainbox/impl/session/remote_assistant.py | |||
2304 | @@ -28,7 +28,6 @@ from contextlib import suppress | |||
2305 | 28 | from tempfile import SpooledTemporaryFile | 28 | from tempfile import SpooledTemporaryFile |
2306 | 29 | from threading import Thread, Lock | 29 | from threading import Thread, Lock |
2307 | 30 | 30 | ||
2308 | 31 | from plainbox.impl.config import Configuration | ||
2309 | 32 | from plainbox.impl.execution import UnifiedRunner | 31 | from plainbox.impl.execution import UnifiedRunner |
2310 | 33 | from plainbox.impl.session.assistant import SessionAssistant | 32 | from plainbox.impl.session.assistant import SessionAssistant |
2311 | 34 | from plainbox.impl.session.assistant import SA_RESTARTABLE | 33 | from plainbox.impl.session.assistant import SA_RESTARTABLE |
2312 | @@ -141,7 +140,7 @@ class BackgroundExecutor(Thread): | |||
2313 | 141 | class RemoteSessionAssistant(): | 140 | class RemoteSessionAssistant(): |
2314 | 142 | """Remote execution enabling wrapper for the SessionAssistant""" | 141 | """Remote execution enabling wrapper for the SessionAssistant""" |
2315 | 143 | 142 | ||
2317 | 144 | REMOTE_API_VERSION = 12 | 143 | REMOTE_API_VERSION = 11 |
2318 | 145 | 144 | ||
2319 | 146 | def __init__(self, cmd_callback): | 145 | def __init__(self, cmd_callback): |
2320 | 147 | _logger.debug("__init__()") | 146 | _logger.debug("__init__()") |
2321 | @@ -274,20 +273,20 @@ class RemoteSessionAssistant(): | |||
2322 | 274 | 273 | ||
2323 | 275 | self._launcher = load_configs() | 274 | self._launcher = load_configs() |
2324 | 276 | if configuration['launcher']: | 275 | if configuration['launcher']: |
2331 | 277 | self._launcher = Configuration.from_text( | 276 | self._launcher.read_string(configuration['launcher'], False) |
2332 | 278 | configuration['launcher'], 'Remote launcher') | 277 | if self._launcher.session_title: |
2333 | 279 | session_title = self._launcher.get_value( | 278 | session_title = self._launcher.session_title |
2334 | 280 | 'launcher', 'session_title') or session_title | 279 | if self._launcher.session_desc: |
2335 | 281 | session_desc = self._launcher.get_value( | 280 | session_desc = self._launcher.session_desc |
2330 | 282 | 'launcher', 'session_desc') or session_desc | ||
2336 | 283 | 281 | ||
2337 | 284 | self._sa.use_alternate_configuration(self._launcher) | 282 | self._sa.use_alternate_configuration(self._launcher) |
2338 | 285 | 283 | ||
2339 | 286 | if configuration['normal_user']: | 284 | if configuration['normal_user']: |
2340 | 287 | self._normal_user = configuration['normal_user'] | 285 | self._normal_user = configuration['normal_user'] |
2341 | 288 | else: | 286 | else: |
2344 | 289 | self._normal_user = self._launcher.get_value( | 287 | self._normal_user = self._launcher.normal_user |
2345 | 290 | 'daemon', 'normal_user') or _guess_normal_user() | 288 | if not self._normal_user: |
2346 | 289 | self._normal_user = _guess_normal_user() | ||
2347 | 291 | runner_kwargs = { | 290 | runner_kwargs = { |
2348 | 292 | 'normal_user_provider': lambda: self._normal_user, | 291 | 'normal_user_provider': lambda: self._normal_user, |
2349 | 293 | 'stdin': self._pipe_to_subproc, | 292 | 'stdin': self._pipe_to_subproc, |
2350 | @@ -301,13 +300,12 @@ class RemoteSessionAssistant(): | |||
2351 | 301 | 'effective_normal_user': self._normal_user, | 300 | 'effective_normal_user': self._normal_user, |
2352 | 302 | }).encode("UTF-8") | 301 | }).encode("UTF-8") |
2353 | 303 | self._sa.update_app_blob(new_blob) | 302 | self._sa.update_app_blob(new_blob) |
2356 | 304 | self._sa.configure_application_restart( | 303 | self._sa.configure_application_restart(self._cmd_callback) |
2355 | 305 | self._cmd_callback, session_type='remote') | ||
2357 | 306 | 304 | ||
2358 | 307 | self._session_id = self._sa.get_session_id() | 305 | self._session_id = self._sa.get_session_id() |
2359 | 308 | tps = self._sa.get_test_plans() | 306 | tps = self._sa.get_test_plans() |
2360 | 309 | filtered_tps = set() | 307 | filtered_tps = set() |
2362 | 310 | for filter in self._launcher.get_value('test plan', 'filter'): | 308 | for filter in self._launcher.test_plan_filters: |
2363 | 311 | filtered_tps.update(fnmatch.filter(tps, filter)) | 309 | filtered_tps.update(fnmatch.filter(tps, filter)) |
2364 | 312 | filtered_tps = list(filtered_tps) | 310 | filtered_tps = list(filtered_tps) |
2365 | 313 | response = zip(filtered_tps, [self._sa.get_test_plan( | 311 | response = zip(filtered_tps, [self._sa.get_test_plan( |
2366 | @@ -324,6 +322,11 @@ class RemoteSessionAssistant(): | |||
2367 | 324 | self._sa.update_app_blob(json.dumps( | 322 | self._sa.update_app_blob(json.dumps( |
2368 | 325 | {'testplan_id': test_plan_id, }).encode("UTF-8")) | 323 | {'testplan_id': test_plan_id, }).encode("UTF-8")) |
2369 | 326 | self._sa.select_test_plan(test_plan_id) | 324 | self._sa.select_test_plan(test_plan_id) |
2370 | 325 | # TODO: REMOTE API RAPI: Change this API on the next RAPI bump | ||
2371 | 326 | # previously the function returned bool signifying the need for sudo | ||
2372 | 327 | # password. With slave being guaranteed to never need it anymor | ||
2373 | 328 | # we can make this funciton return nothing | ||
2374 | 329 | return False | ||
2375 | 327 | 330 | ||
2376 | 328 | @allowed_when(Started) | 331 | @allowed_when(Started) |
2377 | 329 | def get_bootstrapping_todo_list(self): | 332 | def get_bootstrapping_todo_list(self): |
2378 | @@ -332,11 +335,10 @@ class RemoteSessionAssistant(): | |||
2379 | 332 | def finish_bootstrap(self): | 335 | def finish_bootstrap(self): |
2380 | 333 | self._sa.finish_bootstrap() | 336 | self._sa.finish_bootstrap() |
2381 | 334 | self._state = Bootstrapped | 337 | self._state = Bootstrapped |
2383 | 335 | if self._launcher.get_value('ui', 'auto_retry'): | 338 | if self._launcher.auto_retry: |
2384 | 336 | for job_id in self._sa.get_static_todo_list(): | 339 | for job_id in self._sa.get_static_todo_list(): |
2385 | 337 | job_state = self._sa.get_job_state(job_id) | 340 | job_state = self._sa.get_job_state(job_id) |
2388 | 338 | job_state.attempts = self._launcher.get_value( | 341 | job_state.attempts = self._launcher.max_attempts |
2387 | 339 | 'ui', 'max_attempts') | ||
2389 | 340 | return self._sa.get_static_todo_list() | 342 | return self._sa.get_static_todo_list() |
2390 | 341 | 343 | ||
2391 | 342 | def get_manifest_repr(self): | 344 | def get_manifest_repr(self): |
2392 | @@ -361,12 +363,10 @@ class RemoteSessionAssistant(): | |||
2393 | 361 | 363 | ||
2394 | 362 | def _get_ui_for_job(self, job): | 364 | def _get_ui_for_job(self, job): |
2395 | 363 | show_out = True | 365 | show_out = True |
2398 | 364 | if self._launcher.get_value( | 366 | if self._launcher.output == 'hide-resource-and-attachment': |
2397 | 365 | 'ui', 'output') == 'hide-resource-and-attachment': | ||
2399 | 366 | if job.plugin in ('local', 'resource', 'attachment'): | 367 | if job.plugin in ('local', 'resource', 'attachment'): |
2400 | 367 | show_out = False | 368 | show_out = False |
2403 | 368 | elif self._launcher.get_value( | 369 | elif self._launcher.output in ['hide', 'hide-automated']: |
2402 | 369 | 'ui', 'output') in ['hide', 'hide-automated']: | ||
2404 | 370 | if job.plugin in ('shell', 'local', 'resource', 'attachment'): | 370 | if job.plugin in ('shell', 'local', 'resource', 'attachment'): |
2405 | 371 | show_out = False | 371 | show_out = False |
2406 | 372 | if 'suppress-output' in job.get_flag_set(): | 372 | if 'suppress-output' in job.get_flag_set(): |
2407 | @@ -522,6 +522,27 @@ class RemoteSessionAssistant(): | |||
2408 | 522 | "todo": self._sa.get_dynamic_todo_list(), | 522 | "todo": self._sa.get_dynamic_todo_list(), |
2409 | 523 | } | 523 | } |
2410 | 524 | 524 | ||
2411 | 525 | def get_master_public_key(self): | ||
2412 | 526 | # TODO: REMOTE API RAPI: Remove this API on the next RAPI bump | ||
2413 | 527 | # this key is only for RAPI compliance. It will never be used as | ||
2414 | 528 | # this master requires slave to be completely sudoless | ||
2415 | 529 | return ( | ||
2416 | 530 | b'-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMII' | ||
2417 | 531 | b'BCgKCAQEA5r0bjOA+IH5lDKkW3OYb\nDuEjf5VKgUlDSJJuyBlfLTBIXZ8j3s98' | ||
2418 | 532 | b'6AbV0zB62rAcgiFrBOzx51IzBDBmHI8V\nYYpEa+q4OP4yprYpSg6xzX6LRQapC' | ||
2419 | 533 | b'Iv9BAqN4MWrKBukGMzJyemIVEPv4BSHL5L/\nLY98Mwh4dAXxj5ZdsoVPqgeMo8' | ||
2420 | 534 | b'dxfYEOwVRJvSkseIhxRL6tvgP37c48ApUyjdUO\n3C2YgqJRx7mKKDyLOvhDVEl' | ||
2421 | 535 | b'MqkAfp6qS/8xcGBTEqn08dDQIgPl8KofpC9GXMGbK\nV9FGP+c1bpA3vMOfnpsE' | ||
2422 | 536 | b'WCju2qDoTSKJTm3VMZj88mqH7nOpbk7JI/Yz0EmtNXOM\n6QIDAQAB\n-----EN' | ||
2423 | 537 | b'D PUBLIC KEY-----') | ||
2424 | 538 | |||
2425 | 539 | def save_password(self, password): | ||
2426 | 540 | """Store sudo password""" | ||
2427 | 541 | # TODO: REMOTE API RAPI: Remove this API on the next RAPI bump | ||
2428 | 542 | # if the slave is running it means we don't need password | ||
2429 | 543 | # so we can consider call to this function as passing | ||
2430 | 544 | return True | ||
2431 | 545 | |||
2432 | 525 | def finish_job(self, result=None): | 546 | def finish_job(self, result=None): |
2433 | 526 | # assert the thread completed | 547 | # assert the thread completed |
2434 | 527 | self.session_change_lock.acquire(blocking=False) | 548 | self.session_change_lock.acquire(blocking=False) |
2435 | @@ -537,7 +558,7 @@ class RemoteSessionAssistant(): | |||
2436 | 537 | if self._state != Bootstrapping: | 558 | if self._state != Bootstrapping: |
2437 | 538 | if not self._sa.get_dynamic_todo_list(): | 559 | if not self._sa.get_dynamic_todo_list(): |
2438 | 539 | if ( | 560 | if ( |
2440 | 540 | self._launcher.get_value('ui', 'auto_retry') and | 561 | self._launcher.auto_retry and |
2441 | 541 | self.get_rerun_candidates('auto') | 562 | self.get_rerun_candidates('auto') |
2442 | 542 | ): | 563 | ): |
2443 | 543 | self._state = TestsSelected | 564 | self._state = TestsSelected |
2444 | @@ -623,12 +644,11 @@ class RemoteSessionAssistant(): | |||
2445 | 623 | meta = self._sa.resume_session(session_id, runner_kwargs=runner_kwargs) | 644 | meta = self._sa.resume_session(session_id, runner_kwargs=runner_kwargs) |
2446 | 624 | app_blob = json.loads(meta.app_blob.decode("UTF-8")) | 645 | app_blob = json.loads(meta.app_blob.decode("UTF-8")) |
2447 | 625 | launcher = app_blob['launcher'] | 646 | launcher = app_blob['launcher'] |
2449 | 626 | self._launcher = Configuration.from_text(launcher, 'Remote launcher') | 647 | self._launcher.read_string(launcher, False) |
2450 | 627 | self._sa.use_alternate_configuration(self._launcher) | 648 | self._sa.use_alternate_configuration(self._launcher) |
2451 | 628 | 649 | ||
2452 | 629 | self._normal_user = app_blob.get( | 650 | self._normal_user = app_blob.get( |
2455 | 630 | 'effective_normal_user', self._launcher.get_value( | 651 | 'effective_normal_user', self._launcher.normal_user) |
2454 | 631 | 'daemon', 'normal_user')) | ||
2456 | 632 | _logger.info( | 652 | _logger.info( |
2457 | 633 | "normal_user after loading metadata: %r", self._normal_user) | 653 | "normal_user after loading metadata: %r", self._normal_user) |
2458 | 634 | test_plan_id = app_blob['testplan_id'] | 654 | test_plan_id = app_blob['testplan_id'] |
2459 | @@ -664,12 +684,12 @@ class RemoteSessionAssistant(): | |||
2460 | 664 | 684 | ||
2461 | 665 | # some jobs have already been run, so we need to update the attempts | 685 | # some jobs have already been run, so we need to update the attempts |
2462 | 666 | # count for future auto-rerunning | 686 | # count for future auto-rerunning |
2464 | 667 | if self._launcher.get_value('ui', 'auto_retry'): | 687 | if self._launcher.auto_retry: |
2465 | 668 | for job_id in [ | 688 | for job_id in [ |
2466 | 669 | job.id for job in self.get_rerun_candidates('auto')]: | 689 | job.id for job in self.get_rerun_candidates('auto')]: |
2467 | 670 | job_state = self._sa.get_job_state(job_id) | 690 | job_state = self._sa.get_job_state(job_id) |
2470 | 671 | job_state.attempts = self._launcher.get_value( | 691 | job_state.attempts = self._launcher.max_attempts - len( |
2471 | 672 | 'ui', 'max_attempts') - len(job_state.result_history) | 692 | job_state.result_history) |
2472 | 673 | 693 | ||
2473 | 674 | self._state = TestsSelected | 694 | self._state = TestsSelected |
2474 | 675 | 695 | ||
2475 | @@ -695,6 +715,12 @@ class RemoteSessionAssistant(): | |||
2476 | 695 | return self._sa._manager | 715 | return self._sa._manager |
2477 | 696 | 716 | ||
2478 | 697 | @property | 717 | @property |
2479 | 718 | def passwordless_sudo(self): | ||
2480 | 719 | # TODO: REMOTE API RAPI: Remove this API on the next RAPI bump | ||
2481 | 720 | # if the slave is still running it means it's very passwordless | ||
2482 | 721 | return True | ||
2483 | 722 | |||
2484 | 723 | @property | ||
2485 | 698 | def sideloaded_providers(self): | 724 | def sideloaded_providers(self): |
2486 | 699 | return self._sa.sideloaded_providers | 725 | return self._sa.sideloaded_providers |
2487 | 700 | 726 | ||
2488 | diff --git a/plainbox/impl/test_config.py b/plainbox/impl/test_config.py | |||
2489 | 701 | deleted file mode 100644 | 727 | deleted file mode 100644 |
2490 | index fce0700..0000000 | |||
2491 | --- a/plainbox/impl/test_config.py | |||
2492 | +++ /dev/null | |||
2493 | @@ -1,130 +0,0 @@ | |||
2494 | 1 | # This file is part of Checkbox. | ||
2495 | 2 | # | ||
2496 | 3 | # Copyright 2020 Canonical Ltd. | ||
2497 | 4 | # Written by: | ||
2498 | 5 | # Maciej Kisielewski <maciej.kisielewski@canonical.com> | ||
2499 | 6 | # | ||
2500 | 7 | # Checkbox is free software: you can redistribute it and/or modify | ||
2501 | 8 | # it under the terms of the GNU General Public License version 3, | ||
2502 | 9 | # as published by the Free Software Foundation. | ||
2503 | 10 | # | ||
2504 | 11 | # Checkbox is distributed in the hope that it will be useful, | ||
2505 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2506 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2507 | 14 | # GNU General Public License for more details. | ||
2508 | 15 | # | ||
2509 | 16 | # You should have received a copy of the GNU General Public License | ||
2510 | 17 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
2511 | 18 | """ | ||
2512 | 19 | This module contains tests for the new Checkbox Config module | ||
2513 | 20 | """ | ||
2514 | 21 | from contextlib import contextmanager | ||
2515 | 22 | import logging | ||
2516 | 23 | from unittest import TestCase | ||
2517 | 24 | from unittest.mock import mock_open, patch | ||
2518 | 25 | |||
2519 | 26 | from plainbox.impl.config import Configuration | ||
2520 | 27 | |||
2521 | 28 | |||
2522 | 29 | @contextmanager | ||
2523 | 30 | def muted_logging(): | ||
2524 | 31 | """Disable logging so the test that use this have no output.""" | ||
2525 | 32 | saved_level = logging.root.getEffectiveLevel() | ||
2526 | 33 | logging.root.setLevel(logging.CRITICAL) | ||
2527 | 34 | yield | ||
2528 | 35 | logging.root.setLevel(saved_level) | ||
2529 | 36 | |||
2530 | 37 | |||
2531 | 38 | class ConfigurationTests(TestCase): | ||
2532 | 39 | """Tests for the Configuration class.""" | ||
2533 | 40 | |||
2534 | 41 | def test_empty_file_yields_defaults(self): | ||
2535 | 42 | """A default configuration instance should have default values.""" | ||
2536 | 43 | # let's check a few values from random sections | ||
2537 | 44 | cfg = Configuration() | ||
2538 | 45 | self.assertEqual(cfg.get_value('test plan', 'filter'), ['*']) | ||
2539 | 46 | self.assertTrue(cfg.get_value('launcher', 'local_submission')) | ||
2540 | 47 | self.assertEqual(cfg.get_value('daemon', 'normal_user'), '') | ||
2541 | 48 | |||
2542 | 49 | @patch('os.path.isfile', return_value=True) | ||
2543 | 50 | def test_one_var_overwrites(self, _): | ||
2544 | 51 | """ | ||
2545 | 52 | One variable properly shadows defaults. | ||
2546 | 53 | |||
2547 | 54 | Having one (good) value in config should yield a config with | ||
2548 | 55 | defaults except the one var placed in the config file. | ||
2549 | 56 | """ | ||
2550 | 57 | ini_data = """ | ||
2551 | 58 | [launcher] | ||
2552 | 59 | stock_reports = text | ||
2553 | 60 | """ | ||
2554 | 61 | with patch('builtins.open', mock_open(read_data=ini_data)): | ||
2555 | 62 | cfg = Configuration.from_path('unit test') | ||
2556 | 63 | self.assertEqual(cfg.get_value('test plan', 'filter'), ['*']) | ||
2557 | 64 | self.assertTrue(cfg.get_value('launcher', 'local_submission')) | ||
2558 | 65 | self.assertEqual(cfg.get_value('daemon', 'normal_user'), '') | ||
2559 | 66 | self.assertEqual(cfg.get_origin('daemon', 'normal_user'), '') | ||
2560 | 67 | self.assertEqual(cfg.get_value('launcher', 'stock_reports'), ['text']) | ||
2561 | 68 | self.assertEqual( | ||
2562 | 69 | cfg.get_origin('launcher', 'stock_reports'), | ||
2563 | 70 | 'unit test') | ||
2564 | 71 | |||
2565 | 72 | @patch('os.path.isfile', return_value=True) | ||
2566 | 73 | def test_string_list_distinction(self, _): | ||
2567 | 74 | """ | ||
2568 | 75 | Parsing of lists and multi-word strings. | ||
2569 | 76 | |||
2570 | 77 | Depending on the config spec the field can be considered a string | ||
2571 | 78 | (with spaces) or a list. | ||
2572 | 79 | """ | ||
2573 | 80 | ini_data = """ | ||
2574 | 81 | [launcher] | ||
2575 | 82 | launcher_version = 1 | ||
2576 | 83 | stock_reports = submission_files, text | ||
2577 | 84 | session_title = A session title | ||
2578 | 85 | """ | ||
2579 | 86 | with patch('builtins.open', mock_open(read_data=ini_data)): | ||
2580 | 87 | cfg = Configuration.from_path('unit test') | ||
2581 | 88 | self.assertEqual( | ||
2582 | 89 | cfg.get_value('launcher', 'stock_reports'), | ||
2583 | 90 | ['submission_files', 'text']) | ||
2584 | 91 | self.assertEqual( | ||
2585 | 92 | cfg.get_value('launcher', 'session_title'), | ||
2586 | 93 | 'A session title') | ||
2587 | 94 | |||
2588 | 95 | @patch('os.path.isfile', return_value=True) | ||
2589 | 96 | def test_unexpected_content(self, _): | ||
2590 | 97 | """ | ||
2591 | 98 | Yield problems with extra data in configs. | ||
2592 | 99 | """ | ||
2593 | 100 | ini_data = """ | ||
2594 | 101 | [launcher] | ||
2595 | 102 | barfoo = 5 | ||
2596 | 103 | [foobar] | ||
2597 | 104 | """ | ||
2598 | 105 | with muted_logging(): | ||
2599 | 106 | with patch('builtins.open', mock_open(read_data=ini_data)): | ||
2600 | 107 | cfg = Configuration.from_path('unit test') | ||
2601 | 108 | self.assertEqual(len(cfg.get_problems()), 2) | ||
2602 | 109 | |||
2603 | 110 | def test_default_vars_are_not_supported(self): | ||
2604 | 111 | """ | ||
2605 | 112 | Yield a problem when variable is defined in the [DEFAULT] section. | ||
2606 | 113 | """ | ||
2607 | 114 | ini_data = """ | ||
2608 | 115 | [DEFAULT] | ||
2609 | 116 | badvar = 4 | ||
2610 | 117 | """ | ||
2611 | 118 | with muted_logging(): | ||
2612 | 119 | with patch('builtins.open', mock_open(read_data=ini_data)): | ||
2613 | 120 | cfg = Configuration.from_path('unit test') | ||
2614 | 121 | self.assertEqual(len(cfg.get_problems()), 1) | ||
2615 | 122 | |||
2616 | 123 | @patch('os.path.isfile', return_value=False) | ||
2617 | 124 | def test_ini_not_found(self, _): | ||
2618 | 125 | """ | ||
2619 | 126 | Yield a problem when an ini file cannot be opened. | ||
2620 | 127 | """ | ||
2621 | 128 | with muted_logging(): | ||
2622 | 129 | cfg = Configuration.from_path('invalid path') | ||
2623 | 130 | self.assertEqual(len(cfg.get_problems()), 1) | ||
2624 | diff --git a/plainbox/impl/test_launcher.py b/plainbox/impl/test_launcher.py | |||
2625 | 131 | new file mode 100644 | 0 | new file mode 100644 |
2626 | index 0000000..47b47d1 | |||
2627 | --- /dev/null | |||
2628 | +++ b/plainbox/impl/test_launcher.py | |||
2629 | @@ -0,0 +1,136 @@ | |||
2630 | 1 | # This file is part of Checkbox. | ||
2631 | 2 | # | ||
2632 | 3 | # Copyright 2016 Canonical Ltd. | ||
2633 | 4 | # Written by: | ||
2634 | 5 | # Maciej Kisielewski <maciej.kisielewski@canonical.com> | ||
2635 | 6 | # | ||
2636 | 7 | # Checkbox is free software: you can redistribute it and/or modify | ||
2637 | 8 | # it under the terms of the GNU General Public License version 3, | ||
2638 | 9 | # as published by the Free Software Foundation. | ||
2639 | 10 | # | ||
2640 | 11 | # Checkbox is distributed in the hope that it will be useful, | ||
2641 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2642 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2643 | 14 | # GNU General Public License for more details. | ||
2644 | 15 | # | ||
2645 | 16 | # You should have received a copy of the GNU General Public License | ||
2646 | 17 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
2647 | 18 | |||
2648 | 19 | """ | ||
2649 | 20 | plainbox.impl.test_launcher | ||
2650 | 21 | ========================= | ||
2651 | 22 | |||
2652 | 23 | Test definitions for plainbox.imlp.launcher module | ||
2653 | 24 | """ | ||
2654 | 25 | |||
2655 | 26 | from unittest import TestCase | ||
2656 | 27 | from textwrap import dedent | ||
2657 | 28 | |||
2658 | 29 | from plainbox.impl.secure.config import Unset | ||
2659 | 30 | |||
2660 | 31 | from plainbox.impl.launcher import LauncherDefinition | ||
2661 | 32 | from plainbox.impl.launcher import LauncherDefinition1 | ||
2662 | 33 | |||
2663 | 34 | |||
2664 | 35 | class LauncherDefinitionTests(TestCase): | ||
2665 | 36 | launcher_version_legacy = dedent(""" | ||
2666 | 37 | [launcher] | ||
2667 | 38 | """) | ||
2668 | 39 | launcher_version_1 = dedent(""" | ||
2669 | 40 | [launcher] | ||
2670 | 41 | launcher_version = 1 | ||
2671 | 42 | """) | ||
2672 | 43 | launcher_version_future = dedent(""" | ||
2673 | 44 | [launcher] | ||
2674 | 45 | launcher_version = 2 | ||
2675 | 46 | """) | ||
2676 | 47 | |||
2677 | 48 | def test_get_concrete_launcher_legacy(self): | ||
2678 | 49 | l = LauncherDefinition() | ||
2679 | 50 | l.read_string(self.launcher_version_legacy) | ||
2680 | 51 | with self.assertRaises(KeyError): | ||
2681 | 52 | l.get_concrete_launcher() | ||
2682 | 53 | |||
2683 | 54 | def test_get_concrete_launcher_launcher1(self): | ||
2684 | 55 | l = LauncherDefinition() | ||
2685 | 56 | l.read_string(self.launcher_version_1) | ||
2686 | 57 | cls = l.get_concrete_launcher().__class__ | ||
2687 | 58 | self.assertIs(cls, LauncherDefinition1) | ||
2688 | 59 | |||
2689 | 60 | def test_get_concrete_launcher_future_raises(self): | ||
2690 | 61 | l = LauncherDefinition() | ||
2691 | 62 | l.read_string(self.launcher_version_future) | ||
2692 | 63 | with self.assertRaises(KeyError): | ||
2693 | 64 | l.get_concrete_launcher() | ||
2694 | 65 | |||
2695 | 66 | |||
2696 | 67 | class LauncherDefinition1Tests(TestCase): | ||
2697 | 68 | |||
2698 | 69 | def test_defaults(self): | ||
2699 | 70 | empty_launcher = dedent(""" | ||
2700 | 71 | [launcher] | ||
2701 | 72 | launcher_version = 1 | ||
2702 | 73 | """) | ||
2703 | 74 | l = LauncherDefinition1() | ||
2704 | 75 | l.read_string(empty_launcher) | ||
2705 | 76 | self.assertEqual(l.api_version, '0.99') | ||
2706 | 77 | self.assertEqual(l.app_id, 'checkbox-cli') | ||
2707 | 78 | self.assertEqual(l.api_flags, ['restartable']) | ||
2708 | 79 | self.assertEqual(l.test_plan_filters, ['*']) | ||
2709 | 80 | self.assertEqual(l.test_plan_default_selection, Unset) | ||
2710 | 81 | self.assertEqual(l.test_plan_forced, False) | ||
2711 | 82 | self.assertEqual(l.test_selection_forced, False) | ||
2712 | 83 | self.assertEqual(l.ui_type, 'interactive') | ||
2713 | 84 | self.assertEqual(l.auto_retry, False) | ||
2714 | 85 | self.assertEqual(l.max_attempts, 3) | ||
2715 | 86 | self.assertEqual(l.delay_before_retry, 1) | ||
2716 | 87 | self.assertEqual(l.restart_strategy, Unset) | ||
2717 | 88 | |||
2718 | 89 | def test_smoke(self): | ||
2719 | 90 | definition = dedent(""" | ||
2720 | 91 | [launcher] | ||
2721 | 92 | launcher_version = 1 | ||
2722 | 93 | api_version = 0.99 | ||
2723 | 94 | api_flags = restartable | ||
2724 | 95 | app_id = FOOBAR | ||
2725 | 96 | [test plan] | ||
2726 | 97 | unit = 2000.the.chosen.one | ||
2727 | 98 | filter = 2000*, 3000* tp_foo* | ||
2728 | 99 | forced = yes | ||
2729 | 100 | [test selection] | ||
2730 | 101 | forced = yes | ||
2731 | 102 | [ui] | ||
2732 | 103 | type = silent | ||
2733 | 104 | auto_retry = yes | ||
2734 | 105 | max_attempts = 5 | ||
2735 | 106 | delay_before_retry = 60 | ||
2736 | 107 | [restart] | ||
2737 | 108 | strategy = magic | ||
2738 | 109 | [report:foo_report] | ||
2739 | 110 | exporter = bar_exporter | ||
2740 | 111 | transport = file | ||
2741 | 112 | [exporter:bar_exporter] | ||
2742 | 113 | unit = bar_exporter_unit | ||
2743 | 114 | [transport:file] | ||
2744 | 115 | path = /tmp/path | ||
2745 | 116 | """) | ||
2746 | 117 | l = LauncherDefinition1() | ||
2747 | 118 | l.read_string(definition) | ||
2748 | 119 | self.assertEqual(l.api_version, '0.99') | ||
2749 | 120 | self.assertEqual(l.app_id, 'FOOBAR') | ||
2750 | 121 | self.assertEqual(l.api_flags, ['restartable']) | ||
2751 | 122 | self.assertEqual(l.test_plan_filters, ['2000*', '3000*', 'tp_foo*']) | ||
2752 | 123 | self.assertEqual(l.test_plan_default_selection, '2000.the.chosen.one') | ||
2753 | 124 | self.assertEqual(l.test_plan_forced, True) | ||
2754 | 125 | self.assertEqual(l.test_selection_forced, True) | ||
2755 | 126 | self.assertEqual(l.ui_type, 'silent') | ||
2756 | 127 | self.assertEqual(l.auto_retry, True) | ||
2757 | 128 | self.assertEqual(l.max_attempts, 5) | ||
2758 | 129 | self.assertEqual(l.delay_before_retry, 60) | ||
2759 | 130 | self.assertEqual(l.restart_strategy, 'magic') | ||
2760 | 131 | self.assertEqual(l.reports, { | ||
2761 | 132 | 'foo_report': {'exporter': 'bar_exporter', 'transport': 'file'}}) | ||
2762 | 133 | self.assertEqual(l.exporters, { | ||
2763 | 134 | 'bar_exporter': {'unit': 'bar_exporter_unit'}}) | ||
2764 | 135 | self.assertEqual(l.transports, { | ||
2765 | 136 | 'file': {'path': '/tmp/path'}}) | ||
2766 | diff --git a/plainbox/impl/unit/unit.py b/plainbox/impl/unit/unit.py | |||
2767 | index 8041683..cd7b365 100644 | |||
2768 | --- a/plainbox/impl/unit/unit.py | |||
2769 | +++ b/plainbox/impl/unit/unit.py | |||
2770 | @@ -36,6 +36,7 @@ from jinja2 import Template | |||
2771 | 36 | from plainbox.i18n import gettext as _ | 36 | from plainbox.i18n import gettext as _ |
2772 | 37 | from plainbox.impl.decorators import cached_property | 37 | from plainbox.impl.decorators import cached_property |
2773 | 38 | from plainbox.impl.decorators import instance_method_lru_cache | 38 | from plainbox.impl.decorators import instance_method_lru_cache |
2774 | 39 | from plainbox.impl.secure.config import Unset | ||
2775 | 39 | from plainbox.impl.secure.origin import Origin | 40 | from plainbox.impl.secure.origin import Origin |
2776 | 40 | from plainbox.impl.secure.rfc822 import normalize_rfc822_value | 41 | from plainbox.impl.secure.rfc822 import normalize_rfc822_value |
2777 | 41 | from plainbox.impl.symbol import Symbol | 42 | from plainbox.impl.symbol import Symbol |
2778 | @@ -608,7 +609,7 @@ class Unit(metaclass=UnitType): | |||
2779 | 608 | 609 | ||
2780 | 609 | @instance_method_lru_cache(maxsize=None) | 610 | @instance_method_lru_cache(maxsize=None) |
2781 | 610 | def _checkbox_env(self): | 611 | def _checkbox_env(self): |
2783 | 611 | if self.config is not None and self.config.environment: | 612 | if self.config is not None and self.config.environment is not Unset: |
2784 | 612 | return self.config.environment | 613 | return self.config.environment |
2785 | 613 | else: | 614 | else: |
2786 | 614 | return {} | 615 | return {} |
2787 | diff --git a/plainbox/vendor/rpyc/__init__.py b/plainbox/vendor/rpyc/__init__.py | |||
2788 | index 4ad98f7..9cd71d5 100644 | |||
2789 | --- a/plainbox/vendor/rpyc/__init__.py | |||
2790 | +++ b/plainbox/vendor/rpyc/__init__.py | |||
2791 | @@ -46,7 +46,7 @@ from plainbox.vendor.rpyc.core import (SocketStream, TunneledSocketStream, PipeS | |||
2792 | 46 | Connection, Service, BaseNetref, AsyncResult, GenericException, | 46 | Connection, Service, BaseNetref, AsyncResult, GenericException, |
2793 | 47 | AsyncResultTimeout, VoidService, SlaveService, MasterService, ClassicService) | 47 | AsyncResultTimeout, VoidService, SlaveService, MasterService, ClassicService) |
2794 | 48 | from plainbox.vendor.rpyc.utils.factory import (connect_stream, connect_channel, connect_pipes, | 48 | from plainbox.vendor.rpyc.utils.factory import (connect_stream, connect_channel, connect_pipes, |
2796 | 49 | connect_stdpipes, connect, ssl_connect, list_services, discover, connect_by_service, connect_subproc, | 49 | connect_stdpipes, connect, ssl_connect, discover, connect_by_service, connect_subproc, |
2797 | 50 | connect_thread, ssh_connect) | 50 | connect_thread, ssh_connect) |
2798 | 51 | from plainbox.vendor.rpyc.utils.helpers import async_, timed, buffiter, BgServingThread, restricted | 51 | from plainbox.vendor.rpyc.utils.helpers import async_, timed, buffiter, BgServingThread, restricted |
2799 | 52 | from plainbox.vendor.rpyc.utils import classic | 52 | from plainbox.vendor.rpyc.utils import classic |
2800 | diff --git a/plainbox/vendor/rpyc/core/async_.py b/plainbox/vendor/rpyc/core/async_.py | |||
2801 | index b46f4d0..fd69027 100644 | |||
2802 | --- a/plainbox/vendor/rpyc/core/async_.py | |||
2803 | +++ b/plainbox/vendor/rpyc/core/async_.py | |||
2804 | @@ -1,5 +1,4 @@ | |||
2805 | 1 | import time # noqa: F401 | 1 | import time # noqa: F401 |
2806 | 2 | from threading import Event | ||
2807 | 3 | from plainbox.vendor.rpyc.lib import Timeout | 2 | from plainbox.vendor.rpyc.lib import Timeout |
2808 | 4 | from plainbox.vendor.rpyc.lib.compat import TimeoutError as AsyncResultTimeout | 3 | from plainbox.vendor.rpyc.lib.compat import TimeoutError as AsyncResultTimeout |
2809 | 5 | 4 | ||
2810 | @@ -13,14 +12,14 @@ class AsyncResult(object): | |||
2811 | 13 | 12 | ||
2812 | 14 | def __init__(self, conn): | 13 | def __init__(self, conn): |
2813 | 15 | self._conn = conn | 14 | self._conn = conn |
2815 | 16 | self._is_ready = Event() | 15 | self._is_ready = False |
2816 | 17 | self._is_exc = None | 16 | self._is_exc = None |
2817 | 18 | self._obj = None | 17 | self._obj = None |
2818 | 19 | self._callbacks = [] | 18 | self._callbacks = [] |
2819 | 20 | self._ttl = Timeout(None) | 19 | self._ttl = Timeout(None) |
2820 | 21 | 20 | ||
2821 | 22 | def __repr__(self): | 21 | def __repr__(self): |
2823 | 23 | if self._is_ready.is_set(): | 22 | if self._is_ready: |
2824 | 24 | state = "ready" | 23 | state = "ready" |
2825 | 25 | elif self._is_exc: | 24 | elif self._is_exc: |
2826 | 26 | state = "error" | 25 | state = "error" |
2827 | @@ -28,14 +27,14 @@ class AsyncResult(object): | |||
2828 | 28 | state = "expired" | 27 | state = "expired" |
2829 | 29 | else: | 28 | else: |
2830 | 30 | state = "pending" | 29 | state = "pending" |
2832 | 31 | return "<AsyncResult object ({}) at 0x{:08x}>".format((state), (id(self))) | 30 | return "<AsyncResult object (%s) at 0x%08x>" % (state, id(self)) |
2833 | 32 | 31 | ||
2834 | 33 | def __call__(self, is_exc, obj): | 32 | def __call__(self, is_exc, obj): |
2835 | 34 | if self.expired: | 33 | if self.expired: |
2836 | 35 | return | 34 | return |
2837 | 36 | self._is_exc = is_exc | 35 | self._is_exc = is_exc |
2838 | 37 | self._obj = obj | 36 | self._obj = obj |
2840 | 38 | self._is_ready.set() | 37 | self._is_ready = True |
2841 | 39 | for cb in self._callbacks: | 38 | for cb in self._callbacks: |
2842 | 40 | cb(self) | 39 | cb(self) |
2843 | 41 | del self._callbacks[:] | 40 | del self._callbacks[:] |
2844 | @@ -44,9 +43,9 @@ class AsyncResult(object): | |||
2845 | 44 | """Waits for the result to arrive. If the AsyncResult object has an | 43 | """Waits for the result to arrive. If the AsyncResult object has an |
2846 | 45 | expiry set, and the result did not arrive within that timeout, | 44 | expiry set, and the result did not arrive within that timeout, |
2847 | 46 | an :class:`AsyncResultTimeout` exception is raised""" | 45 | an :class:`AsyncResultTimeout` exception is raised""" |
2849 | 47 | while not self._is_ready.is_set() and not self._ttl.expired(): | 46 | while not self._is_ready and not self._ttl.expired(): |
2850 | 48 | self._conn.serve(self._ttl) | 47 | self._conn.serve(self._ttl) |
2852 | 49 | if not self._is_ready.is_set(): | 48 | if not self._is_ready: |
2853 | 50 | raise AsyncResultTimeout("result expired") | 49 | raise AsyncResultTimeout("result expired") |
2854 | 51 | 50 | ||
2855 | 52 | def add_callback(self, func): | 51 | def add_callback(self, func): |
2856 | @@ -57,7 +56,7 @@ class AsyncResult(object): | |||
2857 | 57 | 56 | ||
2858 | 58 | :param func: the callback function to add | 57 | :param func: the callback function to add |
2859 | 59 | """ | 58 | """ |
2861 | 60 | if self._is_ready.is_set(): | 59 | if self._is_ready: |
2862 | 61 | func(self) | 60 | func(self) |
2863 | 62 | else: | 61 | else: |
2864 | 63 | self._callbacks.append(func) | 62 | self._callbacks.append(func) |
2865 | @@ -73,12 +72,12 @@ class AsyncResult(object): | |||
2866 | 73 | @property | 72 | @property |
2867 | 74 | def ready(self): | 73 | def ready(self): |
2868 | 75 | """Indicates whether the result has arrived""" | 74 | """Indicates whether the result has arrived""" |
2870 | 76 | if self._is_ready.is_set(): | 75 | if self._is_ready: |
2871 | 77 | return True | 76 | return True |
2872 | 78 | if self._ttl.expired(): | 77 | if self._ttl.expired(): |
2873 | 79 | return False | 78 | return False |
2874 | 80 | self._conn.poll_all() | 79 | self._conn.poll_all() |
2876 | 81 | return self._is_ready.is_set() | 80 | return self._is_ready |
2877 | 82 | 81 | ||
2878 | 83 | @property | 82 | @property |
2879 | 84 | def error(self): | 83 | def error(self): |
2880 | @@ -88,7 +87,7 @@ class AsyncResult(object): | |||
2881 | 88 | @property | 87 | @property |
2882 | 89 | def expired(self): | 88 | def expired(self): |
2883 | 90 | """Indicates whether the AsyncResult has expired""" | 89 | """Indicates whether the AsyncResult has expired""" |
2885 | 91 | return not self._is_ready.is_set() and self._ttl.expired() | 90 | return not self._is_ready and self._ttl.expired() |
2886 | 92 | 91 | ||
2887 | 93 | @property | 92 | @property |
2888 | 94 | def value(self): | 93 | def value(self): |
2889 | diff --git a/plainbox/vendor/rpyc/core/brine.py b/plainbox/vendor/rpyc/core/brine.py | |||
2890 | index 20fa5c2..0545cb3 100644 | |||
2891 | --- a/plainbox/vendor/rpyc/core/brine.py | |||
2892 | +++ b/plainbox/vendor/rpyc/core/brine.py | |||
2893 | @@ -1,6 +1,6 @@ | |||
2894 | 1 | """*Brine* is a simple, fast and secure object serializer for **immutable** objects. | 1 | """*Brine* is a simple, fast and secure object serializer for **immutable** objects. |
2895 | 2 | 2 | ||
2897 | 3 | The following types are supported: ``int``, ``bool``, ``str``, ``float``, | 3 | The following types are supported: ``int``, ``long``, ``bool``, ``str``, ``float``, |
2898 | 4 | ``unicode``, ``bytes``, ``slice``, ``complex``, ``tuple`` (of simple types), | 4 | ``unicode``, ``bytes``, ``slice``, ``complex``, ``tuple`` (of simple types), |
2899 | 5 | ``frozenset`` (of simple types) as well as the following singletons: ``None``, | 5 | ``frozenset`` (of simple types) as well as the following singletons: ``None``, |
2900 | 6 | ``NotImplemented``, and ``Ellipsis``. | 6 | ``NotImplemented``, and ``Ellipsis``. |
2901 | @@ -17,7 +17,7 @@ Example:: | |||
2902 | 17 | >>> x == z | 17 | >>> x == z |
2903 | 18 | True | 18 | True |
2904 | 19 | """ | 19 | """ |
2906 | 20 | from plainbox.vendor.rpyc.lib.compat import Struct, BytesIO, BYTES_LITERAL | 20 | from plainbox.vendor.rpyc.lib.compat import Struct, BytesIO, is_py_3k, BYTES_LITERAL |
2907 | 21 | 21 | ||
2908 | 22 | 22 | ||
2909 | 23 | # singletons | 23 | # singletons |
2910 | @@ -30,7 +30,7 @@ TAG_NOT_IMPLEMENTED = b"\x05" | |||
2911 | 30 | TAG_ELLIPSIS = b"\x06" | 30 | TAG_ELLIPSIS = b"\x06" |
2912 | 31 | # types | 31 | # types |
2913 | 32 | TAG_UNICODE = b"\x08" | 32 | TAG_UNICODE = b"\x08" |
2915 | 33 | # deprecated w/ py2 support TAG_LONG = b"\x09" | 33 | TAG_LONG = b"\x09" |
2916 | 34 | TAG_STR1 = b"\x0a" | 34 | TAG_STR1 = b"\x0a" |
2917 | 35 | TAG_STR2 = b"\x0b" | 35 | TAG_STR2 = b"\x0b" |
2918 | 36 | TAG_STR3 = b"\x0c" | 36 | TAG_STR3 = b"\x0c" |
2919 | @@ -49,7 +49,10 @@ TAG_FLOAT = b"\x18" | |||
2920 | 49 | TAG_SLICE = b"\x19" | 49 | TAG_SLICE = b"\x19" |
2921 | 50 | TAG_FSET = b"\x1a" | 50 | TAG_FSET = b"\x1a" |
2922 | 51 | TAG_COMPLEX = b"\x1b" | 51 | TAG_COMPLEX = b"\x1b" |
2924 | 52 | IMM_INTS = dict((i, bytes([i + 0x50])) for i in range(-0x30, 0xa0)) | 52 | if is_py_3k: |
2925 | 53 | IMM_INTS = dict((i, bytes([i + 0x50])) for i in range(-0x30, 0xa0)) | ||
2926 | 54 | else: | ||
2927 | 55 | IMM_INTS = dict((i, chr(i + 0x50)) for i in range(-0x30, 0xa0)) | ||
2928 | 53 | 56 | ||
2929 | 54 | I1 = Struct("!B") | 57 | I1 = Struct("!B") |
2930 | 55 | I4 = Struct("!L") | 58 | I4 = Struct("!L") |
2931 | @@ -153,6 +156,13 @@ def _dump_str(obj, stream): | |||
2932 | 153 | _dump_bytes(obj.encode("utf8"), stream) | 156 | _dump_bytes(obj.encode("utf8"), stream) |
2933 | 154 | 157 | ||
2934 | 155 | 158 | ||
2935 | 159 | if not is_py_3k: | ||
2936 | 160 | @register(_dump_registry, long) # noqa: F821 | ||
2937 | 161 | def _dump_long(obj, stream): | ||
2938 | 162 | stream.append(TAG_LONG) | ||
2939 | 163 | _dump_int(obj, stream) | ||
2940 | 164 | |||
2941 | 165 | |||
2942 | 156 | @register(_dump_registry, tuple) | 166 | @register(_dump_registry, tuple) |
2943 | 157 | def _dump_tuple(obj, stream): | 167 | def _dump_tuple(obj, stream): |
2944 | 158 | lenobj = len(obj) | 168 | lenobj = len(obj) |
2945 | @@ -175,7 +185,7 @@ def _dump_tuple(obj, stream): | |||
2946 | 175 | 185 | ||
2947 | 176 | 186 | ||
2948 | 177 | def _undumpable(obj, stream): | 187 | def _undumpable(obj, stream): |
2950 | 178 | raise TypeError("cannot dump {}".format((obj))) | 188 | raise TypeError("cannot dump %r" % (obj,)) |
2951 | 179 | 189 | ||
2952 | 180 | 190 | ||
2953 | 181 | def _dump(obj, stream): | 191 | def _dump(obj, stream): |
2954 | @@ -219,6 +229,18 @@ def _load_empty_str(stream): | |||
2955 | 219 | return b"" | 229 | return b"" |
2956 | 220 | 230 | ||
2957 | 221 | 231 | ||
2958 | 232 | if is_py_3k: | ||
2959 | 233 | @register(_load_registry, TAG_LONG) | ||
2960 | 234 | def _load_long(stream): | ||
2961 | 235 | obj = _load(stream) | ||
2962 | 236 | return int(obj) | ||
2963 | 237 | else: | ||
2964 | 238 | @register(_load_registry, TAG_LONG) | ||
2965 | 239 | def _load_long(stream): | ||
2966 | 240 | obj = _load(stream) | ||
2967 | 241 | return long(obj) # noqa: F821 | ||
2968 | 242 | |||
2969 | 243 | |||
2970 | 222 | @register(_load_registry, TAG_FLOAT) | 244 | @register(_load_registry, TAG_FLOAT) |
2971 | 223 | def _load_float(stream): | 245 | def _load_float(stream): |
2972 | 224 | return F8.unpack(stream.read(8))[0] | 246 | return F8.unpack(stream.read(8))[0] |
2973 | @@ -294,10 +316,16 @@ def _load_tup_l1(stream): | |||
2974 | 294 | return tuple(_load(stream) for i in range(l)) | 316 | return tuple(_load(stream) for i in range(l)) |
2975 | 295 | 317 | ||
2976 | 296 | 318 | ||
2981 | 297 | @register(_load_registry, TAG_TUP_L4) | 319 | if is_py_3k: |
2982 | 298 | def _load_tup_l4(stream): | 320 | @register(_load_registry, TAG_TUP_L4) |
2983 | 299 | l, = I4.unpack(stream.read(4)) | 321 | def _load_tup_l4(stream): |
2984 | 300 | return tuple(_load(stream) for i in range(l)) | 322 | l, = I4.unpack(stream.read(4)) |
2985 | 323 | return tuple(_load(stream) for i in range(l)) | ||
2986 | 324 | else: | ||
2987 | 325 | @register(_load_registry, TAG_TUP_L4) | ||
2988 | 326 | def _load_tup_l4(stream): | ||
2989 | 327 | l, = I4.unpack(stream.read(4)) | ||
2990 | 328 | return tuple(_load(stream) for i in xrange(l)) # noqa | ||
2991 | 301 | 329 | ||
2992 | 302 | 330 | ||
2993 | 303 | @register(_load_registry, TAG_SLICE) | 331 | @register(_load_registry, TAG_SLICE) |
2994 | @@ -357,7 +385,12 @@ def load(data): | |||
2995 | 357 | return _load(stream) | 385 | return _load(stream) |
2996 | 358 | 386 | ||
2997 | 359 | 387 | ||
2999 | 360 | simple_types = frozenset([type(None), int, bool, float, bytes, str, complex, type(NotImplemented), type(Ellipsis)]) | 388 | if is_py_3k: |
3000 | 389 | simple_types = frozenset([type(None), int, bool, float, bytes, str, complex, | ||
3001 | 390 | type(NotImplemented), type(Ellipsis)]) | ||
3002 | 391 | else: | ||
3003 | 392 | simple_types = frozenset([type(None), int, long, bool, float, bytes, unicode, complex, # noqa: F821 | ||
3004 | 393 | type(NotImplemented), type(Ellipsis)]) | ||
3005 | 361 | 394 | ||
3006 | 362 | 395 | ||
3007 | 363 | def dumpable(obj): | 396 | def dumpable(obj): |
3008 | diff --git a/plainbox/vendor/rpyc/core/consts.py b/plainbox/vendor/rpyc/core/consts.py | |||
3009 | index 300186b..31a532d 100644 | |||
3010 | --- a/plainbox/vendor/rpyc/core/consts.py | |||
3011 | +++ b/plainbox/vendor/rpyc/core/consts.py | |||
3012 | @@ -37,9 +37,6 @@ HANDLE_INSTANCECHECK = 20 | |||
3013 | 37 | # optimized exceptions | 37 | # optimized exceptions |
3014 | 38 | EXC_STOP_ITERATION = 1 | 38 | EXC_STOP_ITERATION = 1 |
3015 | 39 | 39 | ||
3016 | 40 | # IO values | ||
3017 | 41 | STREAM_CHUNK = 64000 # read/write chunk is 64KB, too large of a value will degrade response for other clients | ||
3018 | 42 | |||
3019 | 43 | # DEBUG | 40 | # DEBUG |
3020 | 44 | # for k in globals().keys(): | 41 | # for k in globals().keys(): |
3021 | 45 | # globals()[k] = k | 42 | # globals()[k] = k |
3022 | diff --git a/plainbox/vendor/rpyc/core/netref.py b/plainbox/vendor/rpyc/core/netref.py | |||
3023 | index 9287906..fd15ef1 100644 | |||
3024 | --- a/plainbox/vendor/rpyc/core/netref.py | |||
3025 | +++ b/plainbox/vendor/rpyc/core/netref.py | |||
3026 | @@ -4,7 +4,7 @@ of *magic*, so beware. | |||
3027 | 4 | import sys | 4 | import sys |
3028 | 5 | import types | 5 | import types |
3029 | 6 | from plainbox.vendor.rpyc.lib import get_methods, get_id_pack | 6 | from plainbox.vendor.rpyc.lib import get_methods, get_id_pack |
3031 | 7 | from plainbox.vendor.rpyc.lib.compat import pickle, maxint, with_metaclass | 7 | from plainbox.vendor.rpyc.lib.compat import pickle, is_py_3k, maxint, with_metaclass |
3032 | 8 | from plainbox.vendor.rpyc.core import consts | 8 | from plainbox.vendor.rpyc.core import consts |
3033 | 9 | 9 | ||
3034 | 10 | 10 | ||
3035 | @@ -16,7 +16,6 @@ DELETED_ATTRS = frozenset([ | |||
3036 | 16 | '__array_struct__', '__array_interface__', | 16 | '__array_struct__', '__array_interface__', |
3037 | 17 | ]) | 17 | ]) |
3038 | 18 | 18 | ||
3039 | 19 | """the set of attributes that are local to the netref object""" | ||
3040 | 20 | LOCAL_ATTRS = frozenset([ | 19 | LOCAL_ATTRS = frozenset([ |
3041 | 21 | '____conn__', '____id_pack__', '____refcount__', '__class__', '__cmp__', '__del__', '__delattr__', | 20 | '____conn__', '____id_pack__', '____refcount__', '__class__', '__cmp__', '__del__', '__delattr__', |
3042 | 22 | '__dir__', '__doc__', '__getattr__', '__getattribute__', '__hash__', '__instancecheck__', | 21 | '__dir__', '__doc__', '__getattr__', '__getattribute__', '__hash__', '__instancecheck__', |
3043 | @@ -25,25 +24,39 @@ LOCAL_ATTRS = frozenset([ | |||
3044 | 25 | '__weakref__', '__dict__', '__methods__', '__exit__', | 24 | '__weakref__', '__dict__', '__methods__', '__exit__', |
3045 | 26 | '__eq__', '__ne__', '__lt__', '__gt__', '__le__', '__ge__', | 25 | '__eq__', '__ne__', '__lt__', '__gt__', '__le__', '__ge__', |
3046 | 27 | ]) | DELETED_ATTRS | 26 | ]) | DELETED_ATTRS |
3047 | 27 | """the set of attributes that are local to the netref object""" | ||
3048 | 28 | 28 | ||
3049 | 29 | """a list of types considered built-in (shared between connections) | ||
3050 | 30 | this is needed because iterating the members of the builtins module is not enough, | ||
3051 | 31 | some types (e.g NoneType) are not members of the builtins module. | ||
3052 | 32 | TODO: this list is not complete. | ||
3053 | 33 | """ | ||
3054 | 34 | _builtin_types = [ | 29 | _builtin_types = [ |
3055 | 35 | type, object, bool, complex, dict, float, int, list, slice, str, tuple, set, | 30 | type, object, bool, complex, dict, float, int, list, slice, str, tuple, set, |
3057 | 36 | frozenset, BaseException, Exception, type(None), types.BuiltinFunctionType, types.GeneratorType, | 31 | frozenset, Exception, type(None), types.BuiltinFunctionType, types.GeneratorType, |
3058 | 37 | types.MethodType, types.CodeType, types.FrameType, types.TracebackType, | 32 | types.MethodType, types.CodeType, types.FrameType, types.TracebackType, |
3060 | 38 | types.ModuleType, types.FunctionType, types.MappingProxyType, | 33 | types.ModuleType, types.FunctionType, |
3061 | 39 | 34 | ||
3062 | 40 | type(int.__add__), # wrapper_descriptor | 35 | type(int.__add__), # wrapper_descriptor |
3063 | 41 | type((1).__add__), # method-wrapper | 36 | type((1).__add__), # method-wrapper |
3064 | 42 | type(iter([])), # listiterator | 37 | type(iter([])), # listiterator |
3065 | 43 | type(iter(())), # tupleiterator | 38 | type(iter(())), # tupleiterator |
3066 | 44 | type(iter(set())), # setiterator | 39 | type(iter(set())), # setiterator |
3067 | 45 | bytes, bytearray, type(iter(range(10))), memoryview | ||
3068 | 46 | ] | 40 | ] |
3069 | 41 | """a list of types considered built-in (shared between connections)""" | ||
3070 | 42 | |||
3071 | 43 | try: | ||
3072 | 44 | BaseException | ||
3073 | 45 | except NameError: | ||
3074 | 46 | pass | ||
3075 | 47 | else: | ||
3076 | 48 | _builtin_types.append(BaseException) | ||
3077 | 49 | |||
3078 | 50 | if is_py_3k: | ||
3079 | 51 | _builtin_types.extend([ | ||
3080 | 52 | bytes, bytearray, type(iter(range(10))), memoryview, | ||
3081 | 53 | ]) | ||
3082 | 54 | xrange = range | ||
3083 | 55 | else: | ||
3084 | 56 | _builtin_types.extend([ | ||
3085 | 57 | basestring, unicode, long, xrange, type(iter(xrange(10))), file, # noqa | ||
3086 | 58 | types.InstanceType, types.ClassType, types.DictProxyType, | ||
3087 | 59 | ]) | ||
3088 | 47 | _normalized_builtin_types = {} | 60 | _normalized_builtin_types = {} |
3089 | 48 | 61 | ||
3090 | 49 | 62 | ||
3091 | @@ -88,9 +101,9 @@ class NetrefMetaclass(type): | |||
3092 | 88 | 101 | ||
3093 | 89 | def __repr__(self): | 102 | def __repr__(self): |
3094 | 90 | if self.__module__: | 103 | if self.__module__: |
3096 | 91 | return "<netref class '{}.{}'>".format((self.__module__), (self.__name__)) | 104 | return "<netref class '%s.%s'>" % (self.__module__, self.__name__) |
3097 | 92 | else: | 105 | else: |
3099 | 93 | return "<netref class '{}'>".format((self.__name__)) | 106 | return "<netref class '%s'>" % (self.__name__,) |
3100 | 94 | 107 | ||
3101 | 95 | 108 | ||
3102 | 96 | class BaseNetref(with_metaclass(NetrefMetaclass, object)): | 109 | class BaseNetref(with_metaclass(NetrefMetaclass, object)): |
3103 | @@ -303,24 +316,19 @@ def class_factory(id_pack, methods): | |||
3104 | 303 | name_pack = id_pack[0] | 316 | name_pack = id_pack[0] |
3105 | 304 | class_descriptor = None | 317 | class_descriptor = None |
3106 | 305 | if name_pack is not None: | 318 | if name_pack is not None: |
3125 | 306 | # attempt to resolve __class__ using normalized builtins first | 319 | # attempt to resolve __class__ using sys.modules (i.e. builtins and imported modules) |
3126 | 307 | _builtin_class = _normalized_builtin_types.get(name_pack) | 320 | _module = None |
3127 | 308 | if _builtin_class is not None: | 321 | cursor = len(name_pack) |
3128 | 309 | class_descriptor = NetrefClass(_builtin_class) | 322 | while cursor != -1: |
3129 | 310 | # then by imported modules (this also tries all builtins under "builtins") | 323 | _module = sys.modules.get(name_pack[:cursor]) |
3130 | 311 | else: | 324 | if _module is None: |
3131 | 312 | _module = None | 325 | cursor = name_pack[:cursor].rfind('.') |
3132 | 313 | cursor = len(name_pack) | 326 | continue |
3133 | 314 | while cursor != -1: | 327 | _class_name = name_pack[cursor + 1:] |
3134 | 315 | _module = sys.modules.get(name_pack[:cursor]) | 328 | _class = getattr(_module, _class_name, None) |
3135 | 316 | if _module is None: | 329 | if _class is not None and hasattr(_class, '__class__'): |
3136 | 317 | cursor = name_pack[:cursor].rfind('.') | 330 | class_descriptor = NetrefClass(_class) |
3137 | 318 | continue | 331 | break |
3120 | 319 | _class_name = name_pack[cursor + 1:] | ||
3121 | 320 | _class = getattr(_module, _class_name, None) | ||
3122 | 321 | if _class is not None and hasattr(_class, '__class__'): | ||
3123 | 322 | class_descriptor = NetrefClass(_class) | ||
3124 | 323 | break | ||
3138 | 324 | ns['__class__'] = class_descriptor | 332 | ns['__class__'] = class_descriptor |
3139 | 325 | netref_name = class_descriptor.owner.__name__ if class_descriptor is not None else name_pack | 333 | netref_name = class_descriptor.owner.__name__ if class_descriptor is not None else name_pack |
3140 | 326 | # create methods that must perform a syncreq | 334 | # create methods that must perform a syncreq |
3141 | diff --git a/plainbox/vendor/rpyc/core/protocol.py b/plainbox/vendor/rpyc/core/protocol.py | |||
3142 | index 703d499..56cd9a9 100644 | |||
3143 | --- a/plainbox/vendor/rpyc/core/protocol.py | |||
3144 | +++ b/plainbox/vendor/rpyc/core/protocol.py | |||
3145 | @@ -8,7 +8,7 @@ import gc # noqa: F401 | |||
3146 | 8 | 8 | ||
3147 | 9 | from threading import Lock, Condition | 9 | from threading import Lock, Condition |
3148 | 10 | from plainbox.vendor.rpyc.lib import spawn, Timeout, get_methods, get_id_pack | 10 | from plainbox.vendor.rpyc.lib import spawn, Timeout, get_methods, get_id_pack |
3150 | 11 | from plainbox.vendor.rpyc.lib.compat import pickle, next, maxint, select_error, acquire_lock # noqa: F401 | 11 | from plainbox.vendor.rpyc.lib.compat import pickle, next, is_py_3k, maxint, select_error, acquire_lock # noqa: F401 |
3151 | 12 | from plainbox.vendor.rpyc.lib.colls import WeakValueDict, RefCountingColl | 12 | from plainbox.vendor.rpyc.lib.colls import WeakValueDict, RefCountingColl |
3152 | 13 | from plainbox.vendor.rpyc.core import consts, brine, vinegar, netref | 13 | from plainbox.vendor.rpyc.core import consts, brine, vinegar, netref |
3153 | 14 | from plainbox.vendor.rpyc.core.async_ import AsyncResult | 14 | from plainbox.vendor.rpyc.core.async_ import AsyncResult |
3154 | @@ -39,7 +39,7 @@ DEFAULT_CONFIG = dict( | |||
3155 | 39 | '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', | 39 | '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', |
3156 | 40 | '__rxor__', '__setitem__', '__setslice__', '__str__', '__sub__', | 40 | '__rxor__', '__setitem__', '__setslice__', '__str__', '__sub__', |
3157 | 41 | '__truediv__', '__xor__', 'next', '__length_hint__', '__enter__', | 41 | '__truediv__', '__xor__', 'next', '__length_hint__', '__enter__', |
3159 | 42 | '__exit__', '__next__', '__format__']), | 42 | '__exit__', '__next__', ]), |
3160 | 43 | exposed_prefix="exposed_", | 43 | exposed_prefix="exposed_", |
3161 | 44 | allow_getattr=True, | 44 | allow_getattr=True, |
3162 | 45 | allow_setattr=False, | 45 | allow_setattr=False, |
3163 | @@ -60,8 +60,6 @@ DEFAULT_CONFIG = dict( | |||
3164 | 60 | endpoints=None, | 60 | endpoints=None, |
3165 | 61 | logger=None, | 61 | logger=None, |
3166 | 62 | sync_request_timeout=30, | 62 | sync_request_timeout=30, |
3167 | 63 | before_closed=None, | ||
3168 | 64 | close_catchall=False, | ||
3169 | 65 | ) | 63 | ) |
3170 | 66 | """ | 64 | """ |
3171 | 67 | The default configuration dictionary of the protocol. You can override these parameters | 65 | The default configuration dictionary of the protocol. You can override these parameters |
3172 | @@ -140,7 +138,7 @@ class Connection(object): | |||
3173 | 140 | self._config = DEFAULT_CONFIG.copy() | 138 | self._config = DEFAULT_CONFIG.copy() |
3174 | 141 | self._config.update(config) | 139 | self._config.update(config) |
3175 | 142 | if self._config["connid"] is None: | 140 | if self._config["connid"] is None: |
3177 | 143 | self._config["connid"] = "conn{}".format((next(_connection_id_generator))) | 141 | self._config["connid"] = "conn%d" % (next(_connection_id_generator),) |
3178 | 144 | 142 | ||
3179 | 145 | self._HANDLERS = self._request_handlers() | 143 | self._HANDLERS = self._request_handlers() |
3180 | 146 | self._channel = channel | 144 | self._channel = channel |
3181 | @@ -169,7 +167,7 @@ class Connection(object): | |||
3182 | 169 | 167 | ||
3183 | 170 | def __repr__(self): | 168 | def __repr__(self): |
3184 | 171 | a, b = object.__repr__(self).split(" object ") | 169 | a, b = object.__repr__(self).split(" object ") |
3186 | 172 | return "{} {!r} object {}".format((a), (self._config['connid']), (b)) | 170 | return "%s %r object %s" % (a, self._config["connid"], b) |
3187 | 173 | 171 | ||
3188 | 174 | def _cleanup(self, _anyway=True): # IO | 172 | def _cleanup(self, _anyway=True): # IO |
3189 | 175 | if self._closed and not _anyway: | 173 | if self._closed and not _anyway: |
3190 | @@ -188,19 +186,17 @@ class Connection(object): | |||
3191 | 188 | # self._config.clear() | 186 | # self._config.clear() |
3192 | 189 | del self._HANDLERS | 187 | del self._HANDLERS |
3193 | 190 | 188 | ||
3195 | 191 | def close(self): # IO | 189 | def close(self, _catchall=True): # IO |
3196 | 192 | """closes the connection, releasing all held resources""" | 190 | """closes the connection, releasing all held resources""" |
3197 | 193 | if self._closed: | 191 | if self._closed: |
3198 | 194 | return | 192 | return |
3199 | 193 | self._closed = True | ||
3200 | 195 | try: | 194 | try: |
3201 | 196 | self._closed = True | ||
3202 | 197 | if self._config.get("before_closed"): | ||
3203 | 198 | self._config["before_closed"](self.root) | ||
3204 | 199 | self._async_request(consts.HANDLE_CLOSE) | 195 | self._async_request(consts.HANDLE_CLOSE) |
3205 | 200 | except EOFError: | 196 | except EOFError: |
3206 | 201 | pass | 197 | pass |
3207 | 202 | except Exception: | 198 | except Exception: |
3209 | 203 | if not self._config["close_catchall"]: | 199 | if not _catchall: |
3210 | 204 | raise | 200 | raise |
3211 | 205 | finally: | 201 | finally: |
3212 | 206 | self._cleanup(_anyway=True) | 202 | self._cleanup(_anyway=True) |
3213 | @@ -298,7 +294,7 @@ class Connection(object): | |||
3214 | 298 | proxy = self._netref_factory(id_pack) | 294 | proxy = self._netref_factory(id_pack) |
3215 | 299 | self._proxy_cache[id_pack] = proxy | 295 | self._proxy_cache[id_pack] = proxy |
3216 | 300 | return proxy | 296 | return proxy |
3218 | 301 | raise ValueError("invalid label {!r}".format((label))) | 297 | raise ValueError("invalid label %r" % (label,)) |
3219 | 302 | 298 | ||
3220 | 303 | def _netref_factory(self, id_pack): # boxing | 299 | def _netref_factory(self, id_pack): # boxing |
3221 | 304 | """id_pack is for remote, so when class id fails to directly match """ | 300 | """id_pack is for remote, so when class id fails to directly match """ |
3222 | @@ -367,7 +363,7 @@ class Connection(object): | |||
3223 | 367 | obj = self._unbox_exc(args) | 363 | obj = self._unbox_exc(args) |
3224 | 368 | self._seq_request_callback(msg, seq, True, obj) | 364 | self._seq_request_callback(msg, seq, True, obj) |
3225 | 369 | else: | 365 | else: |
3227 | 370 | raise ValueError("invalid message type: {!r}".format((msg))) | 366 | raise ValueError("invalid message type: %r" % (msg,)) |
3228 | 371 | 367 | ||
3229 | 372 | def serve(self, timeout=1, wait_for_lock=True): # serving | 368 | def serve(self, timeout=1, wait_for_lock=True): # serving |
3230 | 373 | """Serves a single request or reply that arrives within the given | 369 | """Serves a single request or reply that arrives within the given |
3231 | @@ -492,7 +488,7 @@ class Connection(object): | |||
3232 | 492 | """ | 488 | """ |
3233 | 493 | timeout = kwargs.pop("timeout", None) | 489 | timeout = kwargs.pop("timeout", None) |
3234 | 494 | if kwargs: | 490 | if kwargs: |
3236 | 495 | raise TypeError("got unexpected keyword argument(s) {list(kwargs.keys()}") | 491 | raise TypeError("got unexpected keyword argument(s) %s" % (list(kwargs.keys()),)) |
3237 | 496 | res = AsyncResult(self) | 492 | res = AsyncResult(self) |
3238 | 497 | self._async_request(handler, args, res) | 493 | self._async_request(handler, args, res) |
3239 | 498 | if timeout is not None: | 494 | if timeout is not None: |
3240 | @@ -509,7 +505,7 @@ class Connection(object): | |||
3241 | 509 | def _check_attr(self, obj, name, perm): # attribute access | 505 | def _check_attr(self, obj, name, perm): # attribute access |
3242 | 510 | config = self._config | 506 | config = self._config |
3243 | 511 | if not config[perm]: | 507 | if not config[perm]: |
3245 | 512 | raise AttributeError("cannot access {!r}".format((name))) | 508 | raise AttributeError("cannot access %r" % (name,)) |
3246 | 513 | prefix = config["allow_exposed_attrs"] and config["exposed_prefix"] | 509 | prefix = config["allow_exposed_attrs"] and config["exposed_prefix"] |
3247 | 514 | plain = config["allow_all_attrs"] | 510 | plain = config["allow_all_attrs"] |
3248 | 515 | plain |= config["allow_exposed_attrs"] and name.startswith(prefix) | 511 | plain |= config["allow_exposed_attrs"] and name.startswith(prefix) |
3249 | @@ -522,13 +518,18 @@ class Connection(object): | |||
3250 | 522 | return prefix + name | 518 | return prefix + name |
3251 | 523 | if plain: | 519 | if plain: |
3252 | 524 | return name # chance for better traceback | 520 | return name # chance for better traceback |
3254 | 525 | raise AttributeError("cannot access {!r}".format((name))) | 521 | raise AttributeError("cannot access %r" % (name,)) |
3255 | 526 | 522 | ||
3256 | 527 | def _access_attr(self, obj, name, args, overrider, param, default): # attribute access | 523 | def _access_attr(self, obj, name, args, overrider, param, default): # attribute access |
3261 | 528 | if type(name) is bytes: | 524 | if is_py_3k: |
3262 | 529 | name = str(name, "utf8") | 525 | if type(name) is bytes: |
3263 | 530 | elif type(name) is not str: | 526 | name = str(name, "utf8") |
3264 | 531 | raise TypeError("name must be a string") | 527 | elif type(name) is not str: |
3265 | 528 | raise TypeError("name must be a string") | ||
3266 | 529 | else: | ||
3267 | 530 | if type(name) not in (str, unicode): # noqa | ||
3268 | 531 | raise TypeError("name must be a string") | ||
3269 | 532 | name = str(name) # IronPython issue #10 + py3k issue | ||
3270 | 532 | accessor = getattr(type(obj), overrider, None) | 533 | accessor = getattr(type(obj), overrider, None) |
3271 | 533 | if accessor is None: | 534 | if accessor is None: |
3272 | 534 | accessor = default | 535 | accessor = default |
3273 | @@ -637,7 +638,7 @@ class Connection(object): | |||
3274 | 637 | # since __mro__ is not a safe attribute the request is forwarded using the proxy connection | 638 | # since __mro__ is not a safe attribute the request is forwarded using the proxy connection |
3275 | 638 | # relates to issue #346 or tests.test_netref_hierachy.Test_Netref_Hierarchy.test_StandardError | 639 | # relates to issue #346 or tests.test_netref_hierachy.Test_Netref_Hierarchy.test_StandardError |
3276 | 639 | conn = obj.____conn__ | 640 | conn = obj.____conn__ |
3278 | 640 | return conn.sync_request(consts.HANDLE_INSPECT, other_id_pack) | 641 | return conn.sync_request(consts.HANDLE_INSPECT, id_pack) |
3279 | 641 | # Create a name pack which would be familiar here and see if there is a hit | 642 | # Create a name pack which would be familiar here and see if there is a hit |
3280 | 642 | other_id_pack2 = (other_id_pack[0], other_id_pack[1], 0) | 643 | other_id_pack2 = (other_id_pack[0], other_id_pack[1], 0) |
3281 | 643 | if other_id_pack[0] in netref.builtin_classes_cache: | 644 | if other_id_pack[0] in netref.builtin_classes_cache: |
3282 | diff --git a/plainbox/vendor/rpyc/core/service.py b/plainbox/vendor/rpyc/core/service.py | |||
3283 | index 6159b75..55526be 100644 | |||
3284 | --- a/plainbox/vendor/rpyc/core/service.py | |||
3285 | +++ b/plainbox/vendor/rpyc/core/service.py | |||
3286 | @@ -9,7 +9,7 @@ can interoperate, you're good to go. | |||
3287 | 9 | from functools import partial | 9 | from functools import partial |
3288 | 10 | 10 | ||
3289 | 11 | from plainbox.vendor.rpyc.lib import hybridmethod | 11 | from plainbox.vendor.rpyc.lib import hybridmethod |
3291 | 12 | from plainbox.vendor.rpyc.lib.compat import execute | 12 | from plainbox.vendor.rpyc.lib.compat import execute, is_py_3k |
3292 | 13 | from plainbox.vendor.rpyc.core.protocol import Connection | 13 | from plainbox.vendor.rpyc.core.protocol import Connection |
3293 | 14 | 14 | ||
3294 | 15 | 15 | ||
3295 | @@ -137,10 +137,7 @@ class ModuleNamespace(object): | |||
3296 | 137 | return self.__cache[name] | 137 | return self.__cache[name] |
3297 | 138 | 138 | ||
3298 | 139 | def __getattr__(self, name): | 139 | def __getattr__(self, name): |
3303 | 140 | try: | 140 | return self[name] |
3300 | 141 | return self[name] | ||
3301 | 142 | except ImportError: | ||
3302 | 143 | raise AttributeError(name) | ||
3304 | 144 | 141 | ||
3305 | 145 | 142 | ||
3306 | 146 | class Slave(object): | 143 | class Slave(object): |
3307 | @@ -218,13 +215,14 @@ class MasterService(Service): | |||
3308 | 218 | @staticmethod | 215 | @staticmethod |
3309 | 219 | def _install(conn, slave): | 216 | def _install(conn, slave): |
3310 | 220 | modules = ModuleNamespace(slave.getmodule) | 217 | modules = ModuleNamespace(slave.getmodule) |
3311 | 218 | builtin = modules.builtins if is_py_3k else modules.__builtin__ | ||
3312 | 221 | conn.modules = modules | 219 | conn.modules = modules |
3313 | 222 | conn.eval = slave.eval | 220 | conn.eval = slave.eval |
3314 | 223 | conn.execute = slave.execute | 221 | conn.execute = slave.execute |
3315 | 224 | conn.namespace = slave.namespace | 222 | conn.namespace = slave.namespace |
3319 | 225 | conn.builtins = modules.builtins | 223 | conn.builtin = builtin |
3320 | 226 | conn.builtin = modules.builtins # TODO: cruft from py2 that requires cleanup elsewhere and CHANGELOG note | 224 | conn.builtins = builtin |
3321 | 227 | from rpyc.utils.classic import teleport_function | 225 | from plainbox.vendor.rpyc.utils.classic import teleport_function |
3322 | 228 | conn.teleport = partial(teleport_function, conn) | 226 | conn.teleport = partial(teleport_function, conn) |
3323 | 229 | 227 | ||
3324 | 230 | 228 | ||
3325 | diff --git a/plainbox/vendor/rpyc/core/stream.py b/plainbox/vendor/rpyc/core/stream.py | |||
3326 | index 7d3cab7..6c8d790 100644 | |||
3327 | --- a/plainbox/vendor/rpyc/core/stream.py | |||
3328 | +++ b/plainbox/vendor/rpyc/core/stream.py | |||
3329 | @@ -8,7 +8,6 @@ import socket | |||
3330 | 8 | import errno | 8 | import errno |
3331 | 9 | from plainbox.vendor.rpyc.lib import safe_import, Timeout, socket_backoff_connect | 9 | from plainbox.vendor.rpyc.lib import safe_import, Timeout, socket_backoff_connect |
3332 | 10 | from plainbox.vendor.rpyc.lib.compat import poll, select_error, BYTES_LITERAL, get_exc_errno, maxint # noqa: F401 | 10 | from plainbox.vendor.rpyc.lib.compat import poll, select_error, BYTES_LITERAL, get_exc_errno, maxint # noqa: F401 |
3333 | 11 | from plainbox.vendor.rpyc.core.consts import STREAM_CHUNK | ||
3334 | 12 | win32file = safe_import("win32file") | 11 | win32file = safe_import("win32file") |
3335 | 13 | win32pipe = safe_import("win32pipe") | 12 | win32pipe = safe_import("win32pipe") |
3336 | 14 | win32event = safe_import("win32event") | 13 | win32event = safe_import("win32event") |
3337 | @@ -112,7 +111,7 @@ class SocketStream(Stream): | |||
3338 | 112 | """A stream over a socket""" | 111 | """A stream over a socket""" |
3339 | 113 | 112 | ||
3340 | 114 | __slots__ = ("sock",) | 113 | __slots__ = ("sock",) |
3342 | 115 | MAX_IO_CHUNK = STREAM_CHUNK | 114 | MAX_IO_CHUNK = 64000 # read/write chunk is 64KB, too large of a value will degrade response for other clients |
3343 | 116 | 115 | ||
3344 | 117 | def __init__(self, sock): | 116 | def __init__(self, sock): |
3345 | 118 | self.sock = sock | 117 | self.sock = sock |
3346 | @@ -292,7 +291,7 @@ class PipeStream(Stream): | |||
3347 | 292 | """A stream over two simplex pipes (one used to input, another for output)""" | 291 | """A stream over two simplex pipes (one used to input, another for output)""" |
3348 | 293 | 292 | ||
3349 | 294 | __slots__ = ("incoming", "outgoing") | 293 | __slots__ = ("incoming", "outgoing") |
3351 | 295 | MAX_IO_CHUNK = STREAM_CHUNK | 294 | MAX_IO_CHUNK = 32000 |
3352 | 296 | 295 | ||
3353 | 297 | def __init__(self, incoming, outgoing): | 296 | def __init__(self, incoming, outgoing): |
3354 | 298 | outgoing.flush() | 297 | outgoing.flush() |
3355 | @@ -370,7 +369,7 @@ class Win32PipeStream(Stream): | |||
3356 | 370 | 369 | ||
3357 | 371 | __slots__ = ("incoming", "outgoing", "_fileno", "_keepalive") | 370 | __slots__ = ("incoming", "outgoing", "_fileno", "_keepalive") |
3358 | 372 | PIPE_BUFFER_SIZE = 130000 | 371 | PIPE_BUFFER_SIZE = 130000 |
3360 | 373 | MAX_IO_CHUNK = STREAM_CHUNK | 372 | MAX_IO_CHUNK = 32000 |
3361 | 374 | 373 | ||
3362 | 375 | def __init__(self, incoming, outgoing): | 374 | def __init__(self, incoming, outgoing): |
3363 | 376 | import msvcrt | 375 | import msvcrt |
3364 | diff --git a/plainbox/vendor/rpyc/core/vinegar.py b/plainbox/vendor/rpyc/core/vinegar.py | |||
3365 | index db1f3e1..b766b58 100644 | |||
3366 | --- a/plainbox/vendor/rpyc/core/vinegar.py | |||
3367 | +++ b/plainbox/vendor/rpyc/core/vinegar.py | |||
3368 | @@ -22,6 +22,7 @@ except ImportError: | |||
3369 | 22 | from plainbox.vendor.rpyc.core import brine | 22 | from plainbox.vendor.rpyc.core import brine |
3370 | 23 | from plainbox.vendor.rpyc.core import consts | 23 | from plainbox.vendor.rpyc.core import consts |
3371 | 24 | from plainbox.vendor.rpyc import version | 24 | from plainbox.vendor.rpyc import version |
3372 | 25 | from plainbox.vendor.rpyc.lib.compat import is_py_3k | ||
3373 | 25 | 26 | ||
3374 | 26 | 27 | ||
3375 | 27 | REMOTE_LINE_START = "\n\n========= Remote Traceback " | 28 | REMOTE_LINE_START = "\n\n========= Remote Traceback " |
3376 | @@ -29,6 +30,13 @@ REMOTE_LINE_END = " =========\n" | |||
3377 | 29 | REMOTE_LINE = "{0}({{}}){1}".format(REMOTE_LINE_START, REMOTE_LINE_END) | 30 | REMOTE_LINE = "{0}({{}}){1}".format(REMOTE_LINE_START, REMOTE_LINE_END) |
3378 | 30 | 31 | ||
3379 | 31 | 32 | ||
3380 | 33 | try: | ||
3381 | 34 | BaseException | ||
3382 | 35 | except NameError: | ||
3383 | 36 | # python 2.4 compatible | ||
3384 | 37 | BaseException = Exception | ||
3385 | 38 | |||
3386 | 39 | |||
3387 | 32 | def dump(typ, val, tb, include_local_traceback, include_local_version): | 40 | def dump(typ, val, tb, include_local_traceback, include_local_version): |
3388 | 33 | """Dumps the given exceptions info, as returned by ``sys.exc_info()`` | 41 | """Dumps the given exceptions info, as returned by ``sys.exc_info()`` |
3389 | 34 | 42 | ||
3390 | @@ -127,15 +135,23 @@ def load(val, import_custom_exceptions, instantiate_custom_exceptions, instantia | |||
3391 | 127 | else: | 135 | else: |
3392 | 128 | cls = None | 136 | cls = None |
3393 | 129 | 137 | ||
3396 | 130 | if not isinstance(cls, type) or not issubclass(cls, BaseException): | 138 | if is_py_3k: |
3397 | 131 | cls = None | 139 | if not isinstance(cls, type) or not issubclass(cls, BaseException): |
3398 | 140 | cls = None | ||
3399 | 141 | else: | ||
3400 | 142 | if not isinstance(cls, (type, ClassType)): | ||
3401 | 143 | cls = None | ||
3402 | 144 | elif issubclass(cls, ClassType) and not instantiate_oldstyle_exceptions: | ||
3403 | 145 | cls = None | ||
3404 | 146 | elif not issubclass(cls, BaseException): | ||
3405 | 147 | cls = None | ||
3406 | 132 | 148 | ||
3407 | 133 | if cls is None: | 149 | if cls is None: |
3409 | 134 | fullname = "{}.{}".format((modname), (clsname)) | 150 | fullname = "%s.%s" % (modname, clsname) |
3410 | 135 | # py2: `type()` expects `str` not `unicode`! | 151 | # py2: `type()` expects `str` not `unicode`! |
3411 | 136 | fullname = str(fullname) | 152 | fullname = str(fullname) |
3412 | 137 | if fullname not in _generic_exceptions_cache: | 153 | if fullname not in _generic_exceptions_cache: |
3414 | 138 | fakemodule = {"__module__": "{}/{}".format((__name__), (modname))} | 154 | fakemodule = {"__module__": "%s/%s" % (__name__, modname)} |
3415 | 139 | if isinstance(GenericException, ClassType): | 155 | if isinstance(GenericException, ClassType): |
3416 | 140 | _generic_exceptions_cache[fullname] = ClassType(fullname, (GenericException,), fakemodule) | 156 | _generic_exceptions_cache[fullname] = ClassType(fullname, (GenericException,), fakemodule) |
3417 | 141 | else: | 157 | else: |
3418 | diff --git a/plainbox/vendor/rpyc/lib/__init__.py b/plainbox/vendor/rpyc/lib/__init__.py | |||
3419 | index 6462d36..d96656b 100644 | |||
3420 | --- a/plainbox/vendor/rpyc/lib/__init__.py | |||
3421 | +++ b/plainbox/vendor/rpyc/lib/__init__.py | |||
3422 | @@ -19,8 +19,8 @@ class MissingModule(object): | |||
3423 | 19 | 19 | ||
3424 | 20 | def __getattr__(self, name): | 20 | def __getattr__(self, name): |
3425 | 21 | if name.startswith("__"): # issue 71 | 21 | if name.startswith("__"): # issue 71 |
3428 | 22 | raise AttributeError("module {!r} not found".format((self.__name))) | 22 | raise AttributeError("module %r not found" % (self.__name,)) |
3429 | 23 | raise ImportError("module {!r} not found".format((self.__name))) | 23 | raise ImportError("module %r not found" % (self.__name,)) |
3430 | 24 | 24 | ||
3431 | 25 | def __bool__(self): | 25 | def __bool__(self): |
3432 | 26 | return False | 26 | return False |
3433 | @@ -96,7 +96,7 @@ def spawn_waitready(init, main): | |||
3434 | 96 | return thread, stack.pop() | 96 | return thread, stack.pop() |
3435 | 97 | 97 | ||
3436 | 98 | 98 | ||
3438 | 99 | class Timeout(object): | 99 | class Timeout: |
3439 | 100 | 100 | ||
3440 | 101 | def __init__(self, timeout): | 101 | def __init__(self, timeout): |
3441 | 102 | if isinstance(timeout, Timeout): | 102 | if isinstance(timeout, Timeout): |
3442 | @@ -175,7 +175,7 @@ def get_id_pack(obj): | |||
3443 | 175 | else: | 175 | else: |
3444 | 176 | name_pack = '{0}.{1}'.format(obj.__class__.__module__, obj.__name__) | 176 | name_pack = '{0}.{1}'.format(obj.__class__.__module__, obj.__name__) |
3445 | 177 | elif inspect.ismodule(obj): | 177 | elif inspect.ismodule(obj): |
3447 | 178 | name_pack = '{0}.{1}'.format(obj.__module__, obj.__name__) | 178 | name_pack = '{0}.{1}'.format(obj__module__, obj.__name__) |
3448 | 179 | print(name_pack) | 179 | print(name_pack) |
3449 | 180 | elif hasattr(obj, '__module__'): | 180 | elif hasattr(obj, '__module__'): |
3450 | 181 | name_pack = '{0}.{1}'.format(obj.__module__, obj.__name__) | 181 | name_pack = '{0}.{1}'.format(obj.__module__, obj.__name__) |
3451 | diff --git a/plainbox/vendor/rpyc/lib/compat.py b/plainbox/vendor/rpyc/lib/compat.py | |||
3452 | index 63c48fe..35be8fd 100644 | |||
3453 | --- a/plainbox/vendor/rpyc/lib/compat.py | |||
3454 | +++ b/plainbox/vendor/rpyc/lib/compat.py | |||
3455 | @@ -7,7 +7,6 @@ import time | |||
3456 | 7 | 7 | ||
3457 | 8 | is_py_3k = (sys.version_info[0] >= 3) | 8 | is_py_3k = (sys.version_info[0] >= 3) |
3458 | 9 | is_py_gte38 = is_py_3k and (sys.version_info[1] >= 8) | 9 | is_py_gte38 = is_py_3k and (sys.version_info[1] >= 8) |
3459 | 10 | is_py_gte37 = is_py_3k and (sys.version_info[1] >= 7) | ||
3460 | 11 | 10 | ||
3461 | 12 | 11 | ||
3462 | 13 | if is_py_3k: | 12 | if is_py_3k: |
3463 | diff --git a/plainbox/vendor/rpyc/utils/authenticators.py b/plainbox/vendor/rpyc/utils/authenticators.py | |||
3464 | index 63ee828..0d97882 100644 | |||
3465 | --- a/plainbox/vendor/rpyc/utils/authenticators.py | |||
3466 | +++ b/plainbox/vendor/rpyc/utils/authenticators.py | |||
3467 | @@ -71,7 +71,7 @@ class SSLAuthenticator(object): | |||
3468 | 71 | else: | 71 | else: |
3469 | 72 | self.cert_reqs = cert_reqs | 72 | self.cert_reqs = cert_reqs |
3470 | 73 | if ssl_version is None: | 73 | if ssl_version is None: |
3472 | 74 | self.ssl_version = ssl.PROTOCOL_TLS | 74 | self.ssl_version = ssl.PROTOCOL_TLSv1 |
3473 | 75 | else: | 75 | else: |
3474 | 76 | self.ssl_version = ssl_version | 76 | self.ssl_version = ssl_version |
3475 | 77 | 77 | ||
3476 | diff --git a/plainbox/vendor/rpyc/utils/classic.py b/plainbox/vendor/rpyc/utils/classic.py | |||
3477 | index 3903a79..2f97dcf 100644 | |||
3478 | --- a/plainbox/vendor/rpyc/utils/classic.py | |||
3479 | +++ b/plainbox/vendor/rpyc/utils/classic.py | |||
3480 | @@ -2,11 +2,10 @@ from __future__ import with_statement | |||
3481 | 2 | import sys | 2 | import sys |
3482 | 3 | import os | 3 | import os |
3483 | 4 | import inspect | 4 | import inspect |
3485 | 5 | from plainbox.vendor.rpyc.lib.compat import pickle, execute | 5 | from plainbox.vendor.rpyc.lib.compat import pickle, execute, is_py_3k # noqa: F401 |
3486 | 6 | from plainbox.vendor.rpyc.core.service import ClassicService, Slave | 6 | from plainbox.vendor.rpyc.core.service import ClassicService, Slave |
3487 | 7 | from plainbox.vendor.rpyc.utils import factory | 7 | from plainbox.vendor.rpyc.utils import factory |
3488 | 8 | from plainbox.vendor.rpyc.core.service import ModuleNamespace # noqa: F401 | 8 | from plainbox.vendor.rpyc.core.service import ModuleNamespace # noqa: F401 |
3489 | 9 | from plainbox.vendor.rpyc.core.consts import STREAM_CHUNK | ||
3490 | 10 | from contextlib import contextmanager | 9 | from contextlib import contextmanager |
3491 | 11 | 10 | ||
3492 | 12 | 11 | ||
3493 | @@ -172,7 +171,7 @@ def connect_multiprocess(args={}): | |||
3494 | 172 | # remoting utilities | 171 | # remoting utilities |
3495 | 173 | # =============================================================================== | 172 | # =============================================================================== |
3496 | 174 | 173 | ||
3498 | 175 | def upload(conn, localpath, remotepath, filter=None, ignore_invalid=False, chunk_size=STREAM_CHUNK): | 174 | def upload(conn, localpath, remotepath, filter=None, ignore_invalid=False, chunk_size=16000): |
3499 | 176 | """uploads a file or a directory to the given remote path | 175 | """uploads a file or a directory to the given remote path |
3500 | 177 | 176 | ||
3501 | 178 | :param localpath: the local file or directory | 177 | :param localpath: the local file or directory |
3502 | @@ -187,10 +186,10 @@ def upload(conn, localpath, remotepath, filter=None, ignore_invalid=False, chunk | |||
3503 | 187 | upload_file(conn, localpath, remotepath, chunk_size) | 186 | upload_file(conn, localpath, remotepath, chunk_size) |
3504 | 188 | else: | 187 | else: |
3505 | 189 | if not ignore_invalid: | 188 | if not ignore_invalid: |
3507 | 190 | raise ValueError("cannot upload {!r}".format((localpath))) | 189 | raise ValueError("cannot upload %r" % (localpath,)) |
3508 | 191 | 190 | ||
3509 | 192 | 191 | ||
3511 | 193 | def upload_file(conn, localpath, remotepath, chunk_size=STREAM_CHUNK): | 192 | def upload_file(conn, localpath, remotepath, chunk_size=16000): |
3512 | 194 | with open(localpath, "rb") as lf: | 193 | with open(localpath, "rb") as lf: |
3513 | 195 | with conn.builtin.open(remotepath, "wb") as rf: | 194 | with conn.builtin.open(remotepath, "wb") as rf: |
3514 | 196 | while True: | 195 | while True: |
3515 | @@ -200,7 +199,7 @@ def upload_file(conn, localpath, remotepath, chunk_size=STREAM_CHUNK): | |||
3516 | 200 | rf.write(buf) | 199 | rf.write(buf) |
3517 | 201 | 200 | ||
3518 | 202 | 201 | ||
3520 | 203 | def upload_dir(conn, localpath, remotepath, filter=None, chunk_size=STREAM_CHUNK): | 202 | def upload_dir(conn, localpath, remotepath, filter=None, chunk_size=16000): |
3521 | 204 | if not conn.modules.os.path.isdir(remotepath): | 203 | if not conn.modules.os.path.isdir(remotepath): |
3522 | 205 | conn.modules.os.makedirs(remotepath) | 204 | conn.modules.os.makedirs(remotepath) |
3523 | 206 | for fn in os.listdir(localpath): | 205 | for fn in os.listdir(localpath): |
3524 | @@ -210,7 +209,7 @@ def upload_dir(conn, localpath, remotepath, filter=None, chunk_size=STREAM_CHUNK | |||
3525 | 210 | upload(conn, lfn, rfn, filter=filter, ignore_invalid=True, chunk_size=chunk_size) | 209 | upload(conn, lfn, rfn, filter=filter, ignore_invalid=True, chunk_size=chunk_size) |
3526 | 211 | 210 | ||
3527 | 212 | 211 | ||
3529 | 213 | def download(conn, remotepath, localpath, filter=None, ignore_invalid=False, chunk_size=STREAM_CHUNK): | 212 | def download(conn, remotepath, localpath, filter=None, ignore_invalid=False, chunk_size=16000): |
3530 | 214 | """ | 213 | """ |
3531 | 215 | download a file or a directory to the given remote path | 214 | download a file or a directory to the given remote path |
3532 | 216 | 215 | ||
3533 | @@ -221,15 +220,15 @@ def download(conn, remotepath, localpath, filter=None, ignore_invalid=False, chu | |||
3534 | 221 | :param chunk_size: the IO chunk size | 220 | :param chunk_size: the IO chunk size |
3535 | 222 | """ | 221 | """ |
3536 | 223 | if conn.modules.os.path.isdir(remotepath): | 222 | if conn.modules.os.path.isdir(remotepath): |
3538 | 224 | download_dir(conn, remotepath, localpath, filter, chunk_size) | 223 | download_dir(conn, remotepath, localpath, filter) |
3539 | 225 | elif conn.modules.os.path.isfile(remotepath): | 224 | elif conn.modules.os.path.isfile(remotepath): |
3540 | 226 | download_file(conn, remotepath, localpath, chunk_size) | 225 | download_file(conn, remotepath, localpath, chunk_size) |
3541 | 227 | else: | 226 | else: |
3542 | 228 | if not ignore_invalid: | 227 | if not ignore_invalid: |
3544 | 229 | raise ValueError("cannot download {!r}".format((remotepath))) | 228 | raise ValueError("cannot download %r" % (remotepath,)) |
3545 | 230 | 229 | ||
3546 | 231 | 230 | ||
3548 | 232 | def download_file(conn, remotepath, localpath, chunk_size=STREAM_CHUNK): | 231 | def download_file(conn, remotepath, localpath, chunk_size=16000): |
3549 | 233 | with conn.builtin.open(remotepath, "rb") as rf: | 232 | with conn.builtin.open(remotepath, "rb") as rf: |
3550 | 234 | with open(localpath, "wb") as lf: | 233 | with open(localpath, "wb") as lf: |
3551 | 235 | while True: | 234 | while True: |
3552 | @@ -239,17 +238,17 @@ def download_file(conn, remotepath, localpath, chunk_size=STREAM_CHUNK): | |||
3553 | 239 | lf.write(buf) | 238 | lf.write(buf) |
3554 | 240 | 239 | ||
3555 | 241 | 240 | ||
3557 | 242 | def download_dir(conn, remotepath, localpath, filter=None, chunk_size=STREAM_CHUNK): | 241 | def download_dir(conn, remotepath, localpath, filter=None, chunk_size=16000): |
3558 | 243 | if not os.path.isdir(localpath): | 242 | if not os.path.isdir(localpath): |
3559 | 244 | os.makedirs(localpath) | 243 | os.makedirs(localpath) |
3560 | 245 | for fn in conn.modules.os.listdir(remotepath): | 244 | for fn in conn.modules.os.listdir(remotepath): |
3561 | 246 | if not filter or filter(fn): | 245 | if not filter or filter(fn): |
3562 | 247 | rfn = conn.modules.os.path.join(remotepath, fn) | 246 | rfn = conn.modules.os.path.join(remotepath, fn) |
3563 | 248 | lfn = os.path.join(localpath, fn) | 247 | lfn = os.path.join(localpath, fn) |
3565 | 249 | download(conn, rfn, lfn, filter=filter, ignore_invalid=True, chunk_size=chunk_size) | 248 | download(conn, rfn, lfn, filter=filter, ignore_invalid=True) |
3566 | 250 | 249 | ||
3567 | 251 | 250 | ||
3569 | 252 | def upload_package(conn, module, remotepath=None, chunk_size=STREAM_CHUNK): | 251 | def upload_package(conn, module, remotepath=None, chunk_size=16000): |
3570 | 253 | """ | 252 | """ |
3571 | 254 | uploads a module or a package to the remote party | 253 | uploads a module or a package to the remote party |
3572 | 255 | 254 | ||
3573 | @@ -385,17 +384,12 @@ def teleport_function(conn, func, globals=None, def_=True): | |||
3574 | 385 | import os | 384 | import os |
3575 | 386 | return (os.getpid() + y) * x | 385 | return (os.getpid() + y) * x |
3576 | 387 | 386 | ||
3577 | 388 | .. note:: While it is not forbidden to "teleport" functions across different Python | ||
3578 | 389 | versions, it *may* result in errors due to Python bytecode differences. It is | ||
3579 | 390 | recommended to ensure both the client and the server are of the same Python | ||
3580 | 391 | version when using this function. | ||
3581 | 392 | |||
3582 | 393 | :param conn: the RPyC connection | 387 | :param conn: the RPyC connection |
3583 | 394 | :param func: the function object to be delivered to the other party | 388 | :param func: the function object to be delivered to the other party |
3584 | 395 | """ | 389 | """ |
3585 | 396 | if globals is None: | 390 | if globals is None: |
3586 | 397 | globals = conn.namespace | 391 | globals = conn.namespace |
3588 | 398 | from rpyc.utils.teleportation import export_function | 392 | from plainbox.vendor.rpyc.utils.teleportation import export_function |
3589 | 399 | exported = export_function(func) | 393 | exported = export_function(func) |
3590 | 400 | return conn.modules["rpyc.utils.teleportation"].import_function( | 394 | return conn.modules["rpyc.utils.teleportation"].import_function( |
3591 | 401 | exported, globals, def_) | 395 | exported, globals, def_) |
3592 | diff --git a/plainbox/vendor/rpyc/utils/factory.py b/plainbox/vendor/rpyc/utils/factory.py | |||
3593 | index aa59a7e..23acc1f 100644 | |||
3594 | --- a/plainbox/vendor/rpyc/utils/factory.py | |||
3595 | +++ b/plainbox/vendor/rpyc/utils/factory.py | |||
3596 | @@ -5,7 +5,7 @@ cases) | |||
3597 | 5 | from __future__ import with_statement | 5 | from __future__ import with_statement |
3598 | 6 | import socket | 6 | import socket |
3599 | 7 | from contextlib import closing | 7 | from contextlib import closing |
3601 | 8 | from functools import partial | 8 | |
3602 | 9 | import threading | 9 | import threading |
3603 | 10 | try: | 10 | try: |
3604 | 11 | from thread import interrupt_main | 11 | from thread import interrupt_main |
3605 | @@ -19,7 +19,7 @@ except ImportError: | |||
3606 | 19 | 19 | ||
3607 | 20 | from plainbox.vendor.rpyc.core.channel import Channel | 20 | from plainbox.vendor.rpyc.core.channel import Channel |
3608 | 21 | from plainbox.vendor.rpyc.core.stream import SocketStream, TunneledSocketStream, PipeStream | 21 | from plainbox.vendor.rpyc.core.stream import SocketStream, TunneledSocketStream, PipeStream |
3610 | 22 | from plainbox.vendor.rpyc.core.service import VoidService, MasterService, SlaveService | 22 | from plainbox.vendor.rpyc.core.service import VoidService |
3611 | 23 | from plainbox.vendor.rpyc.utils.registry import UDPRegistryClient | 23 | from plainbox.vendor.rpyc.utils.registry import UDPRegistryClient |
3612 | 24 | from plainbox.vendor.rpyc.lib import safe_import, spawn | 24 | from plainbox.vendor.rpyc.lib import safe_import, spawn |
3613 | 25 | ssl = safe_import("ssl") | 25 | ssl = safe_import("ssl") |
3614 | @@ -29,10 +29,6 @@ class DiscoveryError(Exception): | |||
3615 | 29 | pass | 29 | pass |
3616 | 30 | 30 | ||
3617 | 31 | 31 | ||
3618 | 32 | class ForbiddenError(Exception): | ||
3619 | 33 | pass | ||
3620 | 34 | |||
3621 | 35 | |||
3622 | 36 | # ------------------------------------------------------------------------------ | 32 | # ------------------------------------------------------------------------------ |
3623 | 37 | # API | 33 | # API |
3624 | 38 | # ------------------------------------------------------------------------------ | 34 | # ------------------------------------------------------------------------------ |
3625 | @@ -220,26 +216,16 @@ def discover(service_name, host=None, registrar=None, timeout=2): | |||
3626 | 220 | registrar = UDPRegistryClient(timeout=timeout) | 216 | registrar = UDPRegistryClient(timeout=timeout) |
3627 | 221 | addrs = registrar.discover(service_name) | 217 | addrs = registrar.discover(service_name) |
3628 | 222 | if not addrs: | 218 | if not addrs: |
3630 | 223 | raise DiscoveryError("no servers exposing {!r} were found".format((service_name))) | 219 | raise DiscoveryError("no servers exposing %r were found" % (service_name,)) |
3631 | 224 | if host: | 220 | if host: |
3632 | 225 | ips = socket.gethostbyname_ex(host)[2] | 221 | ips = socket.gethostbyname_ex(host)[2] |
3633 | 226 | addrs = [(h, p) for h, p in addrs if h in ips] | 222 | addrs = [(h, p) for h, p in addrs if h in ips] |
3634 | 227 | if not addrs: | 223 | if not addrs: |
3636 | 228 | raise DiscoveryError("no servers exposing {} were found on {}".format((service_name), (host))) | 224 | raise DiscoveryError("no servers exposing %r were found on %r" % (service_name, host)) |
3637 | 229 | return addrs | 225 | return addrs |
3638 | 230 | 226 | ||
3639 | 231 | 227 | ||
3651 | 232 | def list_services(registrar=None, timeout=2): | 228 | def connect_by_service(service_name, host=None, service=VoidService, config={}): |
3641 | 233 | services = () | ||
3642 | 234 | if registrar is None: | ||
3643 | 235 | registrar = UDPRegistryClient(timeout=timeout) | ||
3644 | 236 | services = registrar.list() | ||
3645 | 237 | if services is None: | ||
3646 | 238 | raise ForbiddenError("Registry doesn't allow listing") | ||
3647 | 239 | return services | ||
3648 | 240 | |||
3649 | 241 | |||
3650 | 242 | def connect_by_service(service_name, host=None, registrar=None, timeout=2, service=VoidService, config={}): | ||
3652 | 243 | """create a connection to an arbitrary server that exposes the requested service | 229 | """create a connection to an arbitrary server that exposes the requested service |
3653 | 244 | 230 | ||
3654 | 245 | :param service_name: the service to discover | 231 | :param service_name: the service to discover |
3655 | @@ -254,13 +240,13 @@ def connect_by_service(service_name, host=None, registrar=None, timeout=2, servi | |||
3656 | 254 | # some of which could be dead. We iterate over the list returned and return the first | 240 | # some of which could be dead. We iterate over the list returned and return the first |
3657 | 255 | # one we could connect to. If none of the registered servers is responsive we re-throw | 241 | # one we could connect to. If none of the registered servers is responsive we re-throw |
3658 | 256 | # the exception | 242 | # the exception |
3660 | 257 | addrs = discover(service_name, host=host, registrar=registrar, timeout=timeout) | 243 | addrs = discover(service_name, host=host) |
3661 | 258 | for host, port in addrs: | 244 | for host, port in addrs: |
3662 | 259 | try: | 245 | try: |
3663 | 260 | return connect(host, port, service, config=config) | 246 | return connect(host, port, service, config=config) |
3664 | 261 | except socket.error: | 247 | except socket.error: |
3665 | 262 | pass | 248 | pass |
3667 | 263 | raise DiscoveryError("All services are down: {}".format((addrs))) | 249 | raise DiscoveryError("All services are down: %s" % (addrs,)) |
3668 | 264 | 250 | ||
3669 | 265 | 251 | ||
3670 | 266 | def connect_subproc(args, service=VoidService, config={}): | 252 | def connect_subproc(args, service=VoidService, config={}): |
3671 | @@ -278,27 +264,6 @@ def connect_subproc(args, service=VoidService, config={}): | |||
3672 | 278 | return conn | 264 | return conn |
3673 | 279 | 265 | ||
3674 | 280 | 266 | ||
3675 | 281 | def _server(listener, remote_service, remote_config, args=None): | ||
3676 | 282 | try: | ||
3677 | 283 | with closing(listener): | ||
3678 | 284 | client = listener.accept()[0] | ||
3679 | 285 | conn = connect_stream(SocketStream(client), service=remote_service, config=remote_config) | ||
3680 | 286 | if isinstance(args, dict): | ||
3681 | 287 | _oldstyle = (MasterService, SlaveService) | ||
3682 | 288 | is_newstyle = isinstance(remote_service, type) and not issubclass(remote_service, _oldstyle) | ||
3683 | 289 | is_newstyle |= not isinstance(remote_service, type) and not isinstance(remote_service, _oldstyle) | ||
3684 | 290 | is_voidservice = isinstance(remote_service, type) and issubclass(remote_service, VoidService) | ||
3685 | 291 | is_voidservice |= not isinstance(remote_service, type) and isinstance(remote_service, VoidService) | ||
3686 | 292 | if is_newstyle and not is_voidservice: | ||
3687 | 293 | conn._local_root.exposed_namespace.update(args) | ||
3688 | 294 | elif not is_voidservice: | ||
3689 | 295 | conn._local_root.namespace.update(args) | ||
3690 | 296 | |||
3691 | 297 | conn.serve_all() | ||
3692 | 298 | except KeyboardInterrupt: | ||
3693 | 299 | interrupt_main() | ||
3694 | 300 | |||
3695 | 301 | |||
3696 | 302 | def connect_thread(service=VoidService, config={}, remote_service=VoidService, remote_config={}): | 267 | def connect_thread(service=VoidService, config={}, remote_service=VoidService, remote_config={}): |
3697 | 303 | """starts an rpyc server on a new thread, bound to an arbitrary port, | 268 | """starts an rpyc server on a new thread, bound to an arbitrary port, |
3698 | 304 | and connects to it over a socket. | 269 | and connects to it over a socket. |
3699 | @@ -311,8 +276,18 @@ def connect_thread(service=VoidService, config={}, remote_service=VoidService, r | |||
3700 | 311 | listener = socket.socket() | 276 | listener = socket.socket() |
3701 | 312 | listener.bind(("localhost", 0)) | 277 | listener.bind(("localhost", 0)) |
3702 | 313 | listener.listen(1) | 278 | listener.listen(1) |
3705 | 314 | remote_server = partial(_server, listener, remote_service, remote_config) | 279 | |
3706 | 315 | spawn(remote_server) | 280 | def server(listener=listener): |
3707 | 281 | with closing(listener): | ||
3708 | 282 | client = listener.accept()[0] | ||
3709 | 283 | conn = connect_stream(SocketStream(client), service=remote_service, | ||
3710 | 284 | config=remote_config) | ||
3711 | 285 | try: | ||
3712 | 286 | conn.serve_all() | ||
3713 | 287 | except KeyboardInterrupt: | ||
3714 | 288 | interrupt_main() | ||
3715 | 289 | |||
3716 | 290 | spawn(server) | ||
3717 | 316 | host, port = listener.getsockname() | 291 | host, port = listener.getsockname() |
3718 | 317 | return connect(host, port, service=service, config=config) | 292 | return connect(host, port, service=service, config=config) |
3719 | 318 | 293 | ||
3720 | @@ -336,8 +311,19 @@ def connect_multiprocess(service=VoidService, config={}, remote_service=VoidServ | |||
3721 | 336 | listener = socket.socket() | 311 | listener = socket.socket() |
3722 | 337 | listener.bind(("localhost", 0)) | 312 | listener.bind(("localhost", 0)) |
3723 | 338 | listener.listen(1) | 313 | listener.listen(1) |
3726 | 339 | remote_server = partial(_server, listener, remote_service, remote_config, args) | 314 | |
3727 | 340 | t = Process(target=remote_server) | 315 | def server(listener=listener, args=args): |
3728 | 316 | with closing(listener): | ||
3729 | 317 | client = listener.accept()[0] | ||
3730 | 318 | conn = connect_stream(SocketStream(client), service=remote_service, config=remote_config) | ||
3731 | 319 | try: | ||
3732 | 320 | for k in args: | ||
3733 | 321 | conn._local_root.exposed_namespace[k] = args[k] | ||
3734 | 322 | conn.serve_all() | ||
3735 | 323 | except KeyboardInterrupt: | ||
3736 | 324 | interrupt_main() | ||
3737 | 325 | |||
3738 | 326 | t = Process(target=server) | ||
3739 | 341 | t.start() | 327 | t.start() |
3740 | 342 | host, port = listener.getsockname() | 328 | host, port = listener.getsockname() |
3741 | 343 | return connect(host, port, service=service, config=config) | 329 | return connect(host, port, service=service, config=config) |
3742 | diff --git a/plainbox/vendor/rpyc/utils/helpers.py b/plainbox/vendor/rpyc/utils/helpers.py | |||
3743 | index 67be083..521e226 100644 | |||
3744 | --- a/plainbox/vendor/rpyc/utils/helpers.py | |||
3745 | +++ b/plainbox/vendor/rpyc/utils/helpers.py | |||
3746 | @@ -34,7 +34,7 @@ def buffiter(obj, chunk=10, max_chunk=1000, factor=2): | |||
3747 | 34 | print id, name, dob | 34 | print id, name, dob |
3748 | 35 | """ | 35 | """ |
3749 | 36 | if factor < 1: | 36 | if factor < 1: |
3751 | 37 | raise ValueError("factor must be >= 1, got {!r}".format((factor))) | 37 | raise ValueError("factor must be >= 1, got %r" % (factor,)) |
3752 | 38 | it = iter(obj) | 38 | it = iter(obj) |
3753 | 39 | count = chunk | 39 | count = chunk |
3754 | 40 | while True: | 40 | while True: |
3755 | @@ -102,7 +102,7 @@ class _Async(object): | |||
3756 | 102 | return asyncreq(self.proxy, HANDLE_CALL, args, tuple(kwargs.items())) | 102 | return asyncreq(self.proxy, HANDLE_CALL, args, tuple(kwargs.items())) |
3757 | 103 | 103 | ||
3758 | 104 | def __repr__(self): | 104 | def __repr__(self): |
3760 | 105 | return "async_({!r})".format((self.proxy)) | 105 | return "async_(%r)" % (self.proxy,) |
3761 | 106 | 106 | ||
3762 | 107 | 107 | ||
3763 | 108 | _async_proxies_cache = WeakValueDict() | 108 | _async_proxies_cache = WeakValueDict() |
3764 | @@ -145,9 +145,9 @@ def async_(proxy): | |||
3765 | 145 | if pid in _async_proxies_cache: | 145 | if pid in _async_proxies_cache: |
3766 | 146 | return _async_proxies_cache[pid] | 146 | return _async_proxies_cache[pid] |
3767 | 147 | if not hasattr(proxy, "____conn__") or not hasattr(proxy, "____id_pack__"): | 147 | if not hasattr(proxy, "____conn__") or not hasattr(proxy, "____id_pack__"): |
3769 | 148 | raise TypeError("'proxy' must be a Netref: {!r}".format((proxy))) | 148 | raise TypeError("'proxy' must be a Netref: %r", (proxy,)) |
3770 | 149 | if not callable(proxy): | 149 | if not callable(proxy): |
3772 | 150 | raise TypeError("'proxy' must be callable: {!r}".format((proxy))) | 150 | raise TypeError("'proxy' must be callable: %r" % (proxy,)) |
3773 | 151 | caller = _Async(proxy) | 151 | caller = _Async(proxy) |
3774 | 152 | _async_proxies_cache[id(caller)] = _async_proxies_cache[pid] = caller | 152 | _async_proxies_cache[id(caller)] = _async_proxies_cache[pid] = caller |
3775 | 153 | return caller | 153 | return caller |
3776 | @@ -186,7 +186,7 @@ class timed(object): | |||
3777 | 186 | return res | 186 | return res |
3778 | 187 | 187 | ||
3779 | 188 | def __repr__(self): | 188 | def __repr__(self): |
3781 | 189 | return "timed({!r}, {!r})".format((self.proxy.proxy), (self.timeout)) | 189 | return "timed(%r, %r)" % (self.proxy.proxy, self.timeout) |
3782 | 190 | 190 | ||
3783 | 191 | 191 | ||
3784 | 192 | class BgServingThread(object): | 192 | class BgServingThread(object): |
3785 | diff --git a/plainbox/vendor/rpyc/utils/registry.py b/plainbox/vendor/rpyc/utils/registry.py | |||
3786 | index b9a58ff..315d9f3 100644 | |||
3787 | --- a/plainbox/vendor/rpyc/utils/registry.py | |||
3788 | +++ b/plainbox/vendor/rpyc/utils/registry.py | |||
3789 | @@ -1,8 +1,13 @@ | |||
3790 | 1 | """ | 1 | """ |
3792 | 2 | RPyC Registry Server maintains service information on RPyC services for *Service Registry and Discovery patterns*. Service Registry and Discovery patterns solve the connectivity problem for communication between services and external consumers. RPyC services will register with the server when :code:`auto_register` is :code:`True`. | 2 | RPyC **registry server** implementation. The registry is much like |
3793 | 3 | `Avahi <http://en.wikipedia.org/wiki/Avahi_(software)>`_ or | ||
3794 | 4 | `Bonjour <http://en.wikipedia.org/wiki/Bonjour_(software)>`_, but tailored to | ||
3795 | 5 | the needs of RPyC. Also, neither of them supports (or supported) Windows, | ||
3796 | 6 | and Bonjour has a restrictive license. Moreover, they are too "powerful" for | ||
3797 | 7 | what RPyC needed and required too complex a setup. | ||
3798 | 3 | 8 | ||
3801 | 4 | Service registries such as `Avahi <http://en.wikipedia.org/wiki/Avahi_(software)>`_ and | 9 | If anyone wants to implement the RPyC registry using Avahi, Bonjour, or any |
3802 | 5 | `Bonjour <http://en.wikipedia.org/wiki/Bonjour_(software)>`_ are alternatives to the RPyC Registry Server. These alternatives do no support Windows and have more restrictive licensing. | 10 | other zeroconf implementation -- I'll be happy to include them. |
3803 | 6 | 11 | ||
3804 | 7 | Refer to :file:`rpyc/scripts/rpyc_registry.py` for more info. | 12 | Refer to :file:`rpyc/scripts/rpyc_registry.py` for more info. |
3805 | 8 | """ | 13 | """ |
3806 | @@ -26,7 +31,7 @@ REGISTRY_PORT = 18811 | |||
3807 | 26 | class RegistryServer(object): | 31 | class RegistryServer(object): |
3808 | 27 | """Base registry server""" | 32 | """Base registry server""" |
3809 | 28 | 33 | ||
3811 | 29 | def __init__(self, listenersock, pruning_timeout=None, logger=None, allow_listing=False): | 34 | def __init__(self, listenersock, pruning_timeout=None, logger=None): |
3812 | 30 | self.sock = listenersock | 35 | self.sock = listenersock |
3813 | 31 | self.port = self.sock.getsockname()[1] | 36 | self.port = self.sock.getsockname()[1] |
3814 | 32 | self.active = False | 37 | self.active = False |
3815 | @@ -36,7 +41,6 @@ class RegistryServer(object): | |||
3816 | 36 | self.pruning_timeout = pruning_timeout | 41 | self.pruning_timeout = pruning_timeout |
3817 | 37 | if logger is None: | 42 | if logger is None: |
3818 | 38 | logger = self._get_logger() | 43 | logger = self._get_logger() |
3819 | 39 | self.allow_listing = allow_listing | ||
3820 | 40 | self.logger = logger | 44 | self.logger = logger |
3821 | 41 | 45 | ||
3822 | 42 | def _get_logger(self): | 46 | def _get_logger(self): |
3823 | @@ -75,7 +79,7 @@ class RegistryServer(object): | |||
3824 | 75 | def cmd_query(self, host, name): | 79 | def cmd_query(self, host, name): |
3825 | 76 | """implementation of the ``query`` command""" | 80 | """implementation of the ``query`` command""" |
3826 | 77 | name = name.upper() | 81 | name = name.upper() |
3828 | 78 | self.logger.debug("querying for {!r}".format((name))) | 82 | self.logger.debug("querying for %r", name) |
3829 | 79 | if name not in self.services: | 83 | if name not in self.services: |
3830 | 80 | self.logger.debug("no such service") | 84 | self.logger.debug("no such service") |
3831 | 81 | return () | 85 | return () |
3832 | @@ -85,35 +89,24 @@ class RegistryServer(object): | |||
3833 | 85 | servers = [] | 89 | servers = [] |
3834 | 86 | for addrinfo, t in all_servers: | 90 | for addrinfo, t in all_servers: |
3835 | 87 | if t < oldest: | 91 | if t < oldest: |
3837 | 88 | self.logger.debug("discarding stale {}:{}".format((addrinfo[0]), (addrinfo[1]))) | 92 | self.logger.debug("discarding stale %s:%s", *addrinfo) |
3838 | 89 | self._remove_service(name, addrinfo) | 93 | self._remove_service(name, addrinfo) |
3839 | 90 | else: | 94 | else: |
3840 | 91 | servers.append(addrinfo) | 95 | servers.append(addrinfo) |
3841 | 92 | 96 | ||
3843 | 93 | self.logger.debug("replying with {!r}".format((servers))) | 97 | self.logger.debug("replying with %r", servers) |
3844 | 94 | return tuple(servers) | 98 | return tuple(servers) |
3845 | 95 | 99 | ||
3846 | 96 | def cmd_list(self, host): | ||
3847 | 97 | """implementation for the ``list`` command""" | ||
3848 | 98 | self.logger.debug("querying for services list:") | ||
3849 | 99 | if not self.allow_listing: | ||
3850 | 100 | self.logger.debug("listing is disabled") | ||
3851 | 101 | return None | ||
3852 | 102 | services = tuple(self.services.keys()) | ||
3853 | 103 | self.logger.debug("replying with {}".format((services))) | ||
3854 | 104 | |||
3855 | 105 | return services | ||
3856 | 106 | |||
3857 | 107 | def cmd_register(self, host, names, port): | 100 | def cmd_register(self, host, names, port): |
3858 | 108 | """implementation of the ``register`` command""" | 101 | """implementation of the ``register`` command""" |
3860 | 109 | self.logger.debug("registering {}:{} as {}".format((host), (port), (', '.join(names)))) | 102 | self.logger.debug("registering %s:%s as %s", host, port, ", ".join(names)) |
3861 | 110 | for name in names: | 103 | for name in names: |
3862 | 111 | self._add_service(name.upper(), (host, port)) | 104 | self._add_service(name.upper(), (host, port)) |
3863 | 112 | return "OK" | 105 | return "OK" |
3864 | 113 | 106 | ||
3865 | 114 | def cmd_unregister(self, host, port): | 107 | def cmd_unregister(self, host, port): |
3866 | 115 | """implementation of the ``unregister`` command""" | 108 | """implementation of the ``unregister`` command""" |
3868 | 116 | self.logger.debug("unregistering {}:{}".format((host), (port))) | 109 | self.logger.debug("unregistering %s:%s", host, port) |
3869 | 117 | for name in list(self.services.keys()): | 110 | for name in list(self.services.keys()): |
3870 | 118 | self._remove_service(name, (host, port)) | 111 | self._remove_service(name, (host, port)) |
3871 | 119 | return "OK" | 112 | return "OK" |
3872 | @@ -135,11 +128,11 @@ class RegistryServer(object): | |||
3873 | 135 | except Exception: | 128 | except Exception: |
3874 | 136 | continue | 129 | continue |
3875 | 137 | if magic != "RPYC": | 130 | if magic != "RPYC": |
3877 | 138 | self.logger.warn("invalid magic: {!r}".format((magic))) | 131 | self.logger.warn("invalid magic: %r", magic) |
3878 | 139 | continue | 132 | continue |
3880 | 140 | cmdfunc = getattr(self, "cmd_{}".format((cmd.lower())), None) | 133 | cmdfunc = getattr(self, "cmd_%s" % (cmd.lower(),), None) |
3881 | 141 | if not cmdfunc: | 134 | if not cmdfunc: |
3883 | 142 | self.logger.warn("unknown command: {!r}".format((cmd))) | 135 | self.logger.warn("unknown command: %r", cmd) |
3884 | 143 | continue | 136 | continue |
3885 | 144 | 137 | ||
3886 | 145 | try: | 138 | try: |
3887 | @@ -155,8 +148,7 @@ class RegistryServer(object): | |||
3888 | 155 | raise ValueError("server is already running") | 148 | raise ValueError("server is already running") |
3889 | 156 | if self.sock is None: | 149 | if self.sock is None: |
3890 | 157 | raise ValueError("object disposed") | 150 | raise ValueError("object disposed") |
3893 | 158 | addrinfo = self.sock.getsockname()[:2] | 151 | self.logger.debug("server started on %s:%s", *self.sock.getsockname()[:2]) |
3892 | 159 | self.logger.debug("server started on {}:{}".format((addrinfo[0]), (addrinfo[1]))) | ||
3894 | 160 | try: | 152 | try: |
3895 | 161 | self.active = True | 153 | self.active = True |
3896 | 162 | self._work() | 154 | self._work() |
3897 | @@ -182,17 +174,17 @@ class UDPRegistryServer(RegistryServer): | |||
3898 | 182 | 174 | ||
3899 | 183 | TIMEOUT = 1.0 | 175 | TIMEOUT = 1.0 |
3900 | 184 | 176 | ||
3902 | 185 | def __init__(self, host="0.0.0.0", port=REGISTRY_PORT, pruning_timeout=None, logger=None, allow_listing=False): | 177 | def __init__(self, host="0.0.0.0", port=REGISTRY_PORT, pruning_timeout=None, logger=None): |
3903 | 186 | family, socktype, proto, _, sockaddr = socket.getaddrinfo(host, port, 0, | 178 | family, socktype, proto, _, sockaddr = socket.getaddrinfo(host, port, 0, |
3904 | 187 | socket.SOCK_DGRAM)[0] | 179 | socket.SOCK_DGRAM)[0] |
3905 | 188 | sock = socket.socket(family, socktype, proto) | 180 | sock = socket.socket(family, socktype, proto) |
3906 | 189 | sock.bind(sockaddr) | 181 | sock.bind(sockaddr) |
3907 | 190 | sock.settimeout(self.TIMEOUT) | 182 | sock.settimeout(self.TIMEOUT) |
3908 | 191 | RegistryServer.__init__(self, sock, pruning_timeout=pruning_timeout, | 183 | RegistryServer.__init__(self, sock, pruning_timeout=pruning_timeout, |
3910 | 192 | logger=logger, allow_listing=allow_listing) | 184 | logger=logger) |
3911 | 193 | 185 | ||
3912 | 194 | def _get_logger(self): | 186 | def _get_logger(self): |
3914 | 195 | return logging.getLogger("REGSRV/UDP/{}".format((self.port))) | 187 | return logging.getLogger("REGSRV/UDP/%d" % (self.port,)) |
3915 | 196 | 188 | ||
3916 | 197 | def _recv(self): | 189 | def _recv(self): |
3917 | 198 | return self.sock.recvfrom(MAX_DGRAM_SIZE) | 190 | return self.sock.recvfrom(MAX_DGRAM_SIZE) |
3918 | @@ -212,9 +204,10 @@ class TCPRegistryServer(RegistryServer): | |||
3919 | 212 | TIMEOUT = 3.0 | 204 | TIMEOUT = 3.0 |
3920 | 213 | 205 | ||
3921 | 214 | def __init__(self, host="0.0.0.0", port=REGISTRY_PORT, pruning_timeout=None, | 206 | def __init__(self, host="0.0.0.0", port=REGISTRY_PORT, pruning_timeout=None, |
3923 | 215 | logger=None, reuse_addr=True, allow_listing=False): | 207 | logger=None, reuse_addr=True): |
3924 | 216 | 208 | ||
3926 | 217 | family, socktype, proto, _, sockaddr = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)[0] | 209 | family, socktype, proto, _, sockaddr = socket.getaddrinfo(host, port, 0, |
3927 | 210 | socket.SOCK_STREAM)[0] | ||
3928 | 218 | sock = socket.socket(family, socktype, proto) | 211 | sock = socket.socket(family, socktype, proto) |
3929 | 219 | if reuse_addr and sys.platform != "win32": | 212 | if reuse_addr and sys.platform != "win32": |
3930 | 220 | # warning: reuseaddr is not what you expect on windows! | 213 | # warning: reuseaddr is not what you expect on windows! |
3931 | @@ -223,11 +216,11 @@ class TCPRegistryServer(RegistryServer): | |||
3932 | 223 | sock.listen(10) | 216 | sock.listen(10) |
3933 | 224 | sock.settimeout(self.TIMEOUT) | 217 | sock.settimeout(self.TIMEOUT) |
3934 | 225 | RegistryServer.__init__(self, sock, pruning_timeout=pruning_timeout, | 218 | RegistryServer.__init__(self, sock, pruning_timeout=pruning_timeout, |
3936 | 226 | logger=logger, allow_listing=allow_listing) | 219 | logger=logger) |
3937 | 227 | self._connected_sockets = {} | 220 | self._connected_sockets = {} |
3938 | 228 | 221 | ||
3939 | 229 | def _get_logger(self): | 222 | def _get_logger(self): |
3941 | 230 | return logging.getLogger("REGSRV/TCP/{}".format((self.port))) | 223 | return logging.getLogger("REGSRV/TCP/%d" % (self.port,)) |
3942 | 231 | 224 | ||
3943 | 232 | def _recv(self): | 225 | def _recv(self): |
3944 | 233 | sock2, _ = self.sock.accept() | 226 | sock2, _ = self.sock.accept() |
3945 | @@ -274,13 +267,6 @@ class RegistryClient(object): | |||
3946 | 274 | """ | 267 | """ |
3947 | 275 | raise NotImplementedError() | 268 | raise NotImplementedError() |
3948 | 276 | 269 | ||
3949 | 277 | def list(self): | ||
3950 | 278 | """ | ||
3951 | 279 | Send a query for the full lists of exposed servers | ||
3952 | 280 | :returns: a list of `` service_name `` | ||
3953 | 281 | """ | ||
3954 | 282 | raise NotImplementedError() | ||
3955 | 283 | |||
3956 | 284 | def register(self, aliases, port): | 270 | def register(self, aliases, port): |
3957 | 285 | """Registers the given service aliases with the given TCP port. This | 271 | """Registers the given service aliases with the given TCP port. This |
3958 | 286 | API is intended to be called only by an RPyC server. | 272 | API is intended to be called only by an RPyC server. |
3959 | @@ -307,7 +293,6 @@ class UDPRegistryClient(RegistryClient): | |||
3960 | 307 | Example:: | 293 | Example:: |
3961 | 308 | 294 | ||
3962 | 309 | registrar = UDPRegistryClient() | 295 | registrar = UDPRegistryClient() |
3963 | 310 | list_of_services = registrar.list() | ||
3964 | 311 | list_of_servers = registrar.discover("foo") | 296 | list_of_servers = registrar.discover("foo") |
3965 | 312 | 297 | ||
3966 | 313 | .. note:: | 298 | .. note:: |
3967 | @@ -349,26 +334,8 @@ class UDPRegistryClient(RegistryClient): | |||
3968 | 349 | servers = brine.load(data) | 334 | servers = brine.load(data) |
3969 | 350 | return servers | 335 | return servers |
3970 | 351 | 336 | ||
3971 | 352 | def list(self): | ||
3972 | 353 | sock = socket.socket(self.sock_family, socket.SOCK_DGRAM) | ||
3973 | 354 | |||
3974 | 355 | with closing(sock): | ||
3975 | 356 | if self.bcast: | ||
3976 | 357 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, True) | ||
3977 | 358 | data = brine.dump(("RPYC", "LIST", ())) | ||
3978 | 359 | sock.sendto(data, (self.ip, self.port)) | ||
3979 | 360 | sock.settimeout(self.timeout) | ||
3980 | 361 | |||
3981 | 362 | try: | ||
3982 | 363 | data, _ = sock.recvfrom(MAX_DGRAM_SIZE) | ||
3983 | 364 | except (socket.error, socket.timeout): | ||
3984 | 365 | services = () | ||
3985 | 366 | else: | ||
3986 | 367 | services = brine.load(data) | ||
3987 | 368 | return services | ||
3988 | 369 | |||
3989 | 370 | def register(self, aliases, port, interface=""): | 337 | def register(self, aliases, port, interface=""): |
3991 | 371 | self.logger.info("registering on {}:{}".format((self.ip), (self.port))) | 338 | self.logger.info("registering on %s:%s", self.ip, self.port) |
3992 | 372 | sock = socket.socket(self.sock_family, socket.SOCK_DGRAM) | 339 | sock = socket.socket(self.sock_family, socket.SOCK_DGRAM) |
3993 | 373 | with closing(sock): | 340 | with closing(sock): |
3994 | 374 | sock.bind((interface, 0)) | 341 | sock.bind((interface, 0)) |
3995 | @@ -393,14 +360,14 @@ class UDPRegistryClient(RegistryClient): | |||
3996 | 393 | except Exception: | 360 | except Exception: |
3997 | 394 | continue | 361 | continue |
3998 | 395 | if reply == "OK": | 362 | if reply == "OK": |
4000 | 396 | self.logger.info("registry {}:{} acknowledged".format((rip), (rport))) | 363 | self.logger.info("registry %s:%s acknowledged", rip, rport) |
4001 | 397 | return True | 364 | return True |
4002 | 398 | else: | 365 | else: |
4003 | 399 | self.logger.warn("no registry acknowledged") | 366 | self.logger.warn("no registry acknowledged") |
4004 | 400 | return False | 367 | return False |
4005 | 401 | 368 | ||
4006 | 402 | def unregister(self, port): | 369 | def unregister(self, port): |
4008 | 403 | self.logger.info("unregistering from {}:{}".format((self.ip), (self.port))) | 370 | self.logger.info("unregistering from %s:%s", self.ip, self.port) |
4009 | 404 | sock = socket.socket(self.sock_family, socket.SOCK_DGRAM) | 371 | sock = socket.socket(self.sock_family, socket.SOCK_DGRAM) |
4010 | 405 | with closing(sock): | 372 | with closing(sock): |
4011 | 406 | if self.bcast: | 373 | if self.bcast: |
4012 | @@ -416,7 +383,6 @@ class TCPRegistryClient(RegistryClient): | |||
4013 | 416 | Example:: | 383 | Example:: |
4014 | 417 | 384 | ||
4015 | 418 | registrar = TCPRegistryClient("localhost") | 385 | registrar = TCPRegistryClient("localhost") |
4016 | 419 | list_of_services = registrar.list() | ||
4017 | 420 | list_of_servers = registrar.discover("foo") | 386 | list_of_servers = registrar.discover("foo") |
4018 | 421 | 387 | ||
4019 | 422 | .. note:: | 388 | .. note:: |
4020 | @@ -446,24 +412,8 @@ class TCPRegistryClient(RegistryClient): | |||
4021 | 446 | servers = brine.load(data) | 412 | servers = brine.load(data) |
4022 | 447 | return servers | 413 | return servers |
4023 | 448 | 414 | ||
4024 | 449 | def list(self): | ||
4025 | 450 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
4026 | 451 | with closing(sock): | ||
4027 | 452 | sock.settimeout(self.timeout) | ||
4028 | 453 | data = brine.dump(("RPYC", "LIST", ())) | ||
4029 | 454 | sock.connect((self.ip, self.port)) | ||
4030 | 455 | sock.send(data) | ||
4031 | 456 | |||
4032 | 457 | try: | ||
4033 | 458 | data = sock.recv(MAX_DGRAM_SIZE) | ||
4034 | 459 | except (socket.error, socket.timeout): | ||
4035 | 460 | servers = () | ||
4036 | 461 | else: | ||
4037 | 462 | servers = brine.load(data) | ||
4038 | 463 | return servers | ||
4039 | 464 | |||
4040 | 465 | def register(self, aliases, port, interface=""): | 415 | def register(self, aliases, port, interface=""): |
4042 | 466 | self.logger.info("registering on {}:{}".format((self.ip), (self.port))) | 416 | self.logger.info("registering on %s:%s", self.ip, self.port) |
4043 | 467 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | 417 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
4044 | 468 | with closing(sock): | 418 | with closing(sock): |
4045 | 469 | sock.bind((interface, 0)) | 419 | sock.bind((interface, 0)) |
4046 | @@ -486,12 +436,12 @@ class TCPRegistryClient(RegistryClient): | |||
4047 | 486 | self.logger.warn("received corrupted data from registry") | 436 | self.logger.warn("received corrupted data from registry") |
4048 | 487 | return False | 437 | return False |
4049 | 488 | if reply == "OK": | 438 | if reply == "OK": |
4051 | 489 | self.logger.info("registry {}:{} acknowledged".format((self.ip), (self.port))) | 439 | self.logger.info("registry %s:%s acknowledged", self.ip, self.port) |
4052 | 490 | 440 | ||
4053 | 491 | return True | 441 | return True |
4054 | 492 | 442 | ||
4055 | 493 | def unregister(self, port): | 443 | def unregister(self, port): |
4057 | 494 | self.logger.info("unregistering from {}:{}".format((self.ip), (self.port))) | 444 | self.logger.info("unregistering from %s:%s", self.ip, self.port) |
4058 | 495 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | 445 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
4059 | 496 | with closing(sock): | 446 | with closing(sock): |
4060 | 497 | sock.settimeout(self.timeout) | 447 | sock.settimeout(self.timeout) |
4061 | diff --git a/plainbox/vendor/rpyc/utils/server.py b/plainbox/vendor/rpyc/utils/server.py | |||
4062 | index 2e73ecb..d50e118 100644 | |||
4063 | --- a/plainbox/vendor/rpyc/utils/server.py | |||
4064 | +++ b/plainbox/vendor/rpyc/utils/server.py | |||
4065 | @@ -26,8 +26,8 @@ class Server(object): | |||
4066 | 26 | """Base server implementation | 26 | """Base server implementation |
4067 | 27 | 27 | ||
4068 | 28 | :param service: the :class:`~rpyc.core.service.Service` to expose | 28 | :param service: the :class:`~rpyc.core.service.Service` to expose |
4071 | 29 | :param hostname: the host to bind to. By default, the 'wildcard address' is used to listen on all interfaces. | 29 | :param hostname: the host to bind to. Default is IPADDR_ANY, but you may |
4072 | 30 | if not properly secured, the server can receive traffic from unintended or even malicious sources. | 30 | want to restrict it only to ``localhost`` in some setups |
4073 | 31 | :param ipv6: whether to create an IPv6 or IPv4 socket. The default is IPv4 | 31 | :param ipv6: whether to create an IPv6 or IPv4 socket. The default is IPv4 |
4074 | 32 | :param port: the TCP port to bind to | 32 | :param port: the TCP port to bind to |
4075 | 33 | :param backlog: the socket's backlog (passed to ``listen()``) | 33 | :param backlog: the socket's backlog (passed to ``listen()``) |
4076 | @@ -47,9 +47,9 @@ class Server(object): | |||
4077 | 47 | on embedded platforms with limited battery) | 47 | on embedded platforms with limited battery) |
4078 | 48 | """ | 48 | """ |
4079 | 49 | 49 | ||
4081 | 50 | def __init__(self, service, hostname=None, ipv6=False, port=0, | 50 | def __init__(self, service, hostname="", ipv6=False, port=0, |
4082 | 51 | backlog=socket.SOMAXCONN, reuse_addr=True, authenticator=None, registrar=None, | 51 | backlog=socket.SOMAXCONN, reuse_addr=True, authenticator=None, registrar=None, |
4084 | 52 | auto_register=None, protocol_config=None, logger=None, listener_timeout=0.5, | 52 | auto_register=None, protocol_config={}, logger=None, listener_timeout=0.5, |
4085 | 53 | socket_path=None): | 53 | socket_path=None): |
4086 | 54 | self.active = False | 54 | self.active = False |
4087 | 55 | self._closed = False | 55 | self._closed = False |
4088 | @@ -60,15 +60,11 @@ class Server(object): | |||
4089 | 60 | self.auto_register = bool(registrar) | 60 | self.auto_register = bool(registrar) |
4090 | 61 | else: | 61 | else: |
4091 | 62 | self.auto_register = auto_register | 62 | self.auto_register = auto_register |
4092 | 63 | |||
4093 | 64 | if protocol_config is None: | ||
4094 | 65 | protocol_config = {} | ||
4095 | 66 | |||
4096 | 67 | self.protocol_config = protocol_config | 63 | self.protocol_config = protocol_config |
4097 | 68 | self.clients = set() | 64 | self.clients = set() |
4098 | 69 | 65 | ||
4099 | 70 | if socket_path is not None: | 66 | if socket_path is not None: |
4101 | 71 | if hostname is not None or port != 0 or ipv6 is not False: | 67 | if hostname != "" or port != 0 or ipv6 is not False: |
4102 | 72 | raise ValueError("socket_path is mutually exclusive with: hostname, port, ipv6") | 68 | raise ValueError("socket_path is mutually exclusive with: hostname, port, ipv6") |
4103 | 73 | self.listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | 69 | self.listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
4104 | 74 | self.listener.bind(socket_path) | 70 | self.listener.bind(socket_path) |
4105 | @@ -76,18 +72,20 @@ class Server(object): | |||
4106 | 76 | self.host, self.port = "", socket_path | 72 | self.host, self.port = "", socket_path |
4107 | 77 | else: | 73 | else: |
4108 | 78 | if ipv6: | 74 | if ipv6: |
4110 | 79 | family = socket.AF_INET6 | 75 | if hostname == "localhost" and sys.platform != "win32": |
4111 | 76 | # on windows, you should bind to localhost even for ipv6 | ||
4112 | 77 | hostname = "localhost6" | ||
4113 | 78 | self.listener = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) | ||
4114 | 80 | else: | 79 | else: |
4118 | 81 | family = socket.AF_INET | 80 | self.listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
4116 | 82 | self.listener = socket.socket(family, socket.SOCK_STREAM) | ||
4117 | 83 | address = socket.getaddrinfo(hostname, port, family=family, type=socket.SOCK_STREAM, proto=socket.IPPROTO_TCP, flags=socket.AI_PASSIVE)[0][-1] | ||
4119 | 84 | 81 | ||
4120 | 85 | if reuse_addr and sys.platform != "win32": | 82 | if reuse_addr and sys.platform != "win32": |
4121 | 86 | # warning: reuseaddr is not what you'd expect on windows! | 83 | # warning: reuseaddr is not what you'd expect on windows! |
4122 | 87 | # it allows you to bind an already bound port, resulting in | 84 | # it allows you to bind an already bound port, resulting in |
4123 | 88 | # "unexpected behavior" (quoting MSDN) | 85 | # "unexpected behavior" (quoting MSDN) |
4124 | 89 | self.listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | 86 | self.listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
4126 | 90 | self.listener.bind(address) | 87 | |
4127 | 88 | self.listener.bind((hostname, port)) | ||
4128 | 91 | self.listener.settimeout(listener_timeout) | 89 | self.listener.settimeout(listener_timeout) |
4129 | 92 | 90 | ||
4130 | 93 | # hack for IPv6 (the tuple can be longer than 2) | 91 | # hack for IPv6 (the tuple can be longer than 2) |
4131 | @@ -95,7 +93,7 @@ class Server(object): | |||
4132 | 95 | self.host, self.port = sockname[0], sockname[1] | 93 | self.host, self.port = sockname[0], sockname[1] |
4133 | 96 | 94 | ||
4134 | 97 | if logger is None: | 95 | if logger is None: |
4136 | 98 | logger = logging.getLogger("{}/{}".format((self.service.get_service_name()), (self.port))) | 96 | logger = logging.getLogger("%s/%s" % (self.service.get_service_name(), self.port)) |
4137 | 99 | self.logger = logger | 97 | self.logger = logger |
4138 | 100 | if "logger" not in self.protocol_config: | 98 | if "logger" not in self.protocol_config: |
4139 | 101 | self.protocol_config["logger"] = self.logger | 99 | self.protocol_config["logger"] = self.logger |
4140 | @@ -153,7 +151,7 @@ class Server(object): | |||
4141 | 153 | return | 151 | return |
4142 | 154 | 152 | ||
4143 | 155 | sock.setblocking(True) | 153 | sock.setblocking(True) |
4145 | 156 | self.logger.info("accepted {} with fd {}".format((addrinfo), (sock.fileno()))) | 154 | self.logger.info("accepted %s with fd %s", addrinfo, sock.fileno()) |
4146 | 157 | self.clients.add(sock) | 155 | self.clients.add(sock) |
4147 | 158 | self._accept_method(sock) | 156 | self._accept_method(sock) |
4148 | 159 | 157 | ||
4149 | @@ -171,10 +169,10 @@ class Server(object): | |||
4150 | 171 | try: | 169 | try: |
4151 | 172 | sock2, credentials = self.authenticator(sock) | 170 | sock2, credentials = self.authenticator(sock) |
4152 | 173 | except AuthenticationError: | 171 | except AuthenticationError: |
4154 | 174 | self.logger.info("{} failed to authenticate... rejecting connection".format((addrinfo))) | 172 | self.logger.info("%s failed to authenticate, rejecting connection", addrinfo) |
4155 | 175 | return | 173 | return |
4156 | 176 | else: | 174 | else: |
4158 | 177 | self.logger.info("{} authenticated successfully".format((addrinfo))) | 175 | self.logger.info("%s authenticated successfully", addrinfo) |
4159 | 178 | else: | 176 | else: |
4160 | 179 | credentials = None | 177 | credentials = None |
4161 | 180 | sock2 = sock | 178 | sock2 = sock |
4162 | @@ -194,16 +192,16 @@ class Server(object): | |||
4163 | 194 | def _serve_client(self, sock, credentials): | 192 | def _serve_client(self, sock, credentials): |
4164 | 195 | addrinfo = sock.getpeername() | 193 | addrinfo = sock.getpeername() |
4165 | 196 | if credentials: | 194 | if credentials: |
4167 | 197 | self.logger.info("welcome {} ({!r})".format((addrinfo), (credentials))) | 195 | self.logger.info("welcome %s (%r)", addrinfo, credentials) |
4168 | 198 | else: | 196 | else: |
4170 | 199 | self.logger.info("welcome {}".format((addrinfo))) | 197 | self.logger.info("welcome %s", addrinfo) |
4171 | 200 | try: | 198 | try: |
4172 | 201 | config = dict(self.protocol_config, credentials=credentials, | 199 | config = dict(self.protocol_config, credentials=credentials, |
4173 | 202 | endpoints=(sock.getsockname(), addrinfo), logger=self.logger) | 200 | endpoints=(sock.getsockname(), addrinfo), logger=self.logger) |
4174 | 203 | conn = self.service._connect(Channel(SocketStream(sock)), config) | 201 | conn = self.service._connect(Channel(SocketStream(sock)), config) |
4175 | 204 | self._handle_connection(conn) | 202 | self._handle_connection(conn) |
4176 | 205 | finally: | 203 | finally: |
4178 | 206 | self.logger.info("goodbye {}".format((addrinfo))) | 204 | self.logger.info("goodbye %s", addrinfo) |
4179 | 207 | 205 | ||
4180 | 208 | def _handle_connection(self, conn): | 206 | def _handle_connection(self, conn): |
4181 | 209 | """This methoed should implement the server's logic.""" | 207 | """This methoed should implement the server's logic.""" |
4182 | @@ -212,7 +210,7 @@ class Server(object): | |||
4183 | 212 | def _bg_register(self): | 210 | def _bg_register(self): |
4184 | 213 | interval = self.registrar.REREGISTER_INTERVAL | 211 | interval = self.registrar.REREGISTER_INTERVAL |
4185 | 214 | self.logger.info("started background auto-register thread " | 212 | self.logger.info("started background auto-register thread " |
4187 | 215 | "(interval = {})".format((interval))) | 213 | "(interval = %s)", interval) |
4188 | 216 | tnext = 0 | 214 | tnext = 0 |
4189 | 217 | try: | 215 | try: |
4190 | 218 | while self.active: | 216 | while self.active: |
4191 | @@ -247,11 +245,12 @@ class Server(object): | |||
4192 | 247 | # Note that for AF_UNIX the following won't work (but we are safe | 245 | # Note that for AF_UNIX the following won't work (but we are safe |
4193 | 248 | # since we already saved the socket_path into self.port): | 246 | # since we already saved the socket_path into self.port): |
4194 | 249 | self.port = self.listener.getsockname()[1] | 247 | self.port = self.listener.getsockname()[1] |
4196 | 250 | self.logger.info("server started on [{}]:{}".format((self.host), (self.port))) | 248 | self.logger.info("server started on [%s]:%s", self.host, self.port) |
4197 | 251 | self.active = True | 249 | self.active = True |
4198 | 252 | 250 | ||
4199 | 253 | def _register(self): | 251 | def _register(self): |
4200 | 254 | if self.auto_register: | 252 | if self.auto_register: |
4201 | 253 | self.auto_register = False | ||
4202 | 255 | spawn(self._bg_register) | 254 | spawn(self._bg_register) |
4203 | 256 | 255 | ||
4204 | 257 | def start(self): | 256 | def start(self): |
4205 | @@ -341,7 +340,7 @@ class ThreadPoolServer(Server): | |||
4206 | 341 | self.workers = [] | 340 | self.workers = [] |
4207 | 342 | for i in range(self.nbthreads): | 341 | for i in range(self.nbthreads): |
4208 | 343 | t = spawn(self._serve_clients) | 342 | t = spawn(self._serve_clients) |
4210 | 344 | t.setName("Worker{}".format((i))) | 343 | t.setName('Worker%i' % i) |
4211 | 345 | self.workers.append(t) | 344 | self.workers.append(t) |
4212 | 346 | # setup a thread for polling inactive connections | 345 | # setup a thread for polling inactive connections |
4213 | 347 | self.polling_thread = spawn(self._poll_inactive_clients) | 346 | self.polling_thread = spawn(self._poll_inactive_clients) |
4214 | @@ -382,7 +381,7 @@ class ThreadPoolServer(Server): | |||
4215 | 382 | pass | 381 | pass |
4216 | 383 | 382 | ||
4217 | 384 | # close connection | 383 | # close connection |
4219 | 385 | self.logger.info("Closing connection for fd {}".format((fd))) | 384 | self.logger.info("Closing connection for fd %d", fd) |
4220 | 386 | if conn: | 385 | if conn: |
4221 | 387 | conn.close() | 386 | conn.close() |
4222 | 388 | 387 | ||
4223 | @@ -420,7 +419,7 @@ class ThreadPoolServer(Server): | |||
4224 | 420 | except Exception: | 419 | except Exception: |
4225 | 421 | ex = sys.exc_info()[1] | 420 | ex = sys.exc_info()[1] |
4226 | 422 | # "Caught exception in Worker thread" message | 421 | # "Caught exception in Worker thread" message |
4228 | 423 | self.logger.warning("Failed to poll clients, caught exception : {}".format((ex))) | 422 | self.logger.warning("Failed to poll clients, caught exception : %s", str(ex)) |
4229 | 424 | # wait a bit so that we do not loop too fast in case of error | 423 | # wait a bit so that we do not loop too fast in case of error |
4230 | 425 | time.sleep(0.2) | 424 | time.sleep(0.2) |
4231 | 426 | 425 | ||
4232 | @@ -468,7 +467,7 @@ class ThreadPoolServer(Server): | |||
4233 | 468 | time.sleep(0.2) | 467 | time.sleep(0.2) |
4234 | 469 | 468 | ||
4235 | 470 | def _authenticate_and_build_connection(self, sock): | 469 | def _authenticate_and_build_connection(self, sock): |
4237 | 471 | '''Authenticate a client and if it succeeds, wraps the socket in a connection object. | 470 | '''Authenticate a client and if it succees, wraps the socket in a connection object. |
4238 | 472 | Note that this code is cut and paste from the rpyc internals and may have to be | 471 | Note that this code is cut and paste from the rpyc internals and may have to be |
4239 | 473 | changed if rpyc evolves''' | 472 | changed if rpyc evolves''' |
4240 | 474 | # authenticate | 473 | # authenticate |
4241 | @@ -477,27 +476,27 @@ class ThreadPoolServer(Server): | |||
4242 | 477 | else: | 476 | else: |
4243 | 478 | credentials = None | 477 | credentials = None |
4244 | 479 | # build a connection | 478 | # build a connection |
4248 | 480 | addrinfo = sock.getpeername() | 479 | h, p = sock.getpeername() |
4249 | 481 | config = dict(self.protocol_config, credentials=credentials, connid="{}".format(addrinfo), | 480 | config = dict(self.protocol_config, credentials=credentials, connid="%s:%d" % (h, p), |
4250 | 482 | endpoints=(sock.getsockname(), addrinfo)) | 481 | endpoints=(sock.getsockname(), (h, p))) |
4251 | 483 | return sock, self.service._connect(Channel(SocketStream(sock)), config) | 482 | return sock, self.service._connect(Channel(SocketStream(sock)), config) |
4252 | 484 | 483 | ||
4253 | 485 | def _accept_method(self, sock): | 484 | def _accept_method(self, sock): |
4254 | 486 | '''Implementation of the accept method : only pushes the work to the internal queue. | 485 | '''Implementation of the accept method : only pushes the work to the internal queue. |
4255 | 487 | In case the queue is full, raises an AsynResultTimeout error''' | 486 | In case the queue is full, raises an AsynResultTimeout error''' |
4256 | 488 | try: | 487 | try: |
4258 | 489 | addrinfo = None | 488 | h, p = None, None |
4259 | 490 | # authenticate and build connection object | 489 | # authenticate and build connection object |
4260 | 491 | sock, conn = self._authenticate_and_build_connection(sock) | 490 | sock, conn = self._authenticate_and_build_connection(sock) |
4261 | 492 | # put the connection in the active queue | 491 | # put the connection in the active queue |
4263 | 493 | addrinfo = sock.getpeername() | 492 | h, p = sock.getpeername() |
4264 | 494 | fd = conn.fileno() | 493 | fd = conn.fileno() |
4266 | 495 | self.logger.debug("Created connection to {addrinfo} with fd {fd}") | 494 | self.logger.debug("Created connection to %s:%d with fd %d", h, p, fd) |
4267 | 496 | self.fd_to_conn[fd] = conn | 495 | self.fd_to_conn[fd] = conn |
4268 | 497 | self._add_inactive_connection(fd) | 496 | self._add_inactive_connection(fd) |
4269 | 498 | self.clients.clear() | 497 | self.clients.clear() |
4270 | 499 | except Exception: | 498 | except Exception: |
4272 | 500 | err_msg = "Failed to serve client for {}, caught exception".format(addrinfo) | 499 | err_msg = "Failed to serve client for {}:{}, caught exception".format(h, p) |
4273 | 501 | self.logger.exception(err_msg) | 500 | self.logger.exception(err_msg) |
4274 | 502 | sock.close() | 501 | sock.close() |
4275 | 503 | 502 | ||
4276 | @@ -564,6 +563,7 @@ class GeventServer(Server): | |||
4277 | 564 | 563 | ||
4278 | 565 | def _register(self): | 564 | def _register(self): |
4279 | 566 | if self.auto_register: | 565 | if self.auto_register: |
4280 | 566 | self.auto_register = False | ||
4281 | 567 | gevent.spawn(self._bg_register) | 567 | gevent.spawn(self._bg_register) |
4282 | 568 | 568 | ||
4283 | 569 | def _accept_method(self, sock): | 569 | def _accept_method(self, sock): |
4284 | diff --git a/plainbox/vendor/rpyc/utils/teleportation.py b/plainbox/vendor/rpyc/utils/teleportation.py | |||
4285 | index a06af70..451c93d 100644 | |||
4286 | --- a/plainbox/vendor/rpyc/utils/teleportation.py | |||
4287 | +++ b/plainbox/vendor/rpyc/utils/teleportation.py | |||
4288 | @@ -1,21 +1,48 @@ | |||
4289 | 1 | import opcode | 1 | import opcode |
4292 | 2 | 2 | import sys | |
4293 | 3 | from plainbox.vendor.rpyc.lib.compat import is_py_gte38 | 3 | try: |
4294 | 4 | import __builtin__ | ||
4295 | 5 | except ImportError: | ||
4296 | 6 | import builtins as __builtin__ # noqa: F401 | ||
4297 | 7 | from plainbox.vendor.rpyc.lib.compat import is_py_3k, is_py_gte38 | ||
4298 | 4 | from types import CodeType, FunctionType | 8 | from types import CodeType, FunctionType |
4301 | 5 | from plainbox.vendor.rpyc.core import brine, netref | 9 | from plainbox.vendor.rpyc.core import brine |
4302 | 6 | from dis import _unpack_opargs | 10 | from plainbox.vendor.rpyc.core import netref |
4303 | 7 | 11 | ||
4304 | 8 | CODEOBJ_MAGIC = "MAg1c J0hNNzo0hn ZqhuBP17LQk8" | 12 | CODEOBJ_MAGIC = "MAg1c J0hNNzo0hn ZqhuBP17LQk8" |
4305 | 9 | 13 | ||
4306 | 10 | 14 | ||
4307 | 11 | # NOTE: dislike this kind of hacking on the level of implementation details, | 15 | # NOTE: dislike this kind of hacking on the level of implementation details, |
4308 | 12 | # should search for a more reliable/future-proof way: | 16 | # should search for a more reliable/future-proof way: |
4310 | 13 | CODE_HAVEARG_SIZE = 3 | 17 | CODE_HAVEARG_SIZE = 2 if sys.version_info >= (3, 6) else 3 |
4311 | 18 | try: | ||
4312 | 19 | from dis import _unpack_opargs | ||
4313 | 20 | except ImportError: | ||
4314 | 21 | # COPIED from 3.5's `dis.py`, this should hopefully be correct for <=3.5: | ||
4315 | 22 | def _unpack_opargs(code): | ||
4316 | 23 | extended_arg = 0 | ||
4317 | 24 | n = len(code) | ||
4318 | 25 | i = 0 | ||
4319 | 26 | while i < n: | ||
4320 | 27 | op = code[i] | ||
4321 | 28 | offset = i | ||
4322 | 29 | i = i + 1 | ||
4323 | 30 | arg = None | ||
4324 | 31 | if op >= opcode.HAVE_ARGUMENT: | ||
4325 | 32 | arg = code[i] + code[i + 1] * 256 + extended_arg | ||
4326 | 33 | extended_arg = 0 | ||
4327 | 34 | i = i + 2 | ||
4328 | 35 | if op == opcode.EXTENDED_ARG: | ||
4329 | 36 | extended_arg = arg * 65536 | ||
4330 | 37 | yield (offset, op, arg) | ||
4331 | 14 | 38 | ||
4332 | 15 | 39 | ||
4333 | 16 | def decode_codeobj(codeobj): | 40 | def decode_codeobj(codeobj): |
4334 | 17 | # adapted from dis.dis | 41 | # adapted from dis.dis |
4336 | 18 | codestr = codeobj.co_code | 42 | if is_py_3k: |
4337 | 43 | codestr = codeobj.co_code | ||
4338 | 44 | else: | ||
4339 | 45 | codestr = [ord(ch) for ch in codeobj.co_code] | ||
4340 | 19 | free = None | 46 | free = None |
4341 | 20 | for i, op, oparg in _unpack_opargs(codestr): | 47 | for i, op, oparg in _unpack_opargs(codestr): |
4342 | 21 | opname = opcode.opname[op] | 48 | opname = opcode.opname[op] |
4343 | @@ -46,7 +73,7 @@ def _export_codeobj(cobj): | |||
4344 | 46 | elif isinstance(const, CodeType): | 73 | elif isinstance(const, CodeType): |
4345 | 47 | consts2.append(_export_codeobj(const)) | 74 | consts2.append(_export_codeobj(const)) |
4346 | 48 | else: | 75 | else: |
4348 | 49 | raise TypeError(f"Cannot export a function with non-brinable constants: {const!r}") | 76 | raise TypeError("Cannot export a function with non-brinable constants: %r" % (const,)) |
4349 | 50 | 77 | ||
4350 | 51 | if is_py_gte38: | 78 | if is_py_gte38: |
4351 | 52 | # Constructor was changed in 3.8 to support "advanced" programming styles | 79 | # Constructor was changed in 3.8 to support "advanced" programming styles |
4352 | @@ -54,41 +81,50 @@ def _export_codeobj(cobj): | |||
4353 | 54 | cobj.co_stacksize, cobj.co_flags, cobj.co_code, tuple(consts2), cobj.co_names, cobj.co_varnames, | 81 | cobj.co_stacksize, cobj.co_flags, cobj.co_code, tuple(consts2), cobj.co_names, cobj.co_varnames, |
4354 | 55 | cobj.co_filename, cobj.co_name, cobj.co_firstlineno, cobj.co_lnotab, cobj.co_freevars, | 82 | cobj.co_filename, cobj.co_name, cobj.co_firstlineno, cobj.co_lnotab, cobj.co_freevars, |
4355 | 56 | cobj.co_cellvars) | 83 | cobj.co_cellvars) |
4357 | 57 | else: | 84 | elif is_py_3k: |
4358 | 58 | exported = (cobj.co_argcount, cobj.co_kwonlyargcount, cobj.co_nlocals, cobj.co_stacksize, cobj.co_flags, | 85 | exported = (cobj.co_argcount, cobj.co_kwonlyargcount, cobj.co_nlocals, cobj.co_stacksize, cobj.co_flags, |
4359 | 59 | cobj.co_code, tuple(consts2), cobj.co_names, cobj.co_varnames, cobj.co_filename, | 86 | cobj.co_code, tuple(consts2), cobj.co_names, cobj.co_varnames, cobj.co_filename, |
4360 | 60 | cobj.co_name, cobj.co_firstlineno, cobj.co_lnotab, cobj.co_freevars, cobj.co_cellvars) | 87 | cobj.co_name, cobj.co_firstlineno, cobj.co_lnotab, cobj.co_freevars, cobj.co_cellvars) |
4361 | 88 | else: | ||
4362 | 89 | exported = (cobj.co_argcount, cobj.co_nlocals, cobj.co_stacksize, cobj.co_flags, | ||
4363 | 90 | cobj.co_code, tuple(consts2), cobj.co_names, cobj.co_varnames, cobj.co_filename, | ||
4364 | 91 | cobj.co_name, cobj.co_firstlineno, cobj.co_lnotab, cobj.co_freevars, cobj.co_cellvars) | ||
4365 | 92 | |||
4366 | 61 | assert brine.dumpable(exported) | 93 | assert brine.dumpable(exported) |
4367 | 62 | return (CODEOBJ_MAGIC, exported) | 94 | return (CODEOBJ_MAGIC, exported) |
4368 | 63 | 95 | ||
4369 | 64 | 96 | ||
4370 | 65 | def export_function(func): | 97 | def export_function(func): |
4379 | 66 | closure = func.__closure__ | 98 | if is_py_3k: |
4380 | 67 | code = func.__code__ | 99 | func_closure = func.__closure__ |
4381 | 68 | defaults = func.__defaults__ | 100 | func_code = func.__code__ |
4382 | 69 | kwdefaults = func.__kwdefaults__ | 101 | func_defaults = func.__defaults__ |
4383 | 70 | if kwdefaults is not None: | 102 | else: |
4384 | 71 | kwdefaults = tuple(kwdefaults.items()) | 103 | func_closure = func.func_closure |
4385 | 72 | 104 | func_code = func.func_code | |
4386 | 73 | if closure: | 105 | func_defaults = func.func_defaults |
4387 | 106 | |||
4388 | 107 | if func_closure: | ||
4389 | 74 | raise TypeError("Cannot export a function closure") | 108 | raise TypeError("Cannot export a function closure") |
4394 | 75 | if not brine.dumpable(defaults): | 109 | if not brine.dumpable(func_defaults): |
4395 | 76 | raise TypeError("Cannot export a function with non-brinable defaults (__defaults__)") | 110 | raise TypeError("Cannot export a function with non-brinable defaults (func_defaults)") |
4392 | 77 | if not brine.dumpable(kwdefaults): | ||
4393 | 78 | raise TypeError("Cannot export a function with non-brinable defaults (__kwdefaults__)") | ||
4396 | 79 | 111 | ||
4398 | 80 | return func.__name__, func.__module__, defaults, kwdefaults, _export_codeobj(code)[1] | 112 | return func.__name__, func.__module__, func_defaults, _export_codeobj(func_code)[1] |
4399 | 81 | 113 | ||
4400 | 82 | 114 | ||
4401 | 83 | def _import_codetup(codetup): | 115 | def _import_codetup(codetup): |
4406 | 84 | # Handle tuples sent from 3.8 as well as 3 < version < 3.8. | 116 | if is_py_3k: |
4407 | 85 | if len(codetup) == 16: | 117 | # Handle tuples sent from 3.8 as well as 3 < version < 3.8. |
4408 | 86 | (argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, | 118 | if len(codetup) == 16: |
4409 | 87 | filename, name, firstlineno, lnotab, freevars, cellvars) = codetup | 119 | (argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, |
4410 | 120 | filename, name, firstlineno, lnotab, freevars, cellvars) = codetup | ||
4411 | 121 | else: | ||
4412 | 122 | (argcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, | ||
4413 | 123 | filename, name, firstlineno, lnotab, freevars, cellvars) = codetup | ||
4414 | 124 | posonlyargcount = 0 | ||
4415 | 88 | else: | 125 | else: |
4417 | 89 | (argcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, | 126 | (argcount, nlocals, stacksize, flags, code, consts, names, varnames, |
4418 | 90 | filename, name, firstlineno, lnotab, freevars, cellvars) = codetup | 127 | filename, name, firstlineno, lnotab, freevars, cellvars) = codetup |
4419 | 91 | posonlyargcount = 0 | ||
4420 | 92 | 128 | ||
4421 | 93 | consts2 = [] | 129 | consts2 = [] |
4422 | 94 | for const in consts: | 130 | for const in consts: |
4423 | @@ -100,14 +136,17 @@ def _import_codetup(codetup): | |||
4424 | 100 | if is_py_gte38: | 136 | if is_py_gte38: |
4425 | 101 | codetup = (argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, | 137 | codetup = (argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, |
4426 | 102 | filename, name, firstlineno, lnotab, freevars, cellvars) | 138 | filename, name, firstlineno, lnotab, freevars, cellvars) |
4428 | 103 | else: | 139 | elif is_py_3k: |
4429 | 104 | codetup = (argcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, filename, name, | 140 | codetup = (argcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, filename, name, |
4430 | 105 | firstlineno, lnotab, freevars, cellvars) | 141 | firstlineno, lnotab, freevars, cellvars) |
4431 | 142 | else: | ||
4432 | 143 | codetup = (argcount, nlocals, stacksize, flags, code, consts, names, varnames, filename, name, firstlineno, | ||
4433 | 144 | lnotab, freevars, cellvars) | ||
4434 | 106 | return CodeType(*codetup) | 145 | return CodeType(*codetup) |
4435 | 107 | 146 | ||
4436 | 108 | 147 | ||
4437 | 109 | def import_function(functup, globals=None, def_=True): | 148 | def import_function(functup, globals=None, def_=True): |
4439 | 110 | name, modname, defaults, kwdefaults, codetup = functup | 149 | name, modname, defaults, codetup = functup |
4440 | 111 | if globals is None: | 150 | if globals is None: |
4441 | 112 | try: | 151 | try: |
4442 | 113 | mod = __import__(modname, None, None, "*") | 152 | mod = __import__(modname, None, None, "*") |
4443 | @@ -116,13 +155,11 @@ def import_function(functup, globals=None, def_=True): | |||
4444 | 116 | globals = mod.__dict__ | 155 | globals = mod.__dict__ |
4445 | 117 | # function globals must be real dicts, sadly: | 156 | # function globals must be real dicts, sadly: |
4446 | 118 | if isinstance(globals, netref.BaseNetref): | 157 | if isinstance(globals, netref.BaseNetref): |
4448 | 119 | from rpyc.utils.classic import obtain | 158 | from plainbox.vendor.rpyc.utils.classic import obtain |
4449 | 120 | globals = obtain(globals) | 159 | globals = obtain(globals) |
4450 | 121 | globals.setdefault('__builtins__', __builtins__) | 160 | globals.setdefault('__builtins__', __builtins__) |
4451 | 122 | codeobj = _import_codetup(codetup) | 161 | codeobj = _import_codetup(codetup) |
4452 | 123 | funcobj = FunctionType(codeobj, globals, name, defaults) | 162 | funcobj = FunctionType(codeobj, globals, name, defaults) |
4453 | 124 | if kwdefaults is not None: | ||
4454 | 125 | funcobj.__kwdefaults__ = {t[0]: t[1] for t in kwdefaults} | ||
4455 | 126 | if def_: | 163 | if def_: |
4456 | 127 | globals[name] = funcobj | 164 | globals[name] = funcobj |
4457 | 128 | return funcobj | 165 | return funcobj |
4458 | diff --git a/plainbox/vendor/rpyc/utils/zerodeploy.py b/plainbox/vendor/rpyc/utils/zerodeploy.py | |||
4459 | index a246e64..bbc4922 100644 | |||
4460 | --- a/plainbox/vendor/rpyc/utils/zerodeploy.py | |||
4461 | +++ b/plainbox/vendor/rpyc/utils/zerodeploy.py | |||
4462 | @@ -55,7 +55,7 @@ $EXTRA_SETUP$ | |||
4463 | 55 | t = ServerCls(SlaveService, hostname = "localhost", port = 0, reuse_addr = True, logger = logger) | 55 | t = ServerCls(SlaveService, hostname = "localhost", port = 0, reuse_addr = True, logger = logger) |
4464 | 56 | thd = t._start_in_thread() | 56 | thd = t._start_in_thread() |
4465 | 57 | 57 | ||
4467 | 58 | sys.stdout.write(f"{t.port}\n") | 58 | sys.stdout.write("%s\n" % (t.port,)) |
4468 | 59 | sys.stdout.flush() | 59 | sys.stdout.flush() |
4469 | 60 | 60 | ||
4470 | 61 | try: | 61 | try: |
4471 | @@ -111,7 +111,7 @@ class DeployedServer(object): | |||
4472 | 111 | major = sys.version_info[0] | 111 | major = sys.version_info[0] |
4473 | 112 | minor = sys.version_info[1] | 112 | minor = sys.version_info[1] |
4474 | 113 | cmd = None | 113 | cmd = None |
4476 | 114 | for opt in [f"python{major}.{minor}", f"python{major}"]: | 114 | for opt in ["python%s.%s" % (major, minor), "python%s" % (major,)]: |
4477 | 115 | try: | 115 | try: |
4478 | 116 | cmd = remote_machine[opt] | 116 | cmd = remote_machine[opt] |
4479 | 117 | except CommandNotFound: | 117 | except CommandNotFound: |
4480 | @@ -155,26 +155,15 @@ class DeployedServer(object): | |||
4481 | 155 | if self.proc is not None: | 155 | if self.proc is not None: |
4482 | 156 | try: | 156 | try: |
4483 | 157 | self.proc.terminate() | 157 | self.proc.terminate() |
4484 | 158 | self.proc.communicate() | ||
4485 | 159 | except Exception: | 158 | except Exception: |
4486 | 160 | pass | 159 | pass |
4487 | 161 | self.proc = None | 160 | self.proc = None |
4488 | 162 | if self.tun is not None: | 161 | if self.tun is not None: |
4489 | 163 | try: | 162 | try: |
4490 | 164 | self.tun._session.proc.terminate() | ||
4491 | 165 | self.tun._session.proc.communicate() | ||
4492 | 166 | self.tun.close() | 163 | self.tun.close() |
4493 | 167 | except Exception: | 164 | except Exception: |
4494 | 168 | pass | 165 | pass |
4495 | 169 | self.tun = None | 166 | self.tun = None |
4496 | 170 | if self.remote_machine is not None: | ||
4497 | 171 | try: | ||
4498 | 172 | self.remote_machine._session.proc.terminate() | ||
4499 | 173 | self.remote_machine._session.proc.communicate() | ||
4500 | 174 | self.remote_machine.close() | ||
4501 | 175 | except Exception: | ||
4502 | 176 | pass | ||
4503 | 177 | self.remote_machine = None | ||
4504 | 178 | if self._tmpdir_ctx is not None: | 167 | if self._tmpdir_ctx is not None: |
4505 | 179 | try: | 168 | try: |
4506 | 180 | self._tmpdir_ctx.__exit__(None, None, None) | 169 | self._tmpdir_ctx.__exit__(None, None, None) |
4507 | diff --git a/plainbox/vendor/rpyc/version.py b/plainbox/vendor/rpyc/version.py | |||
4508 | index 3028d79..d7618e4 100644 | |||
4509 | --- a/plainbox/vendor/rpyc/version.py | |||
4510 | +++ b/plainbox/vendor/rpyc/version.py | |||
4511 | @@ -1,3 +1,3 @@ | |||
4513 | 1 | version = (5, 1, 0) | 1 | version = (4, 1, 4) |
4514 | 2 | version_string = ".".join(map(str, version)) | 2 | version_string = ".".join(map(str, version)) |
4516 | 3 | release_date = "2022-02-26" | 3 | release_date = "2020.1.30" |
Yeah, +1