Merge ~sylvain-pineau/checkbox-ng:revert-remote-api-bump into checkbox-ng: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)
Reviewer Review Type Date Requested Status
Sylvain Pineau (community) Approve
Maciej Kisielewski Approve
Review via email: mp+419476@code.launchpad.net

Description of the change

Revert the remote api branch

To post a comment you must log in.
Revision history for this message
Maciej Kisielewski (kissiel) wrote :

Yeah, +1

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

Subscribers

People subscribed via source and target branches