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 (community) 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
diff --git a/checkbox_ng/config.py b/checkbox_ng/config.py
index a8e8ad0..5a4ba11 100644
--- a/checkbox_ng/config.py
+++ b/checkbox_ng/config.py
@@ -23,62 +23,55 @@
23=====================================================23=====================================================
24"""24"""
25import gettext25import gettext
26import itertools
26import logging27import logging
27import os28import os
2829
29from plainbox.impl.config import Configuration30from plainbox.impl.launcher import DefaultLauncherDefinition
31from plainbox.impl.launcher import LauncherDefinition
3032
3133
32_ = gettext.gettext34_ = gettext.gettext
3335
34_logger = logging.getLogger("config")36_logger = logging.getLogger("config")
3537
36
37SEARCH_DIRS = [
38 '$SNAP_DATA',
39 '/etc/xdg/',
40 '~/.config/',
41 ]
42
43
44def expand_all(path):38def expand_all(path):
45 """Expand both: envvars and ~ in `path`."""
46 return os.path.expandvars(os.path.expanduser(path))39 return os.path.expandvars(os.path.expanduser(path))
4740
4841def load_configs(launcher_file=None):
49def load_configs(launcher_file=None, cfg=None):42 # launcher can override the default name of config files to look for
50 """43 # so first we need to establish the filename to look for
51 Read a chain of configs/launchers.44 configs = []
5245 config_filename = 'checkbox.conf'
53 In theory there can be a very long list of configs that are linked by46 launcher = DefaultLauncherDefinition()
54 specifying config_filename in each. Each time this happen we need to47 if launcher_file:
55 consider the new one and override all the values contained therein.48 configs.append(launcher_file)
56 And after this chain is exhausted the values in the launcher should49 generic_launcher = LauncherDefinition()
57 take precedence over the previously read.50 if not os.path.exists(launcher_file):
58 Warning: some recursion ahead.51 _logger.error(_(
59 """52 "Unable to load launcher '%s'. File not found!"),
60 if not cfg:53 launcher_file)
61 cfg = Configuration()54 raise SystemExit(1)
62 previous_cfg_name = cfg.get_value('config', 'config_filename')55 generic_launcher.read(launcher_file)
63 if os.path.isabs(expand_all(previous_cfg_name)):56 config_filename = os.path.expandvars(os.path.expanduser(
64 cfg.update_from_another(57 generic_launcher.config_filename))
65 Configuration.from_path(expand_all(previous_cfg_name)),58 launcher = generic_launcher.get_concrete_launcher()
66 'config file: {}'.format(previous_cfg_name))59 if os.path.isabs(config_filename):
60 configs.append(config_filename)
67 else:61 else:
68 for sdir in SEARCH_DIRS:62 search_dirs = [
69 config = expand_all(os.path.join(sdir, previous_cfg_name))63 '$SNAP_DATA',
64 '/etc/xdg/',
65 '~/.config/',
66 ]
67 for d in search_dirs:
68 config = expand_all(os.path.join(d, config_filename))
70 if os.path.exists(config):69 if os.path.exists(config):
71 cfg.update_from_another(70 configs.append(config)
72 Configuration.from_path(config),71 launcher.read(configs)
73 'config file: {}'.format(config))72 if launcher.problem_list:
74 else:73 _logger.error(_("Unable to start launcher because of errors:"))
75 _logger.info(74 for problem in launcher.problem_list:
76 "Referenced config file doesn't exist: %s", config)75 _logger.error("%s", str(problem))
77 new_cfg_filename = cfg.get_value('config', 'config_filename')76 raise SystemExit(1)
78 if new_cfg_filename != previous_cfg_name:77 return launcher
79 load_configs(launcher_file, cfg)
80 if launcher_file:
81 cfg.update_from_another(
82 Configuration.from_path(launcher_file),
83 'Launcher file: {}'.format(launcher_file))
84 return cfg
diff --git a/checkbox_ng/launcher/check_config.py b/checkbox_ng/launcher/check_config.py
index 0ac3088..359d3ca 100644
--- a/checkbox_ng/launcher/check_config.py
+++ b/checkbox_ng/launcher/check_config.py
@@ -1,6 +1,6 @@
1# This file is part of Checkbox.1# This file is part of Checkbox.
2#2#
3# Copyright 2018-2021 Canonical Ltd.3# Copyright 2018-2019 Canonical Ltd.
4# Written by:4# Written by:
5# Maciej Kisielewski <maciej.kisielewski@canonical.com>5# Maciej Kisielewski <maciej.kisielewski@canonical.com>
6#6#
@@ -15,38 +15,41 @@
15#15#
16# You should have received a copy of the GNU General Public License16# You should have received a copy of the GNU General Public License
17# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.17# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
18"""This module contains the implementation of the `check-config` subcmd."""18from plainbox.impl.secure.config import ValidationError
19from plainbox.i18n import gettext as _
1920
20from checkbox_ng.config import load_configs21from checkbox_ng.config import load_configs
2122
2223
23class CheckConfig():24class CheckConfig():
24 """Implementation of the `check-config` sub-command."""25 def invoked(self, ctx):
25 @staticmethod
26 def invoked(_):
27 """Function that's run with `check-config` invocation."""
28 config = load_configs()26 config = load_configs()
29 print("Configuration files:")27 print(_("Configuration files:"))
30 for source in config.sources:28 for filename in config.filename_list:
31 print(" - {}".format(source))29 print(" - {}".format(filename))
32 for sect_name, section in config.sections.items():30 for variable in config.Meta.variable_list:
33 print(" [{0}]".format(sect_name))31 print(" [{0}]".format(variable.section))
34 for var_name in section.keys():32 print(" {0}={1}".format(
35 value = config.get_value(sect_name, var_name)33 variable.name,
36 if isinstance(value, list):34 variable.__get__(config, config.__class__)))
37 value = ', '.join(value)35 for section in config.Meta.section_list:
38 origin = config.get_origin(sect_name, var_name)36 print(" [{0}]".format(section.name))
39 origin = "From {}".format(origin) if origin else "(Default)"37 section_value = section.__get__(config, config.__class__)
40 key_val = "{}={}".format(var_name, value)38 if section_value:
41 print(" {0: <34} {1}".format(key_val, origin))39 for key, value in sorted(section_value.items()):
42 problems = config.get_problems()40 print(" {0}={1}".format(key, value))
43 if not problems:41 if config.problem_list:
44 print("No problems with config(s) found!")42 print(_("Problems:"))
43 for problem in config.problem_list:
44 if isinstance(problem, ValidationError):
45 print(_(" - variable {0}: {1}").format(
46 problem.variable.name, problem.message))
47 else:
48 print(" - {0}".format(problem.message))
49 return 1
50 else:
51 print(_("No validation problems found"))
45 return 052 return 0
46 print('Problems:')
47 for problem in problems:
48 print('- ', problem)
49 return 1
5053
51 def register_arguments(self, parser):54 def register_arguments(self, parser):
52 """Register extra args for this subcmd. No extra args ATM."""55 pass
diff --git a/checkbox_ng/launcher/checkbox_cli.py b/checkbox_ng/launcher/checkbox_cli.py
index 9740eb8..b69da17 100644
--- a/checkbox_ng/launcher/checkbox_cli.py
+++ b/checkbox_ng/launcher/checkbox_cli.py
@@ -27,6 +27,8 @@ import subprocess
27import sys27import sys
2828
29from plainbox.impl.jobcache import ResourceJobCache29from plainbox.impl.jobcache import ResourceJobCache
30from plainbox.impl.launcher import DefaultLauncherDefinition
31from plainbox.impl.launcher import LauncherDefinition
30from plainbox.impl.session.assistant import SessionAssistant32from plainbox.impl.session.assistant import SessionAssistant
3133
32from checkbox_ng.config import load_configs34from checkbox_ng.config import load_configs
diff --git a/checkbox_ng/launcher/master.py b/checkbox_ng/launcher/master.py
index 6f28444..730fc02 100644
--- a/checkbox_ng/launcher/master.py
+++ b/checkbox_ng/launcher/master.py
@@ -37,7 +37,8 @@ from functools import partial
37from tempfile import SpooledTemporaryFile37from tempfile import SpooledTemporaryFile
3838
39from plainbox.impl.color import Colorizer39from plainbox.impl.color import Colorizer
40from plainbox.impl.config import Configuration40from plainbox.impl.launcher import DefaultLauncherDefinition
41from plainbox.impl.secure.config import Unset
41from plainbox.impl.session.remote_assistant import RemoteSessionAssistant42from plainbox.impl.session.remote_assistant import RemoteSessionAssistant
42from plainbox.vendor import rpyc43from plainbox.vendor import rpyc
43from checkbox_ng.urwid_ui import TestPlanBrowser44from checkbox_ng.urwid_ui import TestPlanBrowser
@@ -110,7 +111,7 @@ class RemoteMaster(ReportsStage, MainLoopStage):
110111
111 @property112 @property
112 def is_interactive(self):113 def is_interactive(self):
113 return (self.launcher.get_value('ui', 'type') == 'interactive' and114 return (self.launcher.ui_type == 'interactive' and
114 sys.stdin.isatty() and sys.stdout.isatty())115 sys.stdin.isatty() and sys.stdout.isatty())
115116
116 @property117 @property
@@ -128,7 +129,7 @@ class RemoteMaster(ReportsStage, MainLoopStage):
128 self._is_bootstrapping = False129 self._is_bootstrapping = False
129 self._target_host = ctx.args.host130 self._target_host = ctx.args.host
130 self._normal_user = ''131 self._normal_user = ''
131 self.launcher = Configuration()132 self.launcher = DefaultLauncherDefinition()
132 if ctx.args.launcher:133 if ctx.args.launcher:
133 expanded_path = os.path.expanduser(ctx.args.launcher)134 expanded_path = os.path.expanduser(ctx.args.launcher)
134 if not os.path.exists(expanded_path):135 if not os.path.exists(expanded_path):
@@ -136,8 +137,7 @@ class RemoteMaster(ReportsStage, MainLoopStage):
136 expanded_path))137 expanded_path))
137 with open(expanded_path, 'rt') as f:138 with open(expanded_path, 'rt') as f:
138 self._launcher_text = f.read()139 self._launcher_text = f.read()
139 self.launcher = Configuration.from_text(140 self.launcher.read_string(self._launcher_text)
140 self._launcher_text, 'Remote:{}'.format(expanded_path))
141 if ctx.args.user:141 if ctx.args.user:
142 self._normal_user = ctx.args.user142 self._normal_user = ctx.args.user
143 timeout = 600143 timeout = 600
@@ -185,9 +185,21 @@ class RemoteMaster(ReportsStage, MainLoopStage):
185 nonlocal keep_running185 nonlocal keep_running
186 keep_running = False186 keep_running = False
187 server_msg = msg187 server_msg = msg
188 conn.root.register_master_blaster(quitter)188 with contextlib.suppress(AttributeError):
189 # TODO: REMOTE_API
190 # when bumping the remote api make this bit obligatory
191 # i.e. remove the suppressing
192 conn.root.register_master_blaster(quitter)
189 self._sa = conn.root.get_sa()193 self._sa = conn.root.get_sa()
190 self.sa.conn = conn194 self.sa.conn = conn
195 # TODO: REMOTE API RAPI: Remove this API on the next RAPI bump
196 # the check and bailout is not needed if the slave as up to
197 # date as this master, so after bumping RAPI we can assume
198 # that slave is always passwordless
199 if not self.sa.passwordless_sudo:
200 raise SystemExit(
201 _("This version of Checkbox requires the service"
202 " to be run as root"))
191 try:203 try:
192 slave_api_version = self.sa.get_remote_api_version()204 slave_api_version = self.sa.get_remote_api_version()
193 except AttributeError:205 except AttributeError:
@@ -255,8 +267,8 @@ class RemoteMaster(ReportsStage, MainLoopStage):
255 tps = self.sa.start_session(configuration)267 tps = self.sa.start_session(configuration)
256 except RuntimeError as exc:268 except RuntimeError as exc:
257 raise SystemExit(exc.args[0]) from exc269 raise SystemExit(exc.args[0]) from exc
258 if self.launcher.get_value('test plan', 'forced'):270 if self.launcher.test_plan_forced:
259 self.select_tp(self.launcher.get_value('test plan', 'unit'))271 self.select_tp(self.launcher.test_plan_default_selection)
260 self.select_jobs(self.jobs)272 self.select_jobs(self.jobs)
261 else:273 else:
262 self.interactively_choose_tp(tps)274 self.interactively_choose_tp(tps)
@@ -299,8 +311,8 @@ class RemoteMaster(ReportsStage, MainLoopStage):
299 return val.lower() in ('y', 'yes', 't', 'true', 'on', '1')311 return val.lower() in ('y', 'yes', 't', 'true', 'on', '1')
300312
301 def select_jobs(self, all_jobs):313 def select_jobs(self, all_jobs):
302 if self.launcher.get_value('test selection', 'forced'):314 if self.launcher.test_selection_forced:
303 if self.launcher.manifest:315 if self.launcher.manifest is not Unset:
304 self.sa.save_manifest(316 self.sa.save_manifest(
305 {manifest_id:317 {manifest_id:
306 self._strtobool(self.launcher.manifest[manifest_id]) for318 self._strtobool(self.launcher.manifest[manifest_id]) for
@@ -340,7 +352,7 @@ class RemoteMaster(ReportsStage, MainLoopStage):
340 Returns True if the remote should keep running.352 Returns True if the remote should keep running.
341 And False if it should quit.353 And False if it should quit.
342 """354 """
343 if self.launcher.get_value('ui', 'type') == 'silent':355 if self.launcher.ui_type == 'silent':
344 self._sa.terminate()356 self._sa.terminate()
345 return False357 return False
346 response = interrupt_dialog(self._target_host)358 response = interrupt_dialog(self._target_host)
@@ -360,7 +372,7 @@ class RemoteMaster(ReportsStage, MainLoopStage):
360372
361 def finish_session(self):373 def finish_session(self):
362 print(self.C.header("Results"))374 print(self.C.header("Results"))
363 if self.launcher.get_value('launcher', 'local_submission'):375 if self.launcher.local_submission:
364 # Disable SIGINT while we save local results376 # Disable SIGINT while we save local results
365 with contextlib.ExitStack() as stack:377 with contextlib.ExitStack() as stack:
366 tmp_sig = signal.signal(signal.SIGINT, signal.SIG_IGN)378 tmp_sig = signal.signal(signal.SIGINT, signal.SIG_IGN)
@@ -378,7 +390,7 @@ class RemoteMaster(ReportsStage, MainLoopStage):
378 self.run_jobs()390 self.run_jobs()
379391
380 def _handle_last_job_after_resume(self, resumed_session_info):392 def _handle_last_job_after_resume(self, resumed_session_info):
381 if self.launcher.get_value('ui', 'type') == 'silent':393 if self.launcher.ui_type == 'silent':
382 time.sleep(20)394 time.sleep(20)
383 else:395 else:
384 resume_dialog(10)396 resume_dialog(10)
@@ -408,11 +420,11 @@ class RemoteMaster(ReportsStage, MainLoopStage):
408 self._run_jobs(jobs_repr, total_num)420 self._run_jobs(jobs_repr, total_num)
409 rerun_candidates = self.sa.get_rerun_candidates('manual')421 rerun_candidates = self.sa.get_rerun_candidates('manual')
410 if rerun_candidates:422 if rerun_candidates:
411 if self.launcher.get_value('ui', 'type') == 'interactive':423 if self.launcher.ui_type == 'interactive':
412 while True:424 while True:
413 if not self._maybe_manual_rerun_jobs():425 if not self._maybe_manual_rerun_jobs():
414 break426 break
415 if self.launcher.get_value('ui', 'auto_retry'):427 if self.launcher.auto_retry:
416 while True:428 while True:
417 if not self._maybe_auto_rerun_jobs():429 if not self._maybe_auto_rerun_jobs():
418 break430 break
@@ -498,7 +510,7 @@ class RemoteMaster(ReportsStage, MainLoopStage):
498 if not rerun_candidates:510 if not rerun_candidates:
499 return False511 return False
500 # we wait before retrying512 # we wait before retrying
501 delay = self.launcher.get_value('ui', 'delay_before_retry')513 delay = self.launcher.delay_before_retry
502 _logger.info(_("Waiting {} seconds before retrying failed"514 _logger.info(_("Waiting {} seconds before retrying failed"
503 " jobs...".format(delay)))515 " jobs...".format(delay)))
504 time.sleep(delay)516 time.sleep(delay)
diff --git a/checkbox_ng/launcher/stages.py b/checkbox_ng/launcher/stages.py
index 05566be..126324b 100644
--- a/checkbox_ng/launcher/stages.py
+++ b/checkbox_ng/launcher/stages.py
@@ -26,11 +26,9 @@ import json
26import logging26import logging
27import os27import os
28import time28import time
29import textwrap
3029
31from plainbox.abc import IJobResult30from plainbox.abc import IJobResult
32from plainbox.i18n import pgettext as C_31from plainbox.i18n import pgettext as C_
33from plainbox.impl.config import Configuration
34from plainbox.impl.result import JobResultBuilder32from plainbox.impl.result import JobResultBuilder
35from plainbox.impl.result import tr_outcome33from plainbox.impl.result import tr_outcome
36from plainbox.impl.transport import InvalidSecureIDError34from plainbox.impl.transport import InvalidSecureIDError
@@ -318,44 +316,42 @@ class ReportsStage(CheckboxUiStage):
318 self._export_fn = export_fn316 self._export_fn = export_fn
319317
320 def _prepare_stock_report(self, report):318 def _prepare_stock_report(self, report):
321319 # this is purposefully not using pythonic dict-keying for better
322 new_origin = 'stock_reports'320 # readability
321 if not self.sa.config.transports:
322 self.sa.config.transports = dict()
323 if not self.sa.config.exporters:
324 self.sa.config.exporters = dict()
325 if not self.sa.config.reports:
326 self.sa.config.reports = dict()
323 if report == 'text':327 if report == 'text':
324 additional_config = Configuration.from_text(textwrap.dedent("""328 self.sa.config.exporters['text'] = {
325 [exporter:text]329 'unit': 'com.canonical.plainbox::text'}
326 unit = com.canonical.plainbox::text330 self.sa.config.transports['stdout'] = {
327 [transport:stdout]331 'type': 'stream', 'stream': 'stdout'}
328 stream = stdout332 # '1_' prefix ensures ordering amongst other stock reports. This
329 type = stream333 # report name does not appear anywhere (because of forced: yes)
330 [report:1_text_to_screen]334 self.sa.config.reports['1_text_to_screen'] = {
331 exporter = text335 'transport': 'stdout', 'exporter': 'text', 'forced': 'yes'}
332 forced = yes
333 transport = stdout
334 """), new_origin)
335 self.sa.config.update_from_another(additional_config, new_origin)
336 elif report == 'certification':336 elif report == 'certification':
337 additional_config = Configuration.from_text(textwrap.dedent("""337 self.sa.config.exporters['tar'] = {
338 [exporter:tar]338 'unit': 'com.canonical.plainbox::tar'}
339 unit = com.canonical.plainbox::tar339 self.sa.config.transports['c3'] = {
340 [transport:c3]340 'type': 'submission-service',
341 type = submission-service341 'secure_id': self.sa.config.transports.get('c3', {}).get(
342 [report:upload to certification]342 'secure_id', None)}
343 exporter = tar343 self.sa.config.reports['upload to certification'] = {
344 transport = c3344 'transport': 'c3', 'exporter': 'tar'}
345 """), new_origin)
346 self.sa.config.update_from_another(additional_config, new_origin)
347 elif report == 'certification-staging':345 elif report == 'certification-staging':
348 additional_config = Configuration.from_text(textwrap.dedent("""346 self.sa.config.exporters['tar'] = {
349 [exporter:tar]347 'unit': 'com.canonical.plainbox::tar'}
350 unit = com.canonical.plainbox::tar348 self.sa.config.transports['c3-staging'] = {
351 [transport:c3]349 'type': 'submission-service',
352 staging = yes350 'secure_id': self.sa.config.transports.get('c3', {}).get(
353 type = submission-service351 'secure_id', None),
354 [report:upload to certification-staging]352 'staging': 'yes'}
355 exporter = tar353 self.sa.config.reports['upload to certification-staging'] = {
356 transport = c3354 'transport': 'c3-staging', 'exporter': 'tar'}
357 """), new_origin)
358 self.sa.config.update_from_another(additional_config, new_origin)
359 elif report == 'submission_files':355 elif report == 'submission_files':
360 # LP:1585326 maintain isoformat but removing ':' chars that cause356 # LP:1585326 maintain isoformat but removing ':' chars that cause
361 # issues when copying files.357 # issues when copying files.
@@ -368,21 +364,21 @@ class ReportsStage(CheckboxUiStage):
368 ('tar', '.tar.xz')]:364 ('tar', '.tar.xz')]:
369 path = os.path.join(self.base_dir, ''.join(365 path = os.path.join(self.base_dir, ''.join(
370 ['submission_', timestamp, file_ext]))366 ['submission_', timestamp, file_ext]))
371 template = textwrap.dedent("""367 self.sa.config.transports['{}_file'.format(exporter)] = {
372 [transport:{exporter}_file]368 'type': 'file',
373 path = {path}369 'path': path}
374 type = file370 if exporter not in self.sa.config.exporters:
375 [exporter:{exporter}]371 self.sa.config.exporters[exporter] = {
376 unit = com.canonical.plainbox::{exporter}372 'unit': 'com.canonical.plainbox::{}'.format(
377 [report:2_{exporter}_file]373 exporter)}
378 exporter = {exporter}374 if not self.sa.config.exporters[exporter].get('unit'):
379 forced = yes375 unit = 'com.canonical.plainbox::{}'.format(exporter)
380 transport = {exporter}_file376 self.sa.config.exporters[exporter]['unit'] = unit
381 """)377 self.sa.config.reports['2_{}_file'.format(exporter)] = {
382 additional_config = Configuration.from_text(378 'transport': '{}_file'.format(exporter),
383 template.format(exporter=exporter, path=path), new_origin)379 'exporter': '{}'.format(exporter),
384 self.sa.config.update_from_another(380 'forced': 'yes'
385 additional_config, new_origin)381 }
386382
387 def _prepare_transports(self):383 def _prepare_transports(self):
388 self.base_dir = os.path.join(384 self.base_dir = os.path.join(
@@ -398,9 +394,7 @@ class ReportsStage(CheckboxUiStage):
398 # depending on the type of transport we need to pick variable that394 # depending on the type of transport we need to pick variable that
399 # serves as the 'where' param for the transport. In case of395 # serves as the 'where' param for the transport. In case of
400 # certification site the URL is supplied here396 # certification site the URL is supplied here
401 transport_cfg = self.sa.config.get_parametric_sections(397 tr_type = self.sa.config.transports[transport]['type']
402 'transport')[transport]
403 tr_type = transport_cfg['type']
404 if tr_type not in self._available_transports:398 if tr_type not in self._available_transports:
405 _logger.error(_("Unrecognized type '%s' of transport '%s'"),399 _logger.error(_("Unrecognized type '%s' of transport '%s'"),
406 tr_type, transport)400 tr_type, transport)
@@ -408,11 +402,14 @@ class ReportsStage(CheckboxUiStage):
408 cls = self._available_transports[tr_type]402 cls = self._available_transports[tr_type]
409 if tr_type == 'file':403 if tr_type == 'file':
410 self.transports[transport] = cls(404 self.transports[transport] = cls(
411 os.path.expanduser(transport_cfg['path']))405 os.path.expanduser(
406 self.sa.config.transports[transport]['path']))
412 elif tr_type == 'stream':407 elif tr_type == 'stream':
413 self.transports[transport] = cls(transport_cfg['stream'])408 self.transports[transport] = cls(
409 self.sa.config.transports[transport]['stream'])
414 elif tr_type == 'submission-service':410 elif tr_type == 'submission-service':
415 secure_id = transport_cfg.get('secure_id', None)411 secure_id = self.sa.config.transports[transport].get(
412 'secure_id', None)
416 if self.is_interactive:413 if self.is_interactive:
417 new_description = input(self.C.BLUE(_(414 new_description = input(self.C.BLUE(_(
418 'Enter submission description (press Enter to skip): ')))415 'Enter submission description (press Enter to skip): ')))
@@ -428,7 +425,7 @@ class ReportsStage(CheckboxUiStage):
428 options = "secure_id={}".format(secure_id)425 options = "secure_id={}".format(secure_id)
429 else:426 else:
430 options = ""427 options = ""
431 if transport_cfg.get('staging', False):428 if self.sa.config.transports[transport].get('staging', False):
432 url = ('https://certification.staging.canonical.com/'429 url = ('https://certification.staging.canonical.com/'
433 'api/v1/submission/{}/'.format(secure_id))430 'api/v1/submission/{}/'.format(secure_id))
434 elif os.getenv('C3_URL'):431 elif os.getenv('C3_URL'):
@@ -440,15 +437,14 @@ class ReportsStage(CheckboxUiStage):
440 self.transports[transport] = cls(url, options)437 self.transports[transport] = cls(url, options)
441438
442 def _export_results(self):439 def _export_results(self):
443 stock_reports = self.sa.config.get_value('launcher', 'stock_reports')440 if 'none' not in self.sa.config.stock_reports:
444 if 'none' not in stock_reports:441 for report in self.sa.config.stock_reports:
445 for report in stock_reports:
446 if report in ['certification', 'certification-staging']:442 if report in ['certification', 'certification-staging']:
447 # skip stock c3 report if secure_id is not given from443 # skip stock c3 report if secure_id is not given from
448 # config files or launchers, and the UI is non-interactive444 # config files or launchers, and the UI is non-interactive
449 # (silent)445 # (silent)
450 if ('transport:c3' not in self.sa.config.sections.keys()446 if ('c3' not in self.sa.config.transports and
451 and not self.is_interactive):447 not self.is_interactive):
452 continue448 continue
453 # don't generate stock c3 reports if sideloaded providers449 # don't generate stock c3 reports if sideloaded providers
454 # were in use, something that should only be done during450 # were in use, something that should only be done during
@@ -461,9 +457,7 @@ class ReportsStage(CheckboxUiStage):
461 # reports are stored in an ordinary dict(), so sorting them ensures457 # reports are stored in an ordinary dict(), so sorting them ensures
462 # the same order of submitting them between runs, and if they458 # the same order of submitting them between runs, and if they
463 # share common prefix, they are next to each other459 # share common prefix, they are next to each other
464 for name, params in sorted(460 for name, params in sorted(self.sa.config.reports.items()):
465 self.sa.config.get_parametric_sections('report').items()):
466
467 # don't generate stock c3 reports if sideloaded providers461 # don't generate stock c3 reports if sideloaded providers
468 # were in use, something that should only be done during462 # were in use, something that should only be done during
469 # development463 # development
@@ -482,10 +476,8 @@ class ReportsStage(CheckboxUiStage):
482 cmd = 'y'476 cmd = 'y'
483 if cmd == 'n':477 if cmd == 'n':
484 continue478 continue
485 all_exporters = self.sa.config.get_parametric_sections('exporter')479 exporter_id = self.sa.config.exporters[params['exporter']]['unit']
486 exporter_id = self.sa.config.get_parametric_sections('exporter')[480 exporter_options = self.sa.config.exporters[
487 params['exporter']]['unit']
488 exp_options = self.sa.config.get_parametric_sections('exporter')[
489 params['exporter']].get('options', '').split()481 params['exporter']].get('options', '').split()
490 done_sending = False482 done_sending = False
491 while not done_sending:483 while not done_sending:
@@ -497,7 +489,7 @@ class ReportsStage(CheckboxUiStage):
497 else:489 else:
498 try:490 try:
499 result = self.sa.export_to_transport(491 result = self.sa.export_to_transport(
500 exporter_id, transport, exp_options)492 exporter_id, transport, exporter_options)
501 except ExporterError as exc:493 except ExporterError as exc:
502 _logger.warning(494 _logger.warning(
503 _("Problem occured when preparing %s report:"495 _("Problem occured when preparing %s report:"
@@ -525,14 +517,14 @@ class ReportsStage(CheckboxUiStage):
525 done_sending = True517 done_sending = True
526 continue518 continue
527 if self._retry_dialog():519 if self._retry_dialog():
528 self.sa.config.sections['transports']['c3'].pop(520 self.sa.config.transports['c3'].pop('secure_id')
529 'secure_id')
530 continue521 continue
531 except Exception as exc:522 except Exception:
532 _logger.error(523 _logger.error(
533 _("Problem with a '%s' report using '%s' exporter "524 _("Problem with a '%s' report using '%s' exporter "
534 "sent to '%s' transport. Reason %s"),525 "sent to '%s' transport."),
535 name, exporter_id, transport.url, exc)526 name, exporter_id, transport.url)
527 self._reset_auto_submission_retries()
536 done_sending = True528 done_sending = True
537529
538 def _retry_dialog(self):530 def _retry_dialog(self):
@@ -554,14 +546,3 @@ class ReportsStage(CheckboxUiStage):
554 return True546 return True
555547
556 return False548 return False
557
558template = textwrap.dedent("""
559 [transport:{exporter}_file]
560 type = file
561 path = {path}
562 [exporter:{exporter}]
563 unit = com.canonical.plainbox::{exporter}
564 [report:2_{exporter}_file]
565 transport = {exporter}_file
566 exporter = {exporter}
567 forced = yes""")
diff --git a/checkbox_ng/launcher/subcommands.py b/checkbox_ng/launcher/subcommands.py
index 1d3c278..69f4a4a 100644
--- a/checkbox_ng/launcher/subcommands.py
+++ b/checkbox_ng/launcher/subcommands.py
@@ -42,6 +42,7 @@ from plainbox.impl.execution import UnifiedRunner
42from plainbox.impl.highlevel import Explorer42from plainbox.impl.highlevel import Explorer
43from plainbox.impl.result import MemoryJobResult43from plainbox.impl.result import MemoryJobResult
44from plainbox.impl.runner import slugify44from plainbox.impl.runner import slugify
45from plainbox.impl.secure.config import Unset
45from plainbox.impl.secure.sudo_broker import sudo_password_provider46from plainbox.impl.secure.sudo_broker import sudo_password_provider
46from plainbox.impl.session.assistant import SA_RESTARTABLE47from plainbox.impl.session.assistant import SA_RESTARTABLE
47from plainbox.impl.session.restart import detect_restart_strategy48from plainbox.impl.session.restart import detect_restart_strategy
@@ -167,10 +168,10 @@ class Launcher(MainLoopStage, ReportsStage):
167 return self._C168 return self._C
168169
169 def get_sa_api_version(self):170 def get_sa_api_version(self):
170 return '0.99'171 return self.launcher.api_version
171172
172 def get_sa_api_flags(self):173 def get_sa_api_flags(self):
173 return [SA_RESTARTABLE]174 return self.launcher.api_flags
174175
175 def invoked(self, ctx):176 def invoked(self, ctx):
176 if ctx.args.version:177 if ctx.args.version:
@@ -183,12 +184,12 @@ class Launcher(MainLoopStage, ReportsStage):
183 # exited by now, so validation passed184 # exited by now, so validation passed
184 print(_("Launcher seems valid."))185 print(_("Launcher seems valid."))
185 return186 return
186 self.configuration = load_configs(ctx.args.launcher)187 self.launcher = load_configs(ctx.args.launcher)
187 logging_level = {188 logging_level = {
188 'normal': logging.WARNING,189 'normal': logging.WARNING,
189 'verbose': logging.INFO,190 'verbose': logging.INFO,
190 'debug': logging.DEBUG,191 'debug': logging.DEBUG,
191 }[self.configuration.get_value('ui', 'verbosity')]192 }[self.launcher.verbosity]
192 if not ctx.args.verbose and not ctx.args.debug:193 if not ctx.args.verbose and not ctx.args.debug:
193 # Command line args take precendence194 # Command line args take precendence
194 logging.basicConfig(level=logging_level)195 logging.basicConfig(level=logging_level)
@@ -199,28 +200,25 @@ class Launcher(MainLoopStage, ReportsStage):
199 # replace the previously built SA with the defaults200 # replace the previously built SA with the defaults
200 self._configure_restart(ctx)201 self._configure_restart(ctx)
201 self._prepare_transports()202 self._prepare_transports()
202 ctx.sa.use_alternate_configuration(self.configuration)203 ctx.sa.use_alternate_configuration(self.launcher)
203 if not self._maybe_resume_session():204 if not self._maybe_resume_session():
204 self._start_new_session()205 self._start_new_session()
205 self._pick_jobs_to_run()206 self._pick_jobs_to_run()
206 if not self.ctx.sa.get_static_todo_list():207 if not self.ctx.sa.get_static_todo_list():
207 return 0208 return 0
208 if 'submission_files' in self.configuration.get_value(209 if 'submission_files' in self.launcher.stock_reports:
209 'launcher', 'stock_reports'):
210 print("Reports will be saved to: {}".format(self.base_dir))210 print("Reports will be saved to: {}".format(self.base_dir))
211 # we initialize the nb of attempts for all the selected jobs...211 # we initialize the nb of attempts for all the selected jobs...
212 for job_id in self.ctx.sa.get_dynamic_todo_list():212 for job_id in self.ctx.sa.get_dynamic_todo_list():
213 job_state = self.ctx.sa.get_job_state(job_id)213 job_state = self.ctx.sa.get_job_state(job_id)
214 job_state.attempts = self.configuration.get_value(214 job_state.attempts = self.launcher.max_attempts
215 'ui', 'max_attempts')
216 # ... before running them215 # ... before running them
217 self._run_jobs(self.ctx.sa.get_dynamic_todo_list())216 self._run_jobs(self.ctx.sa.get_dynamic_todo_list())
218 if self.is_interactive and not self.configuration.get_value(217 if self.is_interactive and not self.launcher.auto_retry:
219 'ui', 'auto_retry'):
220 while True:218 while True:
221 if not self._maybe_rerun_jobs():219 if not self._maybe_rerun_jobs():
222 break220 break
223 elif self.configuration.get_value('ui', 'auto_retry'):221 elif self.launcher.auto_retry:
224 while True:222 while True:
225 if not self._maybe_auto_rerun_jobs():223 if not self._maybe_auto_rerun_jobs():
226 break224 break
@@ -237,18 +235,23 @@ class Launcher(MainLoopStage, ReportsStage):
237235
238 We can then interact with the user when we encounter OUTCOME_UNDECIDED.236 We can then interact with the user when we encounter OUTCOME_UNDECIDED.
239 """237 """
240 return (self.configuration.get_value('ui', 'type') == 'interactive'238 return (self.launcher.ui_type == 'interactive' and
241 and sys.stdin.isatty() and sys.stdout.isatty())239 sys.stdin.isatty() and sys.stdout.isatty())
242240
243 def _configure_restart(self, ctx):241 def _configure_restart(self, ctx):
244 if SA_RESTARTABLE not in self.get_sa_api_flags():242 if SA_RESTARTABLE not in self.get_sa_api_flags():
245 return243 return
246 if self.configuration.get_value('restart', 'strategy'):244 if self.launcher.restart_strategy:
247 try:245 try:
248 cls = get_strategy_by_name(246 cls = get_strategy_by_name(
249 self.configuration.get_value('restart', 'strategy'))247 self.launcher.restart_strategy)
250 strategy = cls(**self.configuration.get_strategy_kwargs())248 kwargs = copy.deepcopy(self.launcher.restart)
249 # [restart] section has the kwargs for the strategy initializer
250 # and the 'strategy' which is not one, let's pop it
251 kwargs.pop('strategy')
252 strategy = cls(**kwargs)
251 ctx.sa.use_alternate_restart_strategy(strategy)253 ctx.sa.use_alternate_restart_strategy(strategy)
254
252 except KeyError:255 except KeyError:
253 _logger.warning(_('Unknown restart strategy: %s', (256 _logger.warning(_('Unknown restart strategy: %s', (
254 self.launcher.restart_strategy)))257 self.launcher.restart_strategy)))
@@ -278,7 +281,7 @@ class Launcher(MainLoopStage, ReportsStage):
278 respawn_cmd += os.path.abspath(ctx.args.launcher) + ' '281 respawn_cmd += os.path.abspath(ctx.args.launcher) + ' '
279 respawn_cmd += '--resume {}' # interpolate with session_id282 respawn_cmd += '--resume {}' # interpolate with session_id
280 ctx.sa.configure_application_restart(283 ctx.sa.configure_application_restart(
281 lambda session_id: [respawn_cmd.format(session_id)], 'local')284 lambda session_id: [respawn_cmd.format(session_id)])
282285
283 def _maybe_resume_session(self):286 def _maybe_resume_session(self):
284 resume_candidates = list(self.ctx.sa.get_resumable_sessions())287 resume_candidates = list(self.ctx.sa.get_resumable_sessions())
@@ -335,21 +338,18 @@ class Launcher(MainLoopStage, ReportsStage):
335338
336 def _start_new_session(self):339 def _start_new_session(self):
337 print(_("Preparing..."))340 print(_("Preparing..."))
338 title = self.ctx.args.title or self.configuration.get_value(341 title = self.ctx.args.title or self.launcher.session_title
339 'launcher', 'session_title')342 title = title or self.launcher.app_id
340 title = title or self.configuration.get_value('launcher', 'app_id')343 if self.launcher.app_version:
341 if self.configuration.get_value('launcher', 'app_version'):344 title += ' {}'.format(self.launcher.app_version)
342 title += ' {}'.format(self.configuration.get_value(
343 'launcher', 'app_version'))
344 runner_kwargs = {345 runner_kwargs = {
345 'normal_user_provider': lambda: self.configuration.get_value(346 'normal_user_provider': lambda: self.launcher.normal_user,
346 'daemon', 'normal_user'),
347 'password_provider': sudo_password_provider.get_sudo_password,347 'password_provider': sudo_password_provider.get_sudo_password,
348 'stdin': None,348 'stdin': None,
349 }349 }
350 self.ctx.sa.start_new_session(title, UnifiedRunner, runner_kwargs)350 self.ctx.sa.start_new_session(title, UnifiedRunner, runner_kwargs)
351 if self.configuration.get_value('test plan', 'forced'):351 if self.launcher.test_plan_forced:
352 tp_id = self.configuration.get_value('test plan', 'unit')352 tp_id = self.launcher.test_plan_default_selection
353 if tp_id not in self.ctx.sa.get_test_plans():353 if tp_id not in self.ctx.sa.get_test_plans():
354 _logger.error(_(354 _logger.error(_(
355 'The test plan "%s" is not available!'), tp_id)355 'The test plan "%s" is not available!'), tp_id)
@@ -365,8 +365,7 @@ class Launcher(MainLoopStage, ReportsStage):
365 if tp_id is None:365 if tp_id is None:
366 raise SystemExit(_("No test plan selected."))366 raise SystemExit(_("No test plan selected."))
367 self.ctx.sa.select_test_plan(tp_id)367 self.ctx.sa.select_test_plan(tp_id)
368 description = self.ctx.args.message or self.configuration.get_value(368 description = self.ctx.args.message or self.launcher.session_desc
369 'launcher', 'session_desc')
370 self.ctx.sa.update_app_blob(json.dumps(369 self.ctx.sa.update_app_blob(json.dumps(
371 {'testplan_id': tp_id,370 {'testplan_id': tp_id,
372 'description': description}).encode("UTF-8"))371 'description': description}).encode("UTF-8"))
@@ -381,7 +380,7 @@ class Launcher(MainLoopStage, ReportsStage):
381 def _interactively_pick_test_plan(self):380 def _interactively_pick_test_plan(self):
382 test_plan_ids = self.ctx.sa.get_test_plans()381 test_plan_ids = self.ctx.sa.get_test_plans()
383 filtered_tp_ids = set()382 filtered_tp_ids = set()
384 for filter in self.configuration.get_value('test plan', 'filter'):383 for filter in self.launcher.test_plan_filters:
385 filtered_tp_ids.update(fnmatch.filter(test_plan_ids, filter))384 filtered_tp_ids.update(fnmatch.filter(test_plan_ids, filter))
386 tp_info_list = self._generate_tp_infos(filtered_tp_ids)385 tp_info_list = self._generate_tp_infos(filtered_tp_ids)
387 if not tp_info_list:386 if not tp_info_list:
@@ -389,20 +388,19 @@ class Launcher(MainLoopStage, ReportsStage):
389 return388 return
390 selected_tp = TestPlanBrowser(389 selected_tp = TestPlanBrowser(
391 _("Select test plan"), tp_info_list,390 _("Select test plan"), tp_info_list,
392 self.configuration.get_value('test plan', 'unit')).run()391 self.launcher.test_plan_default_selection).run()
393 return selected_tp392 return selected_tp
394393
395 def _strtobool(self, val):394 def _strtobool(self, val):
396 return val.lower() in ('y', 'yes', 't', 'true', 'on', '1')395 return val.lower() in ('y', 'yes', 't', 'true', 'on', '1')
397396
398 def _pick_jobs_to_run(self):397 def _pick_jobs_to_run(self):
399 if self.configuration.get_value('test selection', 'forced'):398 if self.launcher.test_selection_forced:
400 if self.configuration.manifest:399 if self.launcher.manifest is not Unset:
401 self.ctx.sa.save_manifest(400 self.ctx.sa.save_manifest(
402 {manifest_id:401 {manifest_id:
403 self._strtobool(402 self._strtobool(self.launcher.manifest[manifest_id]) for
404 self.configuration.manifest[manifest_id]) for403 manifest_id in self.launcher.manifest}
405 manifest_id in self.configuration.manifest}
406 )404 )
407 # by default all tests are selected; so we're done here405 # by default all tests are selected; so we're done here
408 return406 return
@@ -494,7 +492,7 @@ class Launcher(MainLoopStage, ReportsStage):
494 if not rerun_candidates:492 if not rerun_candidates:
495 return False493 return False
496 # we wait before retrying494 # we wait before retrying
497 delay = self.configuration.get_value('ui', 'delay_before_retry')495 delay = self.launcher.delay_before_retry
498 _logger.info(_("Waiting {} seconds before retrying failed"496 _logger.info(_("Waiting {} seconds before retrying failed"
499 " jobs...".format(delay)))497 " jobs...".format(delay)))
500 time.sleep(delay)498 time.sleep(delay)
@@ -527,11 +525,10 @@ class Launcher(MainLoopStage, ReportsStage):
527 def considering_job(self, job, job_state):525 def considering_job(self, job, job_state):
528 pass526 pass
529 show_out = True527 show_out = True
530 output = self.configuration.get_value('ui', 'output')528 if self.launcher.output == 'hide-resource-and-attachment':
531 if output == 'hide-resource-and-attachment':
532 if job.plugin in ('local', 'resource', 'attachment'):529 if job.plugin in ('local', 'resource', 'attachment'):
533 show_out = False530 show_out = False
534 elif output in ['hide', 'hide-automated']:531 elif self.launcher.output in ['hide', 'hide-automated']:
535 if job.plugin in ('shell', 'local', 'resource', 'attachment'):532 if job.plugin in ('shell', 'local', 'resource', 'attachment'):
536 show_out = False533 show_out = False
537 if 'suppress-output' in job.get_flag_set():534 if 'suppress-output' in job.get_flag_set():
@@ -751,7 +748,7 @@ class Run(MainLoopStage):
751 respawn_cmd = sys.argv[0] # entry-point to checkbox748 respawn_cmd = sys.argv[0] # entry-point to checkbox
752 respawn_cmd += ' --resume {}' # interpolate with session_id749 respawn_cmd += ' --resume {}' # interpolate with session_id
753 self.sa.configure_application_restart(750 self.sa.configure_application_restart(
754 lambda session_id: [respawn_cmd.format(session_id)], 'local')751 lambda session_id: [respawn_cmd.format(session_id)])
755752
756753
757class List():754class List():
diff --git a/plainbox/impl/applogic.py b/plainbox/impl/applogic.py
index 84f9906..1d90dfd 100644
--- a/plainbox/impl/applogic.py
+++ b/plainbox/impl/applogic.py
@@ -31,6 +31,7 @@ import os
31from plainbox.abc import IJobResult31from plainbox.abc import IJobResult
32from plainbox.i18n import gettext as _32from plainbox.i18n import gettext as _
33from plainbox.impl.result import MemoryJobResult33from plainbox.impl.result import MemoryJobResult
34from plainbox.impl.secure import config
34from plainbox.impl.secure.qualifiers import select_jobs35from plainbox.impl.secure.qualifiers import select_jobs
35from plainbox.impl.session import SessionManager36from plainbox.impl.session import SessionManager
36from plainbox.impl.session.jobs import InhibitionCause37from plainbox.impl.session.jobs import InhibitionCause
@@ -79,6 +80,23 @@ def run_job_if_possible(session, runner, config, job, update=True, ui=None):
79 return job_state, job_result80 return job_state, job_result
8081
8182
83class PlainBoxConfig(config.Config):
84 """
85 Configuration for PlainBox itself
86 """
87
88 environment = config.Section(
89 help_text=_("Environment variables for scripts and jobs"))
90
91 class Meta:
92
93 # TODO: properly depend on xdg and use real code that also handles
94 # XDG_CONFIG_HOME.
95 filename_list = [
96 '/etc/xdg/plainbox.conf',
97 os.path.expanduser('~/.config/plainbox.conf')]
98
99
82def get_all_exporter_names():100def get_all_exporter_names():
83 """101 """
84 Get the identifiers (names) of all the supported session state exporters.102 Get the identifiers (names) of all the supported session state exporters.
diff --git a/plainbox/impl/commands/__init__.py b/plainbox/impl/commands/__init__.py
index efdb1f6..5a26853 100644
--- a/plainbox/impl/commands/__init__.py
+++ b/plainbox/impl/commands/__init__.py
@@ -47,6 +47,7 @@ class PlainBoxToolBase(ToolBase):
47 1. :meth:`get_exec_name()` -- to know how the command will be called47 1. :meth:`get_exec_name()` -- to know how the command will be called
48 2. :meth:`get_exec_version()` -- to know how the version of the tool48 2. :meth:`get_exec_version()` -- to know how the version of the tool
49 3. :meth:`add_subcommands()` -- to add some actual commands to execute49 3. :meth:`add_subcommands()` -- to add some actual commands to execute
50 4. :meth:`get_config_cls()` -- to know which config to use
5051
51 This class has some complex control flow to support important and52 This class has some complex control flow to support important and
52 interesting use cases. There are some concerns to people that subclass this53 interesting use cases. There are some concerns to people that subclass this
@@ -68,6 +69,16 @@ class PlainBoxToolBase(ToolBase):
68 known yet.69 known yet.
69 """70 """
7071
72 @classmethod
73 @abc.abstractmethod
74 def get_config_cls(cls):
75 """
76 Get the Config class that is used by this implementation.
77
78 This can be overridden by subclasses to use a different config class
79 that is suitable for the particular application.
80 """
81
71 def _load_config(self):82 def _load_config(self):
72 return self.get_config_cls().get()83 return self.get_config_cls().get()
7384
diff --git a/plainbox/impl/config.py b/plainbox/impl/config.py
74deleted file mode 10064485deleted file mode 100644
index 044862d..0000000
--- a/plainbox/impl/config.py
+++ /dev/null
@@ -1,398 +0,0 @@
1# This file is part of Checkbox.
2#
3# Copyright 2021-2022 Canonical Ltd.
4# Written by:
5# Maciej Kisielewski <maciej.kisielewski@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 3,
9# as published by the Free Software Foundation.
10#
11# Checkbox is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
18"""
19This module defines class for handling Checkbox configs.
20
21If we ever need to add validators to config variables, the addition should be
22done in VarSpec (the fourth 'field').
23"""
24import copy
25import io
26import logging
27import os
28import shlex
29
30from configparser import ConfigParser
31from collections import namedtuple, OrderedDict
32
33logger = logging.getLogger(__name__)
34
35
36class Configuration:
37 """
38 Checkbox configuration storing objects.
39
40 Checkbox configs store various information on how to run the Checkbox.
41 For instance what reports to generate, should the session be interactive,
42 and many others. Look at CONFIG_SPEC for details.
43 """
44 def __init__(self, source=None):
45 """Create a new configuration object filled with default values."""
46 self.sections = OrderedDict()
47 self._origins = dict()
48 self._problems = []
49 # sources is similar to origins, but instead of keeping an info on
50 # each variable, we note what configs got read in general
51 self._sources = [source] if source else []
52 for section, contents in CONFIG_SPEC:
53 if isinstance(contents, ParametricSection):
54 # we don't know what the actual section name will be,
55 # so let's wait with the creation until we know the full name
56 continue
57 if isinstance(contents, DynamicSection):
58 self.sections[section] = DynamicSection()
59 else:
60 self.sections[section] = OrderedDict()
61 self._origins[section] = dict()
62 for name, spec in sorted(contents.items()):
63 self.sections[section][name] = spec.default
64 self._origins[section][name] = ''
65
66 @property
67 def environment(self):
68 """Return contents of the environment section."""
69 return self.sections['environment']
70
71 @property
72 def manifest(self):
73 """Return contents of the manifest section."""
74 return self.sections['manifest']
75
76 @property
77 def sources(self):
78 """Return list of sources for this configuration."""
79 return self._sources
80
81 def get_strategy_kwargs(self):
82 """Return custom restart strategy parameters."""
83 kwargs = copy.deepcopy(self.sections['restart'])
84 # [restart] section has the kwargs for the strategy initializer
85 # and the 'strategy' which is not one, let's pop it
86 kwargs.pop('strategy')
87 return kwargs
88
89 def notice_problem(self, problem):
90 """ Record and log problem encountered when building configuration."""
91 self._problems.append(problem)
92 logger.warning(problem)
93
94 def get_problems(self):
95 """Return a list of problem as strings."""
96 return self._problems
97
98 def get_value(self, section, name):
99 """Return a value of given `name` from given `section`,"""
100 return self.sections[section][name]
101
102 def get_origin(self, section, name):
103 """Return origin of the value."""
104 return self._origins[section][name]
105
106 def update_from_another(self, configuration, origin):
107 """
108 Update this configuration with values from `configuration`.
109
110 Only the values that are not defaults from 'configuration` are taken
111 into account.
112 """
113 for section, variables in configuration.sections.items():
114 for name in variables.keys():
115 new_origin = configuration.get_origin(section, name)
116 if new_origin:
117 if ':' in section and section not in self.sections.keys():
118 self.sections[section] = OrderedDict()
119 self._origins[section] = dict()
120 self.sections[section][name] = configuration.get_value(
121 section, name)
122 self._origins[section][name] = origin or new_origin
123 self._sources += configuration.sources
124 self._problems += configuration.get_problems()
125
126 def dyn_set_value(self, section, name, value, origin):
127 """Set a value of a var from a dynamic section."""
128 if section == 'environment':
129 name = name.upper()
130 self.sections[section][name] = value
131 self._origins[section][name] = origin
132
133 def set_value(self, section, name, value, origin, parser):
134 """Set a new value for variable and update its origin."""
135 # we are kind off guaranteed that section will be found in the spec
136 # but let's make linters happy
137 if section in self._DYNAMIC_SECTIONS:
138 self.dyn_set_value(section, name, value, origin)
139 return
140 parametrized = False
141 if ':' in section:
142 parametrized = True
143 prefix, _ = section.split(':')
144 if parametrized:
145 # TODO: do the check here for typing
146 pass
147
148 index = -1
149 for i, (sect_name, spec) in enumerate(CONFIG_SPEC):
150 if sect_name == section:
151 index = i
152 if isinstance(spec, ParametricSection):
153 if parametrized and sect_name == prefix:
154 if name not in spec:
155 problem = (
156 "Unexpected variable '{}' in section [{}] "
157 "Origin: {}").format(name, section, origin)
158 self.notice_problem(problem)
159 return
160 index = i
161 if index == -1:
162 # this should happen only for parametric sections
163 problem = "Unexpected section [{}]. Origin: {}".format(
164 section, origin)
165 self.notice_problem(problem)
166 return
167
168 assert index > -1
169 kind = CONFIG_SPEC[index][1][name].kind
170 try:
171 if kind == list:
172 value = shlex.split(value.replace(',', ' '))
173 elif kind == bool:
174 value = parser.getboolean(section, name)
175 elif kind == float:
176 value = parser.getfloat(section, name)
177 elif kind == int:
178 value = parser.getint(section, name)
179 else:
180 value = kind(value)
181 if parametrized:
182 # we couldn't have known the param names eariler (in __init__)
183 # but now we do know them, so let's create the dict to hold
184 # the values
185 if section not in self.sections.keys():
186 self.sections[section] = OrderedDict()
187 self._origins[section] = dict()
188 self.sections[section][name] = value
189 self._origins[section][name] = origin
190 except TypeError:
191 problem = (
192 "Problem with setting field {} in section [{}] "
193 "'{}' cannot be used as {}. Origin: {}").format(
194 name, section, value, kind, origin)
195 self.notice_problem(problem)
196
197 def get_parametric_sections(self, prefix):
198 """
199 Return a dict of parametrised section that share the same prefix.
200
201 The resulting dict is keyed by the parameter, the values are dicts
202 with the declared variables.
203
204 E.g.
205 If there's two sections: [report:myrep] and [report:other]
206 The resulting dict will have two keys: myrep and other.
207 """
208 result = dict()
209 # check if there is such section declared in the SPEC
210 for sect_name, section in CONFIG_SPEC:
211 if not isinstance(section, ParametricSection):
212 continue
213 if sect_name == prefix:
214 break
215 else:
216 raise ValueError("No such section in the spec ({}".format(prefix))
217 for sect_name, section in self.sections.items():
218 sect_prefix, _, sect_param = sect_name.partition(':')
219 if sect_prefix == prefix:
220 result[sect_param] = section
221 return result
222
223 @classmethod
224 def from_text(cls, text, origin):
225 """
226 Create a new configuration with values from the text.
227
228 Behaves just the same as the from_ini_file method, but accepts string
229 as the param.
230 """
231 return cls.from_ini_file(io.StringIO(text), origin)
232
233 @classmethod
234 def from_path(cls, path):
235 """Create a new configuration with values stored in a file at path."""
236 cfg = Configuration()
237 if not os.path.isfile(path):
238 cfg.notice_problem("{} file not found".format(path))
239 return cfg
240 with open(path, 'rt') as ini_file:
241 return cls.from_ini_file(ini_file, path)
242
243 @classmethod
244 def from_ini_file(cls, ini_file, origin):
245 """
246 Create a new configuration with values from the ini file.
247
248 ini_file should be a file object.
249
250 This function is designed not to fail (raise), so if some entry in the
251 ini file is misdefined then it should be ignored and the default value
252 should be kept. Each such problem is kept in the self._problems list.
253 """
254 cfg = Configuration(origin)
255 parser = ConfigParser(delimiters='=')
256 parser.read_string(ini_file.read())
257 for sect_name, section in parser.items():
258 if sect_name == 'DEFAULT':
259 for var_name in section:
260 problem = "[DEFAULT] section is not supported"
261 cfg.notice_problem(problem)
262 continue
263 if ':' in sect_name:
264 for var_name, var in section.items():
265 cfg.set_value(sect_name, var_name, var, origin, parser)
266 continue
267 if sect_name not in cfg.sections:
268 problem = "Unexpected section [{}]. Origin: {}".format(
269 sect_name, origin)
270 cfg.notice_problem(problem)
271 continue
272 for var_name, var in section.items():
273 is_dyn = sect_name in cls._DYNAMIC_SECTIONS
274 if var_name not in cfg.sections[sect_name] and not is_dyn:
275 problem = (
276 "Unexpected variable '{}' in section [{}] "
277 "Origin: {}").format(var_name, sect_name, origin)
278 cfg.notice_problem(problem)
279 continue
280 cfg.set_value(sect_name, var_name, var, origin, parser)
281 return cfg
282
283 _DYNAMIC_SECTIONS = ('environment', 'manifest')
284
285
286VarSpec = namedtuple('VarSpec', ['kind', 'default', 'help'])
287
288
289class ParametricSection(dict):
290 """ Dict for storing parametric section's contents."""
291
292
293class DynamicSection(dict):
294 """
295 Dict for storing dynamic section's contents.
296
297 This is an extra type to record the fact that this is a different section
298 compared to the predefined ones. It works and isn't very complex, but
299 a different way of storing this information might be more elegant.
300 """
301
302
303# in order to maintain the section order the CONFIG_SPEC is a list of pairs,
304# where the first value is the name of the section and the other is a dict
305# of variable specs.
306CONFIG_SPEC = [
307 ('config', {
308 'config_filename': VarSpec(
309 str, 'checkbox.conf',
310 'Name of the configuration file to look for.'),
311 }),
312 ('launcher', {
313 'launcher_version': VarSpec(
314 int, 1, "Version of launcher to use"),
315 'app_id': VarSpec(
316 str, 'checkbox-cli', "Identifier of the application"),
317 'app_version': VarSpec(
318 str, '', "Version of the application"),
319 'stock_reports': VarSpec(
320 list, ['text', 'certification', 'submission_files'],
321 "List of stock reports to use"),
322 'local_submission': VarSpec(
323 bool, True, ("Send/generate submission report locally when using "
324 "checkbox remote")),
325 'session_title': VarSpec(
326 str, 'session title',
327 ("A title to be applied to the sessions created using this "
328 "launcher that can be used in report generation")),
329 'session_desc': VarSpec(
330 str, '', ("A string that can be applied to sessions created using "
331 "this launcher. Useful for storing some contextual "
332 "infomation about the session")),
333 }),
334 ('test plan', {
335 'filter': VarSpec(
336 list, ['*'],
337 "Constrain interactive choice to test plans matching this glob"),
338 'unit': VarSpec(str, '', "Select this test plan by default."),
339 'forced': VarSpec(
340 bool, False, "Don't allow the user to change test plan."),
341 }),
342 ('test selection', {
343 'forced': VarSpec(
344 bool, False, "Don't allow the user to alter test selection."),
345 'exclude': VarSpec(
346 list, [], "Exclude test matching patterns from running."),
347 }),
348 ('ui', {
349 'type': VarSpec(str, 'interactive', "Type of user interface to use."),
350 'output': VarSpec(str, 'show', "Silence or restrict command output."),
351 'dont_suppress_output': VarSpec(
352 bool, False,
353 "Don't suppress the output of certain job plugin types."),
354 'verbosity': VarSpec(str, 'normal', "Verbosity level."),
355 'auto_retry': VarSpec(
356 bool, False,
357 "Automatically retry failed jobs at the end of the session."),
358 'max_attempts': VarSpec(
359 int, 3,
360 "Number of attempts to run a job when in auto-retry mode."),
361 'delay_before_retry': VarSpec(
362 int, 1, ("Delay (in seconds) before "
363 "retrying failed jobs in auto-retry mode.")),
364 }),
365 ('daemon', {
366 'normal_user': VarSpec(
367 str, '', "Username to use for jobs that don't specify user."),
368 }),
369 ('restart', {
370 'strategy': VarSpec(str, '', "Use alternative restart strategy."),
371 }),
372 ('report', ParametricSection({
373 'exporter': VarSpec(
374 str, '', "Name of the exporter to use"),
375 'transport': VarSpec(
376 str, '', "Name of the transport to use"),
377 'forced': VarSpec(
378 bool, False, "Don't ask the user if they want the report."),
379 })),
380 ('transport', ParametricSection({
381 'type': VarSpec(
382 str, '', "Type of transport to use."),
383 'stream': VarSpec(
384 str, 'stdout', "Stream to use - stdout or stderr."),
385 'path': VarSpec(
386 str, '', "Path to where the report should be saved to."),
387 'secure_id': VarSpec(
388 str, '', "Secure ID to use."),
389 'staging': VarSpec(
390 bool, False, "Pushes to staging C3 instead of normal C3."),
391 })),
392 ('exporter', ParametricSection({
393 'unit': VarSpec(str, '', "ID of the exporter to use."),
394 'options': VarSpec(list, [], "Flags to forward to the exporter."),
395 })),
396 ('environment', DynamicSection()),
397 ('manifest', DynamicSection()),
398]
diff --git a/plainbox/impl/ctrl.py b/plainbox/impl/ctrl.py
index 2242114..5facd64 100644
--- a/plainbox/impl/ctrl.py
+++ b/plainbox/impl/ctrl.py
@@ -60,6 +60,7 @@ from plainbox.impl.resource import ExpressionCannotEvaluateError
60from plainbox.impl.resource import ExpressionFailedError60from plainbox.impl.resource import ExpressionFailedError
61from plainbox.impl.resource import ResourceProgramError61from plainbox.impl.resource import ResourceProgramError
62from plainbox.impl.resource import Resource62from plainbox.impl.resource import Resource
63from plainbox.impl.secure.config import Unset
63from plainbox.impl.secure.origin import JobOutputTextSource64from plainbox.impl.secure.origin import JobOutputTextSource
64from plainbox.impl.secure.providers.v1 import Provider165from plainbox.impl.secure.providers.v1 import Provider1
65from plainbox.impl.secure.rfc822 import RFC822SyntaxError66from plainbox.impl.secure.rfc822 import RFC822SyntaxError
diff --git a/plainbox/impl/launcher.py b/plainbox/impl/launcher.py
66new file mode 10064467new file mode 100644
index 0000000..16b3059
--- /dev/null
+++ b/plainbox/impl/launcher.py
@@ -0,0 +1,258 @@
1# This file is part of Checkbox.
2#
3# Copyright 2014-2016 Canonical Ltd.
4# Written by:
5# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
6# Maciej Kisielewski <maciej.kisielewski@canonical.com>
7#
8# Checkbox is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 3,
10# as published by the Free Software Foundation.
11#
12# Checkbox is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
19
20"""
21:mod:`plainbox.impl.launcher` -- launcher definition
22==================================================
23"""
24
25from gettext import gettext as _
26import logging
27
28from plainbox.impl.applogic import PlainBoxConfig
29from plainbox.impl.secure import config
30from plainbox.impl.session.assistant import SA_RESTARTABLE
31from plainbox.impl.session.assistant import get_all_sa_flags
32from plainbox.impl.session.assistant import get_known_sa_api_versions
33from plainbox.impl.transport import get_all_transports
34from plainbox.impl.transport import SECURE_ID_PATTERN
35
36
37logger = logging.getLogger("plainbox.launcher")
38
39
40class LauncherDefinition(PlainBoxConfig):
41 """
42 Launcher definition.
43
44 Launchers are small executables using one of the available user interfaces
45 as the interpreter. This class contains all the available options that can
46 be set inside the launcher, that will affect the user interface at runtime.
47 This generic launcher definition class helps to pick concrete version of
48 the launcher definition.
49 """
50 launcher_version = config.Variable(
51 section="launcher",
52 help_text=_("Version of launcher to use"))
53
54 config_filename = config.Variable(
55 section="config",
56 default="checkbox.conf",
57 help_text=_("Name of custom configuration file"))
58
59 def get_concrete_launcher(self):
60 """Create appropriate LauncherDefinition instance.
61
62 Depending on the value of launcher_version variable appropriate
63 LauncherDefinition class is chosen and its instance returned.
64
65 :returns: LauncherDefinition instance
66 :raises KeyError: for unknown launcher_version values
67 """
68 return {'1': LauncherDefinition1}[self.launcher_version]()
69
70
71class LauncherDefinition1(LauncherDefinition):
72 """
73 Definition for launchers version 1.
74
75 As specced in https://goo.gl/qJYtPX
76 """
77
78 def __init__(self):
79 super().__init__()
80
81 launcher_version = config.Variable(
82 section="launcher",
83 default='1',
84 help_text=_("Version of launcher to use"))
85
86 app_id = config.Variable(
87 section='launcher',
88 default='checkbox-cli',
89 help_text=_('Identifier of the application'))
90
91 app_version = config.Variable(
92 section='launcher',
93 help_text=_('Version of the application'))
94
95 api_flags = config.Variable(
96 section='launcher',
97 kind=list,
98 default=[SA_RESTARTABLE],
99 validator_list=[config.SubsetValidator(get_all_sa_flags())],
100 help_text=_('List of feature-flags the application requires'))
101
102 api_version = config.Variable(
103 section='launcher',
104 default='0.99',
105 validator_list=[config.ChoiceValidator(
106 get_known_sa_api_versions())],
107 help_text=_('Version of API the launcher uses'))
108
109 stock_reports = config.Variable(
110 section='launcher',
111 kind=list,
112 validator_list=[
113 config.SubsetValidator({
114 'text', 'certification', 'certification-staging',
115 'submission_files', 'none'}),
116 config.OneOrTheOtherValidator(
117 {'none'}, {'text', 'certification', 'certification-staging',
118 'submission_files'}),
119 ],
120 default=['text', 'certification', 'submission_files'],
121 help_text=_('List of stock reports to use'))
122
123 local_submission = config.Variable(
124 section='launcher',
125 kind=bool,
126 default=True,
127 help_text=_("Send/generate submission report locally when using "
128 "checkbox remote"))
129
130 session_title = config.Variable(
131 section='launcher',
132 default='session title',
133 help_text=_("A title to be applied to the sessions created using "
134 "this launcher that can be used in report generation"))
135
136 session_desc = config.Variable(
137 section='launcher',
138 default='',
139 help_text=_("A string that can be applied to sessions created using "
140 "this launcher. Useful for storing some contextual "
141 "infomation about the session"))
142
143 test_plan_filters = config.Variable(
144 section='test plan',
145 name='filter',
146 default=['*'],
147 kind=list,
148 help_text=_('Constrain interactive choice to test plans matching this'
149 'glob'))
150
151 test_plan_default_selection = config.Variable(
152 section='test plan',
153 name='unit',
154 help_text=_('Select this test plan by default.'))
155
156 test_plan_forced = config.Variable(
157 section='test plan',
158 name='forced',
159 kind=bool,
160 default=False,
161 help_text=_("Don't allow the user to change test plan."))
162
163 test_selection_forced = config.Variable(
164 section='test selection',
165 name='forced',
166 kind=bool,
167 default=False,
168 help_text=_("Don't allow the user to alter test selection."))
169
170 test_exclude = config.Variable(
171 section='test selection',
172 name='exclude',
173 default=[],
174 kind=list,
175 help_text=_("Exclude test matching the patterns from running"))
176
177 ui_type = config.Variable(
178 section='ui',
179 name='type',
180 default='interactive',
181 validator_list=[config.ChoiceValidator(
182 ['interactive', 'silent'])],
183 help_text=_('Type of stock user interface to use.'))
184
185 output = config.Variable(
186 section='ui',
187 default='show',
188 validator_list=[config.ChoiceValidator(
189 ['show', 'hide', 'hide-resource-and-attachment',
190 'hide-automated'])],
191 help_text=_('Silence or restrict command output'))
192
193 dont_suppress_output = config.Variable(
194 section="ui", kind=bool, default=False,
195 help_text=_("Don't suppress the output of certain job plugin types."))
196
197 verbosity = config.Variable(
198 section="ui", validator_list=[config.ChoiceValidator(
199 ['normal', 'verbose', 'debug'])], help_text=_('Verbosity level'),
200 default='normal')
201
202 auto_retry = config.Variable(
203 section='ui',
204 kind=bool,
205 default=False,
206 help_text=_("Automatically retry failed jobs at the end"
207 " of the session."))
208
209 max_attempts = config.Variable(
210 section='ui',
211 kind=int,
212 default=3,
213 help_text=_("Number of attempts to run a job when in auto-retry mode."))
214
215 delay_before_retry = config.Variable(
216 section='ui',
217 kind=int,
218 default=1,
219 help_text=_("Delay (in seconds) before retrying failed jobs in"
220 " auto-retry mode."))
221
222 normal_user = config.Variable(
223 section='daemon',
224 kind=str,
225 default='',
226 help_text=_("Username to use for jobs that don't specify user"))
227
228 restart_strategy = config.Variable(
229 section='restart',
230 name='strategy',
231 help_text=_('Use alternative restart strategy'))
232
233 restart = config.Section(
234 help_text=_('Restart strategy parameters'))
235
236 reports = config.ParametricSection(
237 name='report',
238 help_text=_('Report declaration'))
239
240 exporters = config.ParametricSection(
241 name='exporter',
242 help_text=_('Exporter declaration'))
243
244 transports = config.ParametricSection(
245 name='transport',
246 help_text=_('Transport declaration'))
247
248 environment = config.Section(
249 help_text=_('Environment variables to use'))
250
251 daemon = config.Section(
252 name='daemon',
253 help_text=_('Daemon-specific configuration'))
254
255 manifest = config.Section(
256 help_text=_('Manifest entries to use'))
257
258DefaultLauncherDefinition = LauncherDefinition1
diff --git a/plainbox/impl/runner.py b/plainbox/impl/runner.py
index fae15fb..f46a99f 100644
--- a/plainbox/impl/runner.py
+++ b/plainbox/impl/runner.py
@@ -50,6 +50,7 @@ from plainbox.i18n import gettext as _
50from plainbox.impl.result import IOLogRecord50from plainbox.impl.result import IOLogRecord
51from plainbox.impl.result import IOLogRecordWriter51from plainbox.impl.result import IOLogRecordWriter
52from plainbox.impl.result import JobResultBuilder52from plainbox.impl.result import JobResultBuilder
53from plainbox.impl.secure.config import Unset
53from plainbox.vendor import extcmd54from plainbox.vendor import extcmd
54from plainbox.vendor import morris55from plainbox.vendor import morris
5556
diff --git a/plainbox/impl/secure/test_config.py b/plainbox/impl/secure/test_config.py
56new file mode 10064457new file mode 100644
index 0000000..1efab12
--- /dev/null
+++ b/plainbox/impl/secure/test_config.py
@@ -0,0 +1,608 @@
1# This file is part of Checkbox.
2#
3# Copyright 2013, 2014 Canonical Ltd.
4# Written by:
5# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 3,
9# as published by the Free Software Foundation.
10
11#
12# Checkbox is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
19
20"""
21plainbox.impl.secure.test_config
22================================
23
24Test definitions for plainbox.impl.secure.config module
25"""
26from io import StringIO
27from unittest import TestCase
28import configparser
29
30from plainbox.impl.secure.config import ChoiceValidator
31from plainbox.impl.secure.config import ConfigMetaData
32from plainbox.impl.secure.config import KindValidator
33from plainbox.impl.secure.config import NotEmptyValidator
34from plainbox.impl.secure.config import NotUnsetValidator
35from plainbox.impl.secure.config import OneOrTheOtherValidator
36from plainbox.impl.secure.config import PatternValidator
37from plainbox.impl.secure.config import ParametricSection
38from plainbox.impl.secure.config import PlainBoxConfigParser, Config
39from plainbox.impl.secure.config import ValidationError
40from plainbox.impl.secure.config import Variable, Section, Unset
41from plainbox.impl.secure.config import understands_Unset
42from plainbox.vendor import mock
43
44
45class UnsetTests(TestCase):
46
47 def test_str(self):
48 self.assertEqual(str(Unset), "unset")
49
50 def test_repr(self):
51 self.assertEqual(repr(Unset), "Unset")
52
53 def test_bool(self):
54 self.assertEqual(bool(Unset), False)
55
56
57class understands_Unset_Tests(TestCase):
58
59 def test_func(self):
60 @understands_Unset
61 def func():
62 pass
63
64 self.assertTrue(hasattr(func, 'understands_Unset'))
65 self.assertTrue(getattr(func, 'understands_Unset'))
66
67 def test_cls(self):
68 @understands_Unset
69 class cls:
70 pass
71
72 self.assertTrue(hasattr(cls, 'understands_Unset'))
73 self.assertTrue(getattr(cls, 'understands_Unset'))
74
75
76class VariableTests(TestCase):
77
78 def test_name(self):
79 v1 = Variable()
80 self.assertIsNone(v1.name)
81 v2 = Variable('var')
82 self.assertEqual(v2.name, 'var')
83 v3 = Variable(name='var')
84 self.assertEqual(v3.name, 'var')
85
86 def test_section(self):
87 v1 = Variable()
88 self.assertEqual(v1.section, 'DEFAULT')
89 v2 = Variable(section='foo')
90 self.assertEqual(v2.section, 'foo')
91
92 def test_kind(self):
93 v1 = Variable(kind=bool)
94 self.assertIs(v1.kind, bool)
95 v2 = Variable(kind=int)
96 self.assertIs(v2.kind, int)
97 v3 = Variable(kind=float)
98 self.assertIs(v3.kind, float)
99 v4 = Variable(kind=str)
100 self.assertIs(v4.kind, str)
101 v5 = Variable()
102 self.assertIs(v5.kind, str)
103 v6 = Variable(kind=list)
104 self.assertIs(v6.kind, list)
105 with self.assertRaises(ValueError):
106 Variable(kind=dict)
107
108 def test_validator_list__default(self):
109 """
110 verify that each Variable has a validator_list and that by default,
111 that list contains a KindValidator as the first element
112 """
113 self.assertEqual(Variable().validator_list, [KindValidator])
114
115 def test_validator_list__explicit(self):
116 """
117 verify that each Variable has a validator_list and that, if
118 customized, the list contains the custom validators, preceded by
119 the implicit KindValidator object
120 """
121 def DummyValidator(variable, new_value):
122 """ Dummy validator for the test below"""
123 pass
124 var = Variable(validator_list=[DummyValidator])
125 self.assertEqual(var.validator_list, [KindValidator, DummyValidator])
126
127 def test_validator_list__with_NotUnsetValidator(self):
128 """
129 verify that each Variable has a validator_list and that, if
130 customized, and if using NotUnsetValidator it will take precedence
131 over all other validators, including the implicit KindValidator
132 """
133 var = Variable(validator_list=[NotUnsetValidator()])
134 self.assertEqual(
135 var.validator_list, [NotUnsetValidator(), KindValidator])
136
137
138class SectionTests(TestCase):
139
140 def test_name(self):
141 s1 = Section()
142 self.assertIsNone(s1.name)
143 s2 = Section('sec')
144 self.assertEqual(s2.name, 'sec')
145 s3 = Variable(name='sec')
146 self.assertEqual(s3.name, 'sec')
147
148
149class ConfigTests(TestCase):
150
151 def test_Meta_present(self):
152 class TestConfig(Config):
153 pass
154 self.assertTrue(hasattr(TestConfig, 'Meta'))
155
156 def test_Meta_base_cls(self):
157 class TestConfig(Config):
158 pass
159 self.assertTrue(issubclass(TestConfig.Meta, ConfigMetaData))
160
161 class HelperMeta:
162 pass
163
164 class TestConfigWMeta(Config):
165 Meta = HelperMeta
166 self.assertTrue(issubclass(TestConfigWMeta.Meta, ConfigMetaData))
167 self.assertTrue(issubclass(TestConfigWMeta.Meta, HelperMeta))
168
169 def test_Meta_variable_list(self):
170 class TestConfig(Config):
171 v1 = Variable()
172 v2 = Variable()
173 self.assertEqual(
174 TestConfig.Meta.variable_list,
175 [TestConfig.v1, TestConfig.v2])
176
177 def test_variable_smoke(self):
178 class TestConfig(Config):
179 v = Variable()
180 conf = TestConfig()
181 self.assertIs(conf.v, Unset)
182 conf.v = "value"
183 self.assertEqual(conf.v, "value")
184 del conf.v
185 self.assertIs(conf.v, Unset)
186
187 def _get_featureful_config(self):
188 # define a featureful config class
189 class TestConfig(Config):
190 v1 = Variable()
191 v2 = Variable(section="v23_section")
192 v3 = Variable(section="v23_section")
193 v_unset = Variable()
194 v_bool = Variable(section="type_section", kind=bool)
195 v_int = Variable(section="type_section", kind=int)
196 v_float = Variable(section="type_section", kind=float)
197 v_list = Variable(section="type_section", kind=list)
198 v_str = Variable(section="type_section", kind=str)
199 s = Section()
200 ps = ParametricSection()
201 conf = TestConfig()
202 # assign value to each variable, except v3_unset
203 conf.v1 = "v1 value"
204 conf.v2 = "v2 value"
205 conf.v3 = "v3 value"
206 conf.v_bool = True
207 conf.v_int = -7
208 conf.v_float = 1.5
209 conf.v_str = "hi"
210 conf.v_list = ['foo', 'bar']
211 # assign value to the section
212 conf.s = {"a": 1, "b": 2}
213 conf.ps = {"foo": {"c": 3, "d": 4}}
214 return conf
215
216 def test_get_parser_obj(self):
217 """
218 verify that Config.get_parser_obj() properly writes all the data to the
219 ConfigParser object.
220 """
221 conf = self._get_featureful_config()
222 parser = conf.get_parser_obj()
223 # verify that section and section-less variables work
224 self.assertEqual(parser.get("DEFAULT", "v1"), "v1 value")
225 self.assertEqual(parser.get("v23_section", "v2"), "v2 value")
226 self.assertEqual(parser.get("v23_section", "v3"), "v3 value")
227 # verify that unset variable is not getting set to anything
228 with self.assertRaises(configparser.Error):
229 parser.get("DEFAULT", "v_unset")
230 # verify that various types got converted correctly and still resolve
231 # to correct typed values
232 self.assertEqual(parser.get("type_section", "v_bool"), "True")
233 self.assertEqual(parser.getboolean("type_section", "v_bool"), True)
234 self.assertEqual(parser.get("type_section", "v_int"), "-7")
235 self.assertEqual(parser.getint("type_section", "v_int"), -7)
236 self.assertEqual(parser.get("type_section", "v_float"), "1.5")
237 self.assertEqual(parser.getfloat("type_section", "v_float"), 1.5)
238 self.assertEqual(parser.get("type_section", "v_str"), "hi")
239 # verify that section work okay
240 self.assertEqual(parser.get("s", "a"), "1")
241 self.assertEqual(parser.get("s", "b"), "2")
242 # verify that parametric section works okay
243 self.assertEqual(parser.get("ps:foo", "c"), "3")
244 self.assertEqual(parser.get("ps:foo", "d"), "4")
245
246 def test_write(self):
247 """
248 verify that Config.write() works
249 """
250 conf = self._get_featureful_config()
251 with StringIO() as stream:
252 conf.write(stream)
253 self.assertEqual(stream.getvalue(), (
254 "[DEFAULT]\n"
255 "v1 = v1 value\n"
256 "\n"
257 "[v23_section]\n"
258 "v2 = v2 value\n"
259 "v3 = v3 value\n"
260 "\n"
261 "[type_section]\n"
262 "v_bool = True\n"
263 "v_float = 1.5\n"
264 "v_int = -7\n"
265 "v_list = foo, bar\n"
266 "v_str = hi\n"
267 "\n"
268 "[s]\n"
269 "a = 1\n"
270 "b = 2\n"
271 "\n"
272 "[ps:foo]\n"
273 "c = 3\n"
274 "d = 4\n"
275 "\n"))
276
277 def test_section_smoke(self):
278 class TestConfig(Config):
279 s = Section()
280 conf = TestConfig()
281 self.assertIs(conf.s, Unset)
282 with self.assertRaises(TypeError):
283 conf.s['key'] = "key-value"
284 conf.s = {}
285 self.assertEqual(conf.s, {})
286 conf.s['key'] = "key-value"
287 self.assertEqual(conf.s['key'], "key-value")
288 del conf.s
289 self.assertIs(conf.s, Unset)
290
291 def test_read_string(self):
292 class TestConfig(Config):
293 v = Variable()
294 conf = TestConfig()
295 conf.read_string(
296 "[DEFAULT]\n"
297 "v = 1")
298 self.assertEqual(conf.v, "1")
299 self.assertEqual(len(conf.problem_list), 0)
300
301 def test_read_list_with_spaces(self):
302 class TestConfig(Config):
303 l = Variable(kind=list)
304 conf = TestConfig()
305 conf.read_string('[DEFAULT]\nl = foo bar')
306 self.assertEqual(conf.l, ['foo', 'bar'])
307 self.assertEqual(len(conf.problem_list), 0)
308
309 def test_read_list_with_commas(self):
310 class TestConfig(Config):
311 l = Variable(kind=list)
312 conf = TestConfig()
313 conf.read_string('[DEFAULT]\nl = foo,bar')
314 self.assertEqual(conf.l, ['foo', 'bar'])
315 self.assertEqual(len(conf.problem_list), 0)
316
317 def test_read_list_quoted_strings(self):
318 class TestConfig(Config):
319 l = Variable(kind=list)
320 conf = TestConfig()
321 conf.read_string('[DEFAULT]\nl = foo "bar baz"')
322 self.assertEqual(conf.l, ['foo', 'bar baz'])
323 self.assertEqual(len(conf.problem_list), 0)
324
325 def test_read_string_calls_validate_whole(self):
326 """
327 verify that Config.read_string() calls validate_whole()"
328 """
329 conf = Config()
330 with mock.patch.object(conf, 'validate_whole') as mocked_validate:
331 conf.read_string('')
332 mocked_validate.assert_called_once_with()
333
334 def test_read_calls_validate_whole(self):
335 """
336 verify that Config.read() calls validate_whole()"
337 """
338 conf = Config()
339 with mock.patch.object(conf, 'validate_whole') as mocked_validate:
340 conf.read([])
341 mocked_validate.assert_called_once_with()
342
343 def test_read__handles_errors_from_validate_whole(self):
344 """
345 verify that Config.read() collects errors from validate_whole()".
346 """
347 class TestConfig(Config):
348 v = Variable()
349
350 def validate_whole(self):
351 raise ValidationError(TestConfig.v, self.v, "v is evil")
352 conf = TestConfig()
353 conf.read([])
354 self.assertEqual(len(conf.problem_list), 1)
355 self.assertEqual(conf.problem_list[0].variable, TestConfig.v)
356 self.assertEqual(conf.problem_list[0].new_value, Unset)
357 self.assertEqual(conf.problem_list[0].message, "v is evil")
358
359 def test_read_string__does_not_ignore_nonmentioned_variables(self):
360 class TestConfig(Config):
361 v = Variable(validator_list=[NotUnsetValidator()])
362 conf = TestConfig()
363 conf.read_string("")
364 # Because Unset is the default, sadly
365 self.assertEqual(conf.v, Unset)
366 # But there was a problem noticed
367 self.assertEqual(len(conf.problem_list), 1)
368 self.assertEqual(conf.problem_list[0].variable, TestConfig.v)
369 self.assertEqual(conf.problem_list[0].new_value, Unset)
370 self.assertEqual(conf.problem_list[0].message,
371 "must be set to something")
372
373 def test_read_string__handles_errors_from_validate_whole(self):
374 """
375 verify that Config.read_strig() collects errors from validate_whole()".
376 """
377 class TestConfig(Config):
378 v = Variable()
379
380 def validate_whole(self):
381 raise ValidationError(TestConfig.v, self.v, "v is evil")
382 conf = TestConfig()
383 conf.read_string("")
384 self.assertEqual(len(conf.problem_list), 1)
385 self.assertEqual(conf.problem_list[0].variable, TestConfig.v)
386 self.assertEqual(conf.problem_list[0].new_value, Unset)
387 self.assertEqual(conf.problem_list[0].message, "v is evil")
388
389
390class ConfigMetaDataTests(TestCase):
391
392 def test_filename_list(self):
393 self.assertEqual(ConfigMetaData.filename_list, [])
394
395 def test_variable_list(self):
396 self.assertEqual(ConfigMetaData.variable_list, [])
397
398 def test_section_list(self):
399 self.assertEqual(ConfigMetaData.section_list, [])
400
401 def test_parametric_section_list(self):
402 self.assertEqual(ConfigMetaData.parametric_section_list, [])
403
404
405class PlainBoxConfigParserTest(TestCase):
406
407 def test_parser(self):
408 conf_file = StringIO("[testsection]\nlower = low\nUPPER = up")
409 config = PlainBoxConfigParser()
410 config.read_file(conf_file)
411
412 self.assertEqual(['testsection'], config.sections())
413 all_keys = list(config['testsection'].keys())
414 self.assertTrue('lower' in all_keys)
415 self.assertTrue('UPPER' in all_keys)
416 self.assertFalse('upper' in all_keys)
417
418 def test_parametric_sections_parsing(self):
419 class TestConfig(Config):
420 ps = ParametricSection()
421 conf_str = "[ps:foo]\nval = baz\n[ps:bar]\nvar = biz"
422 config = TestConfig()
423 config.read_string(conf_str)
424 self.assertEqual(
425 config.ps,
426 {'foo': {'val': 'baz'}, 'bar': {'var': 'biz'}})
427
428
429class KindValidatorTests(TestCase):
430
431 class _Config(Config):
432 var_bool = Variable(kind=bool)
433 var_int = Variable(kind=int)
434 var_float = Variable(kind=float)
435 var_str = Variable(kind=str)
436
437 def test_error_msg(self):
438 """
439 verify that KindValidator() has correct error message for each type
440 """
441 bad_value = object()
442 self.assertEqual(
443 KindValidator(self._Config.var_bool, bad_value),
444 "expected a boolean")
445 self.assertEqual(
446 KindValidator(self._Config.var_int, bad_value),
447 "expected an integer")
448 self.assertEqual(
449 KindValidator(self._Config.var_float, bad_value),
450 "expected a floating point number")
451 self.assertEqual(
452 KindValidator(self._Config.var_str, bad_value),
453 "expected a string")
454
455
456class PatternValidatorTests(TestCase):
457
458 class _Config(Config):
459 var = Variable()
460
461 def test_smoke(self):
462 """
463 verify that PatternValidator works as intended
464 """
465 validator = PatternValidator("foo.+")
466 self.assertEqual(validator(self._Config.var, "foobar"), None)
467 self.assertEqual(
468 validator(self._Config.var, "foo"),
469 "does not match pattern: 'foo.+'")
470
471 def test_comparison_works(self):
472 self.assertTrue(PatternValidator('foo') == PatternValidator('foo'))
473 self.assertTrue(PatternValidator('foo') != PatternValidator('bar'))
474 self.assertTrue(PatternValidator('foo') != object())
475
476
477class ChoiceValidatorTests(TestCase):
478
479 class _Config(Config):
480 var = Variable()
481
482 def test_smoke(self):
483 """
484 verify that ChoiceValidator works as intended
485 """
486 validator = ChoiceValidator(["foo", "bar"])
487 self.assertEqual(validator(self._Config.var, "foo"), None)
488 self.assertEqual(
489 validator(self._Config.var, "omg"),
490 "var must be one of foo, bar. Got 'omg'")
491
492 def test_comparison_works(self):
493 self.assertTrue(ChoiceValidator(["a"]) == ChoiceValidator(["a"]))
494 self.assertTrue(ChoiceValidator(["a"]) != ChoiceValidator(["b"]))
495 self.assertTrue(ChoiceValidator(["a"]) != object())
496
497
498class NotUnsetValidatorTests(TestCase):
499 """
500 Tests for the NotUnsetValidator class
501 """
502
503 class _Config(Config):
504 var = Variable()
505
506 def test_understands_Unset(self):
507 """
508 verify that Unset can be handled at all
509 """
510 self.assertTrue(getattr(NotUnsetValidator, "understands_Unset"))
511
512 def test_rejects_unset_values(self):
513 """
514 verify that Unset variables are rejected
515 """
516 validator = NotUnsetValidator()
517 self.assertEqual(
518 validator(self._Config.var, Unset), "must be set to something")
519
520 def test_accepts_other_values(self):
521 """
522 verify that other values are accepted
523 """
524 validator = NotUnsetValidator()
525 self.assertIsNone(validator(self._Config.var, None))
526 self.assertIsNone(validator(self._Config.var, "string"))
527 self.assertIsNone(validator(self._Config.var, 15))
528
529 def test_supports_custom_message(self):
530 """
531 verify that custom message is used
532 """
533 validator = NotUnsetValidator("value required!")
534 self.assertEqual(
535 validator(self._Config.var, Unset), "value required!")
536
537 def test_comparison_works(self):
538 """
539 verify that comparison works as expected
540 """
541 self.assertTrue(NotUnsetValidator() == NotUnsetValidator())
542 self.assertTrue(NotUnsetValidator("?") == NotUnsetValidator("?"))
543 self.assertTrue(NotUnsetValidator() != NotUnsetValidator("?"))
544 self.assertTrue(NotUnsetValidator() != object())
545
546
547class NotEmptyValidatorTests(TestCase):
548
549 class _Config(Config):
550 var = Variable()
551
552 def test_rejects_empty_values(self):
553 validator = NotEmptyValidator()
554 self.assertEqual(validator(self._Config.var, ""), "cannot be empty")
555
556 def test_supports_custom_message(self):
557 validator = NotEmptyValidator("name required!")
558 self.assertEqual(validator(self._Config.var, ""), "name required!")
559
560 def test_isnt_broken(self):
561 validator = NotEmptyValidator()
562 self.assertEqual(validator(self._Config.var, "some value"), None)
563
564 def test_comparison_works(self):
565 self.assertTrue(NotEmptyValidator() == NotEmptyValidator())
566 self.assertTrue(NotEmptyValidator("?") == NotEmptyValidator("?"))
567 self.assertTrue(NotEmptyValidator() != NotEmptyValidator("?"))
568 self.assertTrue(NotEmptyValidator() != object())
569
570
571class OneOrTheOtherValidatorTests(TestCase):
572
573 class _Config(Config):
574 var = Variable("The Name", kind=list)
575
576 def test_pass_validation(self):
577 validator = OneOrTheOtherValidator({'foo'}, {'bar'})
578 value = ['foo']
579 self.assertIsNone(validator(self._Config.var, value))
580 value = ['bar']
581 self.assertIsNone(validator(self._Config.var, value))
582
583 def test_fail_validation(self):
584 validator = OneOrTheOtherValidator({'foo'}, {'bar'})
585 value = ['foo', 'bar']
586 self.assertEquals(
587 validator(self._Config.var, value),
588 "The Name can only use values from {'foo'} or from {'bar'}")
589
590 def test_pass_empty(self):
591 validator = OneOrTheOtherValidator({'foo'}, {'bar'})
592 value = []
593 self.assertIsNone(validator(self._Config.var, value))
594
595 def test_comparison_works(self):
596 self.assertEqual(
597 OneOrTheOtherValidator({'foo'}, {'bar'}),
598 OneOrTheOtherValidator({'foo'}, {'bar'})
599 )
600 self.assertEqual(
601 OneOrTheOtherValidator({1, 2}, {3, 4}),
602 OneOrTheOtherValidator({2, 1}, {4, 3})
603 )
604 self.assertNotEqual(
605 OneOrTheOtherValidator({1}, {2}),
606 OneOrTheOtherValidator({1}, {'foo'})
607 )
608 self.assertNotEqual(OneOrTheOtherValidator({1}, {2}), object())
diff --git a/plainbox/impl/session/assistant.py b/plainbox/impl/session/assistant.py
index fa852c8..41ec411 100644
--- a/plainbox/impl/session/assistant.py
+++ b/plainbox/impl/session/assistant.py
@@ -38,15 +38,16 @@ from plainbox.abc import IJobResult
38from plainbox.abc import IJobRunnerUI38from plainbox.abc import IJobRunnerUI
39from plainbox.abc import ISessionStateTransport39from plainbox.abc import ISessionStateTransport
40from plainbox.i18n import gettext as _40from plainbox.i18n import gettext as _
41from plainbox.impl.applogic import PlainBoxConfig
41from plainbox.impl.decorators import raises42from plainbox.impl.decorators import raises
42from plainbox.impl.developer import UnexpectedMethodCall43from plainbox.impl.developer import UnexpectedMethodCall
43from plainbox.impl.developer import UsageExpectation44from plainbox.impl.developer import UsageExpectation
44from plainbox.impl.execution import UnifiedRunner45from plainbox.impl.execution import UnifiedRunner
45from plainbox.impl.config import Configuration
46from plainbox.impl.providers import get_providers46from plainbox.impl.providers import get_providers
47from plainbox.impl.result import JobResultBuilder47from plainbox.impl.result import JobResultBuilder
48from plainbox.impl.result import MemoryJobResult48from plainbox.impl.result import MemoryJobResult
49from plainbox.impl.runner import JobRunnerUIDelegate49from plainbox.impl.runner import JobRunnerUIDelegate
50from plainbox.impl.secure.config import Unset
50from plainbox.impl.secure.origin import Origin51from plainbox.impl.secure.origin import Origin
51from plainbox.impl.secure.qualifiers import select_jobs52from plainbox.impl.secure.qualifiers import select_jobs
52from plainbox.impl.secure.qualifiers import FieldQualifier53from plainbox.impl.secure.qualifiers import FieldQualifier
@@ -161,7 +162,7 @@ class SessionAssistant:
161 self._app_version = app_version162 self._app_version = app_version
162 self._api_version = api_version163 self._api_version = api_version
163 self._api_flags = api_flags164 self._api_flags = api_flags
164 self._config = Configuration()165 self._config = PlainBoxConfig().get()
165 Unit.config = self._config166 Unit.config = self._config
166 self._execution_ctrl_list = None # None is "default"167 self._execution_ctrl_list = None # None is "default"
167 self._ctrl_setup_list = []168 self._ctrl_setup_list = []
@@ -210,8 +211,7 @@ class SessionAssistant:
210211
211 @raises(UnexpectedMethodCall, LookupError)212 @raises(UnexpectedMethodCall, LookupError)
212 def configure_application_restart(213 def configure_application_restart(
213 self, cmd_callback, session_type:214 self, cmd_callback: 'Callable[[str], List[str]]') -> None:
214 'Callable[[str], List[str]], str') -> None:
215 """215 """
216 Configure automatic restart capability.216 Configure automatic restart capability.
217217
@@ -219,8 +219,6 @@ class SessionAssistant:
219 A callable (function or lambda) that when called with a single219 A callable (function or lambda) that when called with a single
220 string argument, session_id, returns a list of strings describing220 string argument, session_id, returns a list of strings describing
221 how to execute the tool in order to restart a particular session.221 how to execute the tool in order to restart a particular session.
222 :param session_type:
223 Kind of the session we're running. Either 'local' or 'remote'
224 :raises UnexpectedMethodCall:222 :raises UnexpectedMethodCall:
225 If the call is made at an unexpected time. Do not catch this error.223 If the call is made at an unexpected time. Do not catch this error.
226 It is a bug in your program. The error message will indicate what224 It is a bug in your program. The error message will indicate what
@@ -247,6 +245,25 @@ class SessionAssistant:
247 """245 """
248 UsageExpectation.of(self).enforce()246 UsageExpectation.of(self).enforce()
249 if self._restart_strategy is None:247 if self._restart_strategy is None:
248 # 'checkbox-slave' is deprecated, it's here so people can resume
249 # old session, the next if statement can be changed to just checking
250 # for 'remote' type
251 # session_type = 'remote' if self._metadata.title == 'remote'
252 # else 'local'
253 # with the next release or when we do inclusive naming refactor
254 # or roughly after April of 2022
255 # TODO: REMOTE API RAPI:
256 # this heuristic of guessing session type from the title
257 # should be changed to a proper arg/flag with the Remote API bump
258 remote_types = ('remote', 'checkbox-slave')
259 session_type = 'local'
260 try:
261 app_blob = json.loads(self._metadata.app_blob.decode("UTF-8"))
262 session_type = app_blob['type']
263 if session_type in remote_types:
264 session_type = 'remote'
265 except (AttributeError, ValueError, KeyError):
266 session_type = 'local'
250 self._restart_strategy = detect_restart_strategy(267 self._restart_strategy = detect_restart_strategy(
251 self, session_type=session_type)268 self, session_type=session_type)
252 self._restart_cmd_callback = cmd_callback269 self._restart_cmd_callback = cmd_callback
@@ -300,7 +317,8 @@ class SessionAssistant:
300 Use alternate configuration object.317 Use alternate configuration object.
301318
302 :param config:319 :param config:
303 A Checkbox configuration object.320 A configuration object that implements a superset of the plainbox
321 configuration.
304 :raises UnexpectedMethodCall:322 :raises UnexpectedMethodCall:
305 If the call is made at an unexpected time. Do not catch this error.323 If the call is made at an unexpected time. Do not catch this error.
306 It is a bug in your program. The error message will indicate what324 It is a bug in your program. The error message will indicate what
@@ -313,7 +331,7 @@ class SessionAssistant:
313 UsageExpectation.of(self).enforce()331 UsageExpectation.of(self).enforce()
314 self._config = config332 self._config = config
315 self._exclude_qualifiers = []333 self._exclude_qualifiers = []
316 for pattern in self._config.get_value('test selection', 'exclude'):334 for pattern in self._config.test_exclude:
317 self._exclude_qualifiers.append(335 self._exclude_qualifiers.append(
318 RegExpJobQualifier(pattern, None, False))336 RegExpJobQualifier(pattern, None, False))
319 Unit.config = config337 Unit.config = config
@@ -1201,7 +1219,7 @@ class SessionAssistant:
1201 if os.path.isfile(manifest):1219 if os.path.isfile(manifest):
1202 with open(manifest, 'rt', encoding='UTF-8') as stream:1220 with open(manifest, 'rt', encoding='UTF-8') as stream:
1203 manifest_cache = json.load(stream)1221 manifest_cache = json.load(stream)
1204 if self._config is not None and self._config.manifest:1222 if self._config is not None and self._config.manifest is not Unset:
1205 for manifest_id in self._config.manifest:1223 for manifest_id in self._config.manifest:
1206 manifest_cache.update(1224 manifest_cache.update(
1207 {manifest_id: self._config.manifest[manifest_id]})1225 {manifest_id: self._config.manifest[manifest_id]})
@@ -1364,8 +1382,11 @@ class SessionAssistant:
1364 f.writelines(self._restart_cmd_callback(1382 f.writelines(self._restart_cmd_callback(
1365 self.get_session_id()))1383 self.get_session_id()))
1366 if not native:1384 if not native:
1367 result = self._runner.run_job(job, job_state,1385 if self._config.environment is Unset:
1368 self._config.environment, ui)1386 result = self._runner.run_job(job, job_state, ui=ui)
1387 else:
1388 result = self._runner.run_job(job, job_state,
1389 self._config.environment, ui)
1369 builder = result.get_builder()1390 builder = result.get_builder()
1370 else:1391 else:
1371 builder = JobResultBuilder(1392 builder = JobResultBuilder(
diff --git a/plainbox/impl/session/remote_assistant.py b/plainbox/impl/session/remote_assistant.py
index e15961b..e3c6c11 100644
--- a/plainbox/impl/session/remote_assistant.py
+++ b/plainbox/impl/session/remote_assistant.py
@@ -28,7 +28,6 @@ from contextlib import suppress
28from tempfile import SpooledTemporaryFile28from tempfile import SpooledTemporaryFile
29from threading import Thread, Lock29from threading import Thread, Lock
3030
31from plainbox.impl.config import Configuration
32from plainbox.impl.execution import UnifiedRunner31from plainbox.impl.execution import UnifiedRunner
33from plainbox.impl.session.assistant import SessionAssistant32from plainbox.impl.session.assistant import SessionAssistant
34from plainbox.impl.session.assistant import SA_RESTARTABLE33from plainbox.impl.session.assistant import SA_RESTARTABLE
@@ -141,7 +140,7 @@ class BackgroundExecutor(Thread):
141class RemoteSessionAssistant():140class RemoteSessionAssistant():
142 """Remote execution enabling wrapper for the SessionAssistant"""141 """Remote execution enabling wrapper for the SessionAssistant"""
143142
144 REMOTE_API_VERSION = 12143 REMOTE_API_VERSION = 11
145144
146 def __init__(self, cmd_callback):145 def __init__(self, cmd_callback):
147 _logger.debug("__init__()")146 _logger.debug("__init__()")
@@ -274,20 +273,20 @@ class RemoteSessionAssistant():
274273
275 self._launcher = load_configs()274 self._launcher = load_configs()
276 if configuration['launcher']:275 if configuration['launcher']:
277 self._launcher = Configuration.from_text(276 self._launcher.read_string(configuration['launcher'], False)
278 configuration['launcher'], 'Remote launcher')277 if self._launcher.session_title:
279 session_title = self._launcher.get_value(278 session_title = self._launcher.session_title
280 'launcher', 'session_title') or session_title279 if self._launcher.session_desc:
281 session_desc = self._launcher.get_value(280 session_desc = self._launcher.session_desc
282 'launcher', 'session_desc') or session_desc
283281
284 self._sa.use_alternate_configuration(self._launcher)282 self._sa.use_alternate_configuration(self._launcher)
285283
286 if configuration['normal_user']:284 if configuration['normal_user']:
287 self._normal_user = configuration['normal_user']285 self._normal_user = configuration['normal_user']
288 else:286 else:
289 self._normal_user = self._launcher.get_value(287 self._normal_user = self._launcher.normal_user
290 'daemon', 'normal_user') or _guess_normal_user()288 if not self._normal_user:
289 self._normal_user = _guess_normal_user()
291 runner_kwargs = {290 runner_kwargs = {
292 'normal_user_provider': lambda: self._normal_user,291 'normal_user_provider': lambda: self._normal_user,
293 'stdin': self._pipe_to_subproc,292 'stdin': self._pipe_to_subproc,
@@ -301,13 +300,12 @@ class RemoteSessionAssistant():
301 'effective_normal_user': self._normal_user,300 'effective_normal_user': self._normal_user,
302 }).encode("UTF-8")301 }).encode("UTF-8")
303 self._sa.update_app_blob(new_blob)302 self._sa.update_app_blob(new_blob)
304 self._sa.configure_application_restart(303 self._sa.configure_application_restart(self._cmd_callback)
305 self._cmd_callback, session_type='remote')
306304
307 self._session_id = self._sa.get_session_id()305 self._session_id = self._sa.get_session_id()
308 tps = self._sa.get_test_plans()306 tps = self._sa.get_test_plans()
309 filtered_tps = set()307 filtered_tps = set()
310 for filter in self._launcher.get_value('test plan', 'filter'):308 for filter in self._launcher.test_plan_filters:
311 filtered_tps.update(fnmatch.filter(tps, filter))309 filtered_tps.update(fnmatch.filter(tps, filter))
312 filtered_tps = list(filtered_tps)310 filtered_tps = list(filtered_tps)
313 response = zip(filtered_tps, [self._sa.get_test_plan(311 response = zip(filtered_tps, [self._sa.get_test_plan(
@@ -324,6 +322,11 @@ class RemoteSessionAssistant():
324 self._sa.update_app_blob(json.dumps(322 self._sa.update_app_blob(json.dumps(
325 {'testplan_id': test_plan_id, }).encode("UTF-8"))323 {'testplan_id': test_plan_id, }).encode("UTF-8"))
326 self._sa.select_test_plan(test_plan_id)324 self._sa.select_test_plan(test_plan_id)
325 # TODO: REMOTE API RAPI: Change this API on the next RAPI bump
326 # previously the function returned bool signifying the need for sudo
327 # password. With slave being guaranteed to never need it anymor
328 # we can make this funciton return nothing
329 return False
327330
328 @allowed_when(Started)331 @allowed_when(Started)
329 def get_bootstrapping_todo_list(self):332 def get_bootstrapping_todo_list(self):
@@ -332,11 +335,10 @@ class RemoteSessionAssistant():
332 def finish_bootstrap(self):335 def finish_bootstrap(self):
333 self._sa.finish_bootstrap()336 self._sa.finish_bootstrap()
334 self._state = Bootstrapped337 self._state = Bootstrapped
335 if self._launcher.get_value('ui', 'auto_retry'):338 if self._launcher.auto_retry:
336 for job_id in self._sa.get_static_todo_list():339 for job_id in self._sa.get_static_todo_list():
337 job_state = self._sa.get_job_state(job_id)340 job_state = self._sa.get_job_state(job_id)
338 job_state.attempts = self._launcher.get_value(341 job_state.attempts = self._launcher.max_attempts
339 'ui', 'max_attempts')
340 return self._sa.get_static_todo_list()342 return self._sa.get_static_todo_list()
341343
342 def get_manifest_repr(self):344 def get_manifest_repr(self):
@@ -361,12 +363,10 @@ class RemoteSessionAssistant():
361363
362 def _get_ui_for_job(self, job):364 def _get_ui_for_job(self, job):
363 show_out = True365 show_out = True
364 if self._launcher.get_value(366 if self._launcher.output == 'hide-resource-and-attachment':
365 'ui', 'output') == 'hide-resource-and-attachment':
366 if job.plugin in ('local', 'resource', 'attachment'):367 if job.plugin in ('local', 'resource', 'attachment'):
367 show_out = False368 show_out = False
368 elif self._launcher.get_value(369 elif self._launcher.output in ['hide', 'hide-automated']:
369 'ui', 'output') in ['hide', 'hide-automated']:
370 if job.plugin in ('shell', 'local', 'resource', 'attachment'):370 if job.plugin in ('shell', 'local', 'resource', 'attachment'):
371 show_out = False371 show_out = False
372 if 'suppress-output' in job.get_flag_set():372 if 'suppress-output' in job.get_flag_set():
@@ -522,6 +522,27 @@ class RemoteSessionAssistant():
522 "todo": self._sa.get_dynamic_todo_list(),522 "todo": self._sa.get_dynamic_todo_list(),
523 }523 }
524524
525 def get_master_public_key(self):
526 # TODO: REMOTE API RAPI: Remove this API on the next RAPI bump
527 # this key is only for RAPI compliance. It will never be used as
528 # this master requires slave to be completely sudoless
529 return (
530 b'-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMII'
531 b'BCgKCAQEA5r0bjOA+IH5lDKkW3OYb\nDuEjf5VKgUlDSJJuyBlfLTBIXZ8j3s98'
532 b'6AbV0zB62rAcgiFrBOzx51IzBDBmHI8V\nYYpEa+q4OP4yprYpSg6xzX6LRQapC'
533 b'Iv9BAqN4MWrKBukGMzJyemIVEPv4BSHL5L/\nLY98Mwh4dAXxj5ZdsoVPqgeMo8'
534 b'dxfYEOwVRJvSkseIhxRL6tvgP37c48ApUyjdUO\n3C2YgqJRx7mKKDyLOvhDVEl'
535 b'MqkAfp6qS/8xcGBTEqn08dDQIgPl8KofpC9GXMGbK\nV9FGP+c1bpA3vMOfnpsE'
536 b'WCju2qDoTSKJTm3VMZj88mqH7nOpbk7JI/Yz0EmtNXOM\n6QIDAQAB\n-----EN'
537 b'D PUBLIC KEY-----')
538
539 def save_password(self, password):
540 """Store sudo password"""
541 # TODO: REMOTE API RAPI: Remove this API on the next RAPI bump
542 # if the slave is running it means we don't need password
543 # so we can consider call to this function as passing
544 return True
545
525 def finish_job(self, result=None):546 def finish_job(self, result=None):
526 # assert the thread completed547 # assert the thread completed
527 self.session_change_lock.acquire(blocking=False)548 self.session_change_lock.acquire(blocking=False)
@@ -537,7 +558,7 @@ class RemoteSessionAssistant():
537 if self._state != Bootstrapping:558 if self._state != Bootstrapping:
538 if not self._sa.get_dynamic_todo_list():559 if not self._sa.get_dynamic_todo_list():
539 if (560 if (
540 self._launcher.get_value('ui', 'auto_retry') and561 self._launcher.auto_retry and
541 self.get_rerun_candidates('auto')562 self.get_rerun_candidates('auto')
542 ):563 ):
543 self._state = TestsSelected564 self._state = TestsSelected
@@ -623,12 +644,11 @@ class RemoteSessionAssistant():
623 meta = self._sa.resume_session(session_id, runner_kwargs=runner_kwargs)644 meta = self._sa.resume_session(session_id, runner_kwargs=runner_kwargs)
624 app_blob = json.loads(meta.app_blob.decode("UTF-8"))645 app_blob = json.loads(meta.app_blob.decode("UTF-8"))
625 launcher = app_blob['launcher']646 launcher = app_blob['launcher']
626 self._launcher = Configuration.from_text(launcher, 'Remote launcher')647 self._launcher.read_string(launcher, False)
627 self._sa.use_alternate_configuration(self._launcher)648 self._sa.use_alternate_configuration(self._launcher)
628649
629 self._normal_user = app_blob.get(650 self._normal_user = app_blob.get(
630 'effective_normal_user', self._launcher.get_value(651 'effective_normal_user', self._launcher.normal_user)
631 'daemon', 'normal_user'))
632 _logger.info(652 _logger.info(
633 "normal_user after loading metadata: %r", self._normal_user)653 "normal_user after loading metadata: %r", self._normal_user)
634 test_plan_id = app_blob['testplan_id']654 test_plan_id = app_blob['testplan_id']
@@ -664,12 +684,12 @@ class RemoteSessionAssistant():
664684
665 # some jobs have already been run, so we need to update the attempts685 # some jobs have already been run, so we need to update the attempts
666 # count for future auto-rerunning686 # count for future auto-rerunning
667 if self._launcher.get_value('ui', 'auto_retry'):687 if self._launcher.auto_retry:
668 for job_id in [688 for job_id in [
669 job.id for job in self.get_rerun_candidates('auto')]:689 job.id for job in self.get_rerun_candidates('auto')]:
670 job_state = self._sa.get_job_state(job_id)690 job_state = self._sa.get_job_state(job_id)
671 job_state.attempts = self._launcher.get_value(691 job_state.attempts = self._launcher.max_attempts - len(
672 'ui', 'max_attempts') - len(job_state.result_history)692 job_state.result_history)
673693
674 self._state = TestsSelected694 self._state = TestsSelected
675695
@@ -695,6 +715,12 @@ class RemoteSessionAssistant():
695 return self._sa._manager715 return self._sa._manager
696716
697 @property717 @property
718 def passwordless_sudo(self):
719 # TODO: REMOTE API RAPI: Remove this API on the next RAPI bump
720 # if the slave is still running it means it's very passwordless
721 return True
722
723 @property
698 def sideloaded_providers(self):724 def sideloaded_providers(self):
699 return self._sa.sideloaded_providers725 return self._sa.sideloaded_providers
700726
diff --git a/plainbox/impl/test_config.py b/plainbox/impl/test_config.py
701deleted file mode 100644727deleted file mode 100644
index fce0700..0000000
--- a/plainbox/impl/test_config.py
+++ /dev/null
@@ -1,130 +0,0 @@
1# This file is part of Checkbox.
2#
3# Copyright 2020 Canonical Ltd.
4# Written by:
5# Maciej Kisielewski <maciej.kisielewski@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 3,
9# as published by the Free Software Foundation.
10#
11# Checkbox is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
18"""
19This module contains tests for the new Checkbox Config module
20"""
21from contextlib import contextmanager
22import logging
23from unittest import TestCase
24from unittest.mock import mock_open, patch
25
26from plainbox.impl.config import Configuration
27
28
29@contextmanager
30def muted_logging():
31 """Disable logging so the test that use this have no output."""
32 saved_level = logging.root.getEffectiveLevel()
33 logging.root.setLevel(logging.CRITICAL)
34 yield
35 logging.root.setLevel(saved_level)
36
37
38class ConfigurationTests(TestCase):
39 """Tests for the Configuration class."""
40
41 def test_empty_file_yields_defaults(self):
42 """A default configuration instance should have default values."""
43 # let's check a few values from random sections
44 cfg = Configuration()
45 self.assertEqual(cfg.get_value('test plan', 'filter'), ['*'])
46 self.assertTrue(cfg.get_value('launcher', 'local_submission'))
47 self.assertEqual(cfg.get_value('daemon', 'normal_user'), '')
48
49 @patch('os.path.isfile', return_value=True)
50 def test_one_var_overwrites(self, _):
51 """
52 One variable properly shadows defaults.
53
54 Having one (good) value in config should yield a config with
55 defaults except the one var placed in the config file.
56 """
57 ini_data = """
58 [launcher]
59 stock_reports = text
60 """
61 with patch('builtins.open', mock_open(read_data=ini_data)):
62 cfg = Configuration.from_path('unit test')
63 self.assertEqual(cfg.get_value('test plan', 'filter'), ['*'])
64 self.assertTrue(cfg.get_value('launcher', 'local_submission'))
65 self.assertEqual(cfg.get_value('daemon', 'normal_user'), '')
66 self.assertEqual(cfg.get_origin('daemon', 'normal_user'), '')
67 self.assertEqual(cfg.get_value('launcher', 'stock_reports'), ['text'])
68 self.assertEqual(
69 cfg.get_origin('launcher', 'stock_reports'),
70 'unit test')
71
72 @patch('os.path.isfile', return_value=True)
73 def test_string_list_distinction(self, _):
74 """
75 Parsing of lists and multi-word strings.
76
77 Depending on the config spec the field can be considered a string
78 (with spaces) or a list.
79 """
80 ini_data = """
81 [launcher]
82 launcher_version = 1
83 stock_reports = submission_files, text
84 session_title = A session title
85 """
86 with patch('builtins.open', mock_open(read_data=ini_data)):
87 cfg = Configuration.from_path('unit test')
88 self.assertEqual(
89 cfg.get_value('launcher', 'stock_reports'),
90 ['submission_files', 'text'])
91 self.assertEqual(
92 cfg.get_value('launcher', 'session_title'),
93 'A session title')
94
95 @patch('os.path.isfile', return_value=True)
96 def test_unexpected_content(self, _):
97 """
98 Yield problems with extra data in configs.
99 """
100 ini_data = """
101 [launcher]
102 barfoo = 5
103 [foobar]
104 """
105 with muted_logging():
106 with patch('builtins.open', mock_open(read_data=ini_data)):
107 cfg = Configuration.from_path('unit test')
108 self.assertEqual(len(cfg.get_problems()), 2)
109
110 def test_default_vars_are_not_supported(self):
111 """
112 Yield a problem when variable is defined in the [DEFAULT] section.
113 """
114 ini_data = """
115 [DEFAULT]
116 badvar = 4
117 """
118 with muted_logging():
119 with patch('builtins.open', mock_open(read_data=ini_data)):
120 cfg = Configuration.from_path('unit test')
121 self.assertEqual(len(cfg.get_problems()), 1)
122
123 @patch('os.path.isfile', return_value=False)
124 def test_ini_not_found(self, _):
125 """
126 Yield a problem when an ini file cannot be opened.
127 """
128 with muted_logging():
129 cfg = Configuration.from_path('invalid path')
130 self.assertEqual(len(cfg.get_problems()), 1)
diff --git a/plainbox/impl/test_launcher.py b/plainbox/impl/test_launcher.py
131new file mode 1006440new file mode 100644
index 0000000..47b47d1
--- /dev/null
+++ b/plainbox/impl/test_launcher.py
@@ -0,0 +1,136 @@
1# This file is part of Checkbox.
2#
3# Copyright 2016 Canonical Ltd.
4# Written by:
5# Maciej Kisielewski <maciej.kisielewski@canonical.com>
6#
7# Checkbox is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 3,
9# as published by the Free Software Foundation.
10#
11# Checkbox is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
18
19"""
20plainbox.impl.test_launcher
21=========================
22
23Test definitions for plainbox.imlp.launcher module
24"""
25
26from unittest import TestCase
27from textwrap import dedent
28
29from plainbox.impl.secure.config import Unset
30
31from plainbox.impl.launcher import LauncherDefinition
32from plainbox.impl.launcher import LauncherDefinition1
33
34
35class LauncherDefinitionTests(TestCase):
36 launcher_version_legacy = dedent("""
37 [launcher]
38 """)
39 launcher_version_1 = dedent("""
40 [launcher]
41 launcher_version = 1
42 """)
43 launcher_version_future = dedent("""
44 [launcher]
45 launcher_version = 2
46 """)
47
48 def test_get_concrete_launcher_legacy(self):
49 l = LauncherDefinition()
50 l.read_string(self.launcher_version_legacy)
51 with self.assertRaises(KeyError):
52 l.get_concrete_launcher()
53
54 def test_get_concrete_launcher_launcher1(self):
55 l = LauncherDefinition()
56 l.read_string(self.launcher_version_1)
57 cls = l.get_concrete_launcher().__class__
58 self.assertIs(cls, LauncherDefinition1)
59
60 def test_get_concrete_launcher_future_raises(self):
61 l = LauncherDefinition()
62 l.read_string(self.launcher_version_future)
63 with self.assertRaises(KeyError):
64 l.get_concrete_launcher()
65
66
67class LauncherDefinition1Tests(TestCase):
68
69 def test_defaults(self):
70 empty_launcher = dedent("""
71 [launcher]
72 launcher_version = 1
73 """)
74 l = LauncherDefinition1()
75 l.read_string(empty_launcher)
76 self.assertEqual(l.api_version, '0.99')
77 self.assertEqual(l.app_id, 'checkbox-cli')
78 self.assertEqual(l.api_flags, ['restartable'])
79 self.assertEqual(l.test_plan_filters, ['*'])
80 self.assertEqual(l.test_plan_default_selection, Unset)
81 self.assertEqual(l.test_plan_forced, False)
82 self.assertEqual(l.test_selection_forced, False)
83 self.assertEqual(l.ui_type, 'interactive')
84 self.assertEqual(l.auto_retry, False)
85 self.assertEqual(l.max_attempts, 3)
86 self.assertEqual(l.delay_before_retry, 1)
87 self.assertEqual(l.restart_strategy, Unset)
88
89 def test_smoke(self):
90 definition = dedent("""
91 [launcher]
92 launcher_version = 1
93 api_version = 0.99
94 api_flags = restartable
95 app_id = FOOBAR
96 [test plan]
97 unit = 2000.the.chosen.one
98 filter = 2000*, 3000* tp_foo*
99 forced = yes
100 [test selection]
101 forced = yes
102 [ui]
103 type = silent
104 auto_retry = yes
105 max_attempts = 5
106 delay_before_retry = 60
107 [restart]
108 strategy = magic
109 [report:foo_report]
110 exporter = bar_exporter
111 transport = file
112 [exporter:bar_exporter]
113 unit = bar_exporter_unit
114 [transport:file]
115 path = /tmp/path
116 """)
117 l = LauncherDefinition1()
118 l.read_string(definition)
119 self.assertEqual(l.api_version, '0.99')
120 self.assertEqual(l.app_id, 'FOOBAR')
121 self.assertEqual(l.api_flags, ['restartable'])
122 self.assertEqual(l.test_plan_filters, ['2000*', '3000*', 'tp_foo*'])
123 self.assertEqual(l.test_plan_default_selection, '2000.the.chosen.one')
124 self.assertEqual(l.test_plan_forced, True)
125 self.assertEqual(l.test_selection_forced, True)
126 self.assertEqual(l.ui_type, 'silent')
127 self.assertEqual(l.auto_retry, True)
128 self.assertEqual(l.max_attempts, 5)
129 self.assertEqual(l.delay_before_retry, 60)
130 self.assertEqual(l.restart_strategy, 'magic')
131 self.assertEqual(l.reports, {
132 'foo_report': {'exporter': 'bar_exporter', 'transport': 'file'}})
133 self.assertEqual(l.exporters, {
134 'bar_exporter': {'unit': 'bar_exporter_unit'}})
135 self.assertEqual(l.transports, {
136 'file': {'path': '/tmp/path'}})
diff --git a/plainbox/impl/unit/unit.py b/plainbox/impl/unit/unit.py
index 8041683..cd7b365 100644
--- a/plainbox/impl/unit/unit.py
+++ b/plainbox/impl/unit/unit.py
@@ -36,6 +36,7 @@ from jinja2 import Template
36from plainbox.i18n import gettext as _36from plainbox.i18n import gettext as _
37from plainbox.impl.decorators import cached_property37from plainbox.impl.decorators import cached_property
38from plainbox.impl.decorators import instance_method_lru_cache38from plainbox.impl.decorators import instance_method_lru_cache
39from plainbox.impl.secure.config import Unset
39from plainbox.impl.secure.origin import Origin40from plainbox.impl.secure.origin import Origin
40from plainbox.impl.secure.rfc822 import normalize_rfc822_value41from plainbox.impl.secure.rfc822 import normalize_rfc822_value
41from plainbox.impl.symbol import Symbol42from plainbox.impl.symbol import Symbol
@@ -608,7 +609,7 @@ class Unit(metaclass=UnitType):
608609
609 @instance_method_lru_cache(maxsize=None)610 @instance_method_lru_cache(maxsize=None)
610 def _checkbox_env(self):611 def _checkbox_env(self):
611 if self.config is not None and self.config.environment:612 if self.config is not None and self.config.environment is not Unset:
612 return self.config.environment613 return self.config.environment
613 else:614 else:
614 return {}615 return {}
diff --git a/plainbox/vendor/rpyc/__init__.py b/plainbox/vendor/rpyc/__init__.py
index 4ad98f7..9cd71d5 100644
--- a/plainbox/vendor/rpyc/__init__.py
+++ b/plainbox/vendor/rpyc/__init__.py
@@ -46,7 +46,7 @@ from plainbox.vendor.rpyc.core import (SocketStream, TunneledSocketStream, PipeS
46 Connection, Service, BaseNetref, AsyncResult, GenericException,46 Connection, Service, BaseNetref, AsyncResult, GenericException,
47 AsyncResultTimeout, VoidService, SlaveService, MasterService, ClassicService)47 AsyncResultTimeout, VoidService, SlaveService, MasterService, ClassicService)
48from plainbox.vendor.rpyc.utils.factory import (connect_stream, connect_channel, connect_pipes,48from plainbox.vendor.rpyc.utils.factory import (connect_stream, connect_channel, connect_pipes,
49 connect_stdpipes, connect, ssl_connect, list_services, discover, connect_by_service, connect_subproc,49 connect_stdpipes, connect, ssl_connect, discover, connect_by_service, connect_subproc,
50 connect_thread, ssh_connect)50 connect_thread, ssh_connect)
51from plainbox.vendor.rpyc.utils.helpers import async_, timed, buffiter, BgServingThread, restricted51from plainbox.vendor.rpyc.utils.helpers import async_, timed, buffiter, BgServingThread, restricted
52from plainbox.vendor.rpyc.utils import classic52from plainbox.vendor.rpyc.utils import classic
diff --git a/plainbox/vendor/rpyc/core/async_.py b/plainbox/vendor/rpyc/core/async_.py
index b46f4d0..fd69027 100644
--- a/plainbox/vendor/rpyc/core/async_.py
+++ b/plainbox/vendor/rpyc/core/async_.py
@@ -1,5 +1,4 @@
1import time # noqa: F4011import time # noqa: F401
2from threading import Event
3from plainbox.vendor.rpyc.lib import Timeout2from plainbox.vendor.rpyc.lib import Timeout
4from plainbox.vendor.rpyc.lib.compat import TimeoutError as AsyncResultTimeout3from plainbox.vendor.rpyc.lib.compat import TimeoutError as AsyncResultTimeout
54
@@ -13,14 +12,14 @@ class AsyncResult(object):
1312
14 def __init__(self, conn):13 def __init__(self, conn):
15 self._conn = conn14 self._conn = conn
16 self._is_ready = Event()15 self._is_ready = False
17 self._is_exc = None16 self._is_exc = None
18 self._obj = None17 self._obj = None
19 self._callbacks = []18 self._callbacks = []
20 self._ttl = Timeout(None)19 self._ttl = Timeout(None)
2120
22 def __repr__(self):21 def __repr__(self):
23 if self._is_ready.is_set():22 if self._is_ready:
24 state = "ready"23 state = "ready"
25 elif self._is_exc:24 elif self._is_exc:
26 state = "error"25 state = "error"
@@ -28,14 +27,14 @@ class AsyncResult(object):
28 state = "expired"27 state = "expired"
29 else:28 else:
30 state = "pending"29 state = "pending"
31 return "<AsyncResult object ({}) at 0x{:08x}>".format((state), (id(self)))30 return "<AsyncResult object (%s) at 0x%08x>" % (state, id(self))
3231
33 def __call__(self, is_exc, obj):32 def __call__(self, is_exc, obj):
34 if self.expired:33 if self.expired:
35 return34 return
36 self._is_exc = is_exc35 self._is_exc = is_exc
37 self._obj = obj36 self._obj = obj
38 self._is_ready.set()37 self._is_ready = True
39 for cb in self._callbacks:38 for cb in self._callbacks:
40 cb(self)39 cb(self)
41 del self._callbacks[:]40 del self._callbacks[:]
@@ -44,9 +43,9 @@ class AsyncResult(object):
44 """Waits for the result to arrive. If the AsyncResult object has an43 """Waits for the result to arrive. If the AsyncResult object has an
45 expiry set, and the result did not arrive within that timeout,44 expiry set, and the result did not arrive within that timeout,
46 an :class:`AsyncResultTimeout` exception is raised"""45 an :class:`AsyncResultTimeout` exception is raised"""
47 while not self._is_ready.is_set() and not self._ttl.expired():46 while not self._is_ready and not self._ttl.expired():
48 self._conn.serve(self._ttl)47 self._conn.serve(self._ttl)
49 if not self._is_ready.is_set():48 if not self._is_ready:
50 raise AsyncResultTimeout("result expired")49 raise AsyncResultTimeout("result expired")
5150
52 def add_callback(self, func):51 def add_callback(self, func):
@@ -57,7 +56,7 @@ class AsyncResult(object):
5756
58 :param func: the callback function to add57 :param func: the callback function to add
59 """58 """
60 if self._is_ready.is_set():59 if self._is_ready:
61 func(self)60 func(self)
62 else:61 else:
63 self._callbacks.append(func)62 self._callbacks.append(func)
@@ -73,12 +72,12 @@ class AsyncResult(object):
73 @property72 @property
74 def ready(self):73 def ready(self):
75 """Indicates whether the result has arrived"""74 """Indicates whether the result has arrived"""
76 if self._is_ready.is_set():75 if self._is_ready:
77 return True76 return True
78 if self._ttl.expired():77 if self._ttl.expired():
79 return False78 return False
80 self._conn.poll_all()79 self._conn.poll_all()
81 return self._is_ready.is_set()80 return self._is_ready
8281
83 @property82 @property
84 def error(self):83 def error(self):
@@ -88,7 +87,7 @@ class AsyncResult(object):
88 @property87 @property
89 def expired(self):88 def expired(self):
90 """Indicates whether the AsyncResult has expired"""89 """Indicates whether the AsyncResult has expired"""
91 return not self._is_ready.is_set() and self._ttl.expired()90 return not self._is_ready and self._ttl.expired()
9291
93 @property92 @property
94 def value(self):93 def value(self):
diff --git a/plainbox/vendor/rpyc/core/brine.py b/plainbox/vendor/rpyc/core/brine.py
index 20fa5c2..0545cb3 100644
--- a/plainbox/vendor/rpyc/core/brine.py
+++ b/plainbox/vendor/rpyc/core/brine.py
@@ -1,6 +1,6 @@
1"""*Brine* is a simple, fast and secure object serializer for **immutable** objects.1"""*Brine* is a simple, fast and secure object serializer for **immutable** objects.
22
3The following types are supported: ``int``, ``bool``, ``str``, ``float``,3The following types are supported: ``int``, ``long``, ``bool``, ``str``, ``float``,
4``unicode``, ``bytes``, ``slice``, ``complex``, ``tuple`` (of simple types),4``unicode``, ``bytes``, ``slice``, ``complex``, ``tuple`` (of simple types),
5``frozenset`` (of simple types) as well as the following singletons: ``None``,5``frozenset`` (of simple types) as well as the following singletons: ``None``,
6``NotImplemented``, and ``Ellipsis``.6``NotImplemented``, and ``Ellipsis``.
@@ -17,7 +17,7 @@ Example::
17 >>> x == z17 >>> x == z
18 True18 True
19"""19"""
20from plainbox.vendor.rpyc.lib.compat import Struct, BytesIO, BYTES_LITERAL20from plainbox.vendor.rpyc.lib.compat import Struct, BytesIO, is_py_3k, BYTES_LITERAL
2121
2222
23# singletons23# singletons
@@ -30,7 +30,7 @@ TAG_NOT_IMPLEMENTED = b"\x05"
30TAG_ELLIPSIS = b"\x06"30TAG_ELLIPSIS = b"\x06"
31# types31# types
32TAG_UNICODE = b"\x08"32TAG_UNICODE = b"\x08"
33# deprecated w/ py2 support TAG_LONG = b"\x09"33TAG_LONG = b"\x09"
34TAG_STR1 = b"\x0a"34TAG_STR1 = b"\x0a"
35TAG_STR2 = b"\x0b"35TAG_STR2 = b"\x0b"
36TAG_STR3 = b"\x0c"36TAG_STR3 = b"\x0c"
@@ -49,7 +49,10 @@ TAG_FLOAT = b"\x18"
49TAG_SLICE = b"\x19"49TAG_SLICE = b"\x19"
50TAG_FSET = b"\x1a"50TAG_FSET = b"\x1a"
51TAG_COMPLEX = b"\x1b"51TAG_COMPLEX = b"\x1b"
52IMM_INTS = dict((i, bytes([i + 0x50])) for i in range(-0x30, 0xa0))52if is_py_3k:
53 IMM_INTS = dict((i, bytes([i + 0x50])) for i in range(-0x30, 0xa0))
54else:
55 IMM_INTS = dict((i, chr(i + 0x50)) for i in range(-0x30, 0xa0))
5356
54I1 = Struct("!B")57I1 = Struct("!B")
55I4 = Struct("!L")58I4 = Struct("!L")
@@ -153,6 +156,13 @@ def _dump_str(obj, stream):
153 _dump_bytes(obj.encode("utf8"), stream)156 _dump_bytes(obj.encode("utf8"), stream)
154157
155158
159if not is_py_3k:
160 @register(_dump_registry, long) # noqa: F821
161 def _dump_long(obj, stream):
162 stream.append(TAG_LONG)
163 _dump_int(obj, stream)
164
165
156@register(_dump_registry, tuple)166@register(_dump_registry, tuple)
157def _dump_tuple(obj, stream):167def _dump_tuple(obj, stream):
158 lenobj = len(obj)168 lenobj = len(obj)
@@ -175,7 +185,7 @@ def _dump_tuple(obj, stream):
175185
176186
177def _undumpable(obj, stream):187def _undumpable(obj, stream):
178 raise TypeError("cannot dump {}".format((obj)))188 raise TypeError("cannot dump %r" % (obj,))
179189
180190
181def _dump(obj, stream):191def _dump(obj, stream):
@@ -219,6 +229,18 @@ def _load_empty_str(stream):
219 return b""229 return b""
220230
221231
232if is_py_3k:
233 @register(_load_registry, TAG_LONG)
234 def _load_long(stream):
235 obj = _load(stream)
236 return int(obj)
237else:
238 @register(_load_registry, TAG_LONG)
239 def _load_long(stream):
240 obj = _load(stream)
241 return long(obj) # noqa: F821
242
243
222@register(_load_registry, TAG_FLOAT)244@register(_load_registry, TAG_FLOAT)
223def _load_float(stream):245def _load_float(stream):
224 return F8.unpack(stream.read(8))[0]246 return F8.unpack(stream.read(8))[0]
@@ -294,10 +316,16 @@ def _load_tup_l1(stream):
294 return tuple(_load(stream) for i in range(l))316 return tuple(_load(stream) for i in range(l))
295317
296318
297@register(_load_registry, TAG_TUP_L4)319if is_py_3k:
298def _load_tup_l4(stream):320 @register(_load_registry, TAG_TUP_L4)
299 l, = I4.unpack(stream.read(4))321 def _load_tup_l4(stream):
300 return tuple(_load(stream) for i in range(l))322 l, = I4.unpack(stream.read(4))
323 return tuple(_load(stream) for i in range(l))
324else:
325 @register(_load_registry, TAG_TUP_L4)
326 def _load_tup_l4(stream):
327 l, = I4.unpack(stream.read(4))
328 return tuple(_load(stream) for i in xrange(l)) # noqa
301329
302330
303@register(_load_registry, TAG_SLICE)331@register(_load_registry, TAG_SLICE)
@@ -357,7 +385,12 @@ def load(data):
357 return _load(stream)385 return _load(stream)
358386
359387
360simple_types = frozenset([type(None), int, bool, float, bytes, str, complex, type(NotImplemented), type(Ellipsis)])388if is_py_3k:
389 simple_types = frozenset([type(None), int, bool, float, bytes, str, complex,
390 type(NotImplemented), type(Ellipsis)])
391else:
392 simple_types = frozenset([type(None), int, long, bool, float, bytes, unicode, complex, # noqa: F821
393 type(NotImplemented), type(Ellipsis)])
361394
362395
363def dumpable(obj):396def dumpable(obj):
diff --git a/plainbox/vendor/rpyc/core/consts.py b/plainbox/vendor/rpyc/core/consts.py
index 300186b..31a532d 100644
--- a/plainbox/vendor/rpyc/core/consts.py
+++ b/plainbox/vendor/rpyc/core/consts.py
@@ -37,9 +37,6 @@ HANDLE_INSTANCECHECK = 20
37# optimized exceptions37# optimized exceptions
38EXC_STOP_ITERATION = 138EXC_STOP_ITERATION = 1
3939
40# IO values
41STREAM_CHUNK = 64000 # read/write chunk is 64KB, too large of a value will degrade response for other clients
42
43# DEBUG40# DEBUG
44# for k in globals().keys():41# for k in globals().keys():
45# globals()[k] = k42# globals()[k] = k
diff --git a/plainbox/vendor/rpyc/core/netref.py b/plainbox/vendor/rpyc/core/netref.py
index 9287906..fd15ef1 100644
--- a/plainbox/vendor/rpyc/core/netref.py
+++ b/plainbox/vendor/rpyc/core/netref.py
@@ -4,7 +4,7 @@ of *magic*, so beware.
4import sys4import sys
5import types5import types
6from plainbox.vendor.rpyc.lib import get_methods, get_id_pack6from plainbox.vendor.rpyc.lib import get_methods, get_id_pack
7from plainbox.vendor.rpyc.lib.compat import pickle, maxint, with_metaclass7from plainbox.vendor.rpyc.lib.compat import pickle, is_py_3k, maxint, with_metaclass
8from plainbox.vendor.rpyc.core import consts8from plainbox.vendor.rpyc.core import consts
99
1010
@@ -16,7 +16,6 @@ DELETED_ATTRS = frozenset([
16 '__array_struct__', '__array_interface__',16 '__array_struct__', '__array_interface__',
17])17])
1818
19"""the set of attributes that are local to the netref object"""
20LOCAL_ATTRS = frozenset([19LOCAL_ATTRS = frozenset([
21 '____conn__', '____id_pack__', '____refcount__', '__class__', '__cmp__', '__del__', '__delattr__',20 '____conn__', '____id_pack__', '____refcount__', '__class__', '__cmp__', '__del__', '__delattr__',
22 '__dir__', '__doc__', '__getattr__', '__getattribute__', '__hash__', '__instancecheck__',21 '__dir__', '__doc__', '__getattr__', '__getattribute__', '__hash__', '__instancecheck__',
@@ -25,25 +24,39 @@ LOCAL_ATTRS = frozenset([
25 '__weakref__', '__dict__', '__methods__', '__exit__',24 '__weakref__', '__dict__', '__methods__', '__exit__',
26 '__eq__', '__ne__', '__lt__', '__gt__', '__le__', '__ge__',25 '__eq__', '__ne__', '__lt__', '__gt__', '__le__', '__ge__',
27]) | DELETED_ATTRS26]) | DELETED_ATTRS
27"""the set of attributes that are local to the netref object"""
2828
29"""a list of types considered built-in (shared between connections)
30this is needed because iterating the members of the builtins module is not enough,
31some types (e.g NoneType) are not members of the builtins module.
32TODO: this list is not complete.
33"""
34_builtin_types = [29_builtin_types = [
35 type, object, bool, complex, dict, float, int, list, slice, str, tuple, set,30 type, object, bool, complex, dict, float, int, list, slice, str, tuple, set,
36 frozenset, BaseException, Exception, type(None), types.BuiltinFunctionType, types.GeneratorType,31 frozenset, Exception, type(None), types.BuiltinFunctionType, types.GeneratorType,
37 types.MethodType, types.CodeType, types.FrameType, types.TracebackType,32 types.MethodType, types.CodeType, types.FrameType, types.TracebackType,
38 types.ModuleType, types.FunctionType, types.MappingProxyType,33 types.ModuleType, types.FunctionType,
3934
40 type(int.__add__), # wrapper_descriptor35 type(int.__add__), # wrapper_descriptor
41 type((1).__add__), # method-wrapper36 type((1).__add__), # method-wrapper
42 type(iter([])), # listiterator37 type(iter([])), # listiterator
43 type(iter(())), # tupleiterator38 type(iter(())), # tupleiterator
44 type(iter(set())), # setiterator39 type(iter(set())), # setiterator
45 bytes, bytearray, type(iter(range(10))), memoryview
46]40]
41"""a list of types considered built-in (shared between connections)"""
42
43try:
44 BaseException
45except NameError:
46 pass
47else:
48 _builtin_types.append(BaseException)
49
50if is_py_3k:
51 _builtin_types.extend([
52 bytes, bytearray, type(iter(range(10))), memoryview,
53 ])
54 xrange = range
55else:
56 _builtin_types.extend([
57 basestring, unicode, long, xrange, type(iter(xrange(10))), file, # noqa
58 types.InstanceType, types.ClassType, types.DictProxyType,
59 ])
47_normalized_builtin_types = {}60_normalized_builtin_types = {}
4861
4962
@@ -88,9 +101,9 @@ class NetrefMetaclass(type):
88101
89 def __repr__(self):102 def __repr__(self):
90 if self.__module__:103 if self.__module__:
91 return "<netref class '{}.{}'>".format((self.__module__), (self.__name__))104 return "<netref class '%s.%s'>" % (self.__module__, self.__name__)
92 else:105 else:
93 return "<netref class '{}'>".format((self.__name__))106 return "<netref class '%s'>" % (self.__name__,)
94107
95108
96class BaseNetref(with_metaclass(NetrefMetaclass, object)):109class BaseNetref(with_metaclass(NetrefMetaclass, object)):
@@ -303,24 +316,19 @@ def class_factory(id_pack, methods):
303 name_pack = id_pack[0]316 name_pack = id_pack[0]
304 class_descriptor = None317 class_descriptor = None
305 if name_pack is not None:318 if name_pack is not None:
306 # attempt to resolve __class__ using normalized builtins first319 # attempt to resolve __class__ using sys.modules (i.e. builtins and imported modules)
307 _builtin_class = _normalized_builtin_types.get(name_pack)320 _module = None
308 if _builtin_class is not None:321 cursor = len(name_pack)
309 class_descriptor = NetrefClass(_builtin_class)322 while cursor != -1:
310 # then by imported modules (this also tries all builtins under "builtins")323 _module = sys.modules.get(name_pack[:cursor])
311 else:324 if _module is None:
312 _module = None325 cursor = name_pack[:cursor].rfind('.')
313 cursor = len(name_pack)326 continue
314 while cursor != -1:327 _class_name = name_pack[cursor + 1:]
315 _module = sys.modules.get(name_pack[:cursor])328 _class = getattr(_module, _class_name, None)
316 if _module is None:329 if _class is not None and hasattr(_class, '__class__'):
317 cursor = name_pack[:cursor].rfind('.')330 class_descriptor = NetrefClass(_class)
318 continue331 break
319 _class_name = name_pack[cursor + 1:]
320 _class = getattr(_module, _class_name, None)
321 if _class is not None and hasattr(_class, '__class__'):
322 class_descriptor = NetrefClass(_class)
323 break
324 ns['__class__'] = class_descriptor332 ns['__class__'] = class_descriptor
325 netref_name = class_descriptor.owner.__name__ if class_descriptor is not None else name_pack333 netref_name = class_descriptor.owner.__name__ if class_descriptor is not None else name_pack
326 # create methods that must perform a syncreq334 # create methods that must perform a syncreq
diff --git a/plainbox/vendor/rpyc/core/protocol.py b/plainbox/vendor/rpyc/core/protocol.py
index 703d499..56cd9a9 100644
--- a/plainbox/vendor/rpyc/core/protocol.py
+++ b/plainbox/vendor/rpyc/core/protocol.py
@@ -8,7 +8,7 @@ import gc # noqa: F401
88
9from threading import Lock, Condition9from threading import Lock, Condition
10from plainbox.vendor.rpyc.lib import spawn, Timeout, get_methods, get_id_pack10from plainbox.vendor.rpyc.lib import spawn, Timeout, get_methods, get_id_pack
11from plainbox.vendor.rpyc.lib.compat import pickle, next, maxint, select_error, acquire_lock # noqa: F40111from plainbox.vendor.rpyc.lib.compat import pickle, next, is_py_3k, maxint, select_error, acquire_lock # noqa: F401
12from plainbox.vendor.rpyc.lib.colls import WeakValueDict, RefCountingColl12from plainbox.vendor.rpyc.lib.colls import WeakValueDict, RefCountingColl
13from plainbox.vendor.rpyc.core import consts, brine, vinegar, netref13from plainbox.vendor.rpyc.core import consts, brine, vinegar, netref
14from plainbox.vendor.rpyc.core.async_ import AsyncResult14from plainbox.vendor.rpyc.core.async_ import AsyncResult
@@ -39,7 +39,7 @@ DEFAULT_CONFIG = dict(
39 '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__',39 '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__',
40 '__rxor__', '__setitem__', '__setslice__', '__str__', '__sub__',40 '__rxor__', '__setitem__', '__setslice__', '__str__', '__sub__',
41 '__truediv__', '__xor__', 'next', '__length_hint__', '__enter__',41 '__truediv__', '__xor__', 'next', '__length_hint__', '__enter__',
42 '__exit__', '__next__', '__format__']),42 '__exit__', '__next__', ]),
43 exposed_prefix="exposed_",43 exposed_prefix="exposed_",
44 allow_getattr=True,44 allow_getattr=True,
45 allow_setattr=False,45 allow_setattr=False,
@@ -60,8 +60,6 @@ DEFAULT_CONFIG = dict(
60 endpoints=None,60 endpoints=None,
61 logger=None,61 logger=None,
62 sync_request_timeout=30,62 sync_request_timeout=30,
63 before_closed=None,
64 close_catchall=False,
65)63)
66"""64"""
67The default configuration dictionary of the protocol. You can override these parameters65The default configuration dictionary of the protocol. You can override these parameters
@@ -140,7 +138,7 @@ class Connection(object):
140 self._config = DEFAULT_CONFIG.copy()138 self._config = DEFAULT_CONFIG.copy()
141 self._config.update(config)139 self._config.update(config)
142 if self._config["connid"] is None:140 if self._config["connid"] is None:
143 self._config["connid"] = "conn{}".format((next(_connection_id_generator)))141 self._config["connid"] = "conn%d" % (next(_connection_id_generator),)
144142
145 self._HANDLERS = self._request_handlers()143 self._HANDLERS = self._request_handlers()
146 self._channel = channel144 self._channel = channel
@@ -169,7 +167,7 @@ class Connection(object):
169167
170 def __repr__(self):168 def __repr__(self):
171 a, b = object.__repr__(self).split(" object ")169 a, b = object.__repr__(self).split(" object ")
172 return "{} {!r} object {}".format((a), (self._config['connid']), (b))170 return "%s %r object %s" % (a, self._config["connid"], b)
173171
174 def _cleanup(self, _anyway=True): # IO172 def _cleanup(self, _anyway=True): # IO
175 if self._closed and not _anyway:173 if self._closed and not _anyway:
@@ -188,19 +186,17 @@ class Connection(object):
188 # self._config.clear()186 # self._config.clear()
189 del self._HANDLERS187 del self._HANDLERS
190188
191 def close(self): # IO189 def close(self, _catchall=True): # IO
192 """closes the connection, releasing all held resources"""190 """closes the connection, releasing all held resources"""
193 if self._closed:191 if self._closed:
194 return192 return
193 self._closed = True
195 try:194 try:
196 self._closed = True
197 if self._config.get("before_closed"):
198 self._config["before_closed"](self.root)
199 self._async_request(consts.HANDLE_CLOSE)195 self._async_request(consts.HANDLE_CLOSE)
200 except EOFError:196 except EOFError:
201 pass197 pass
202 except Exception:198 except Exception:
203 if not self._config["close_catchall"]:199 if not _catchall:
204 raise200 raise
205 finally:201 finally:
206 self._cleanup(_anyway=True)202 self._cleanup(_anyway=True)
@@ -298,7 +294,7 @@ class Connection(object):
298 proxy = self._netref_factory(id_pack)294 proxy = self._netref_factory(id_pack)
299 self._proxy_cache[id_pack] = proxy295 self._proxy_cache[id_pack] = proxy
300 return proxy296 return proxy
301 raise ValueError("invalid label {!r}".format((label)))297 raise ValueError("invalid label %r" % (label,))
302298
303 def _netref_factory(self, id_pack): # boxing299 def _netref_factory(self, id_pack): # boxing
304 """id_pack is for remote, so when class id fails to directly match """300 """id_pack is for remote, so when class id fails to directly match """
@@ -367,7 +363,7 @@ class Connection(object):
367 obj = self._unbox_exc(args)363 obj = self._unbox_exc(args)
368 self._seq_request_callback(msg, seq, True, obj)364 self._seq_request_callback(msg, seq, True, obj)
369 else:365 else:
370 raise ValueError("invalid message type: {!r}".format((msg)))366 raise ValueError("invalid message type: %r" % (msg,))
371367
372 def serve(self, timeout=1, wait_for_lock=True): # serving368 def serve(self, timeout=1, wait_for_lock=True): # serving
373 """Serves a single request or reply that arrives within the given369 """Serves a single request or reply that arrives within the given
@@ -492,7 +488,7 @@ class Connection(object):
492 """488 """
493 timeout = kwargs.pop("timeout", None)489 timeout = kwargs.pop("timeout", None)
494 if kwargs:490 if kwargs:
495 raise TypeError("got unexpected keyword argument(s) {list(kwargs.keys()}")491 raise TypeError("got unexpected keyword argument(s) %s" % (list(kwargs.keys()),))
496 res = AsyncResult(self)492 res = AsyncResult(self)
497 self._async_request(handler, args, res)493 self._async_request(handler, args, res)
498 if timeout is not None:494 if timeout is not None:
@@ -509,7 +505,7 @@ class Connection(object):
509 def _check_attr(self, obj, name, perm): # attribute access505 def _check_attr(self, obj, name, perm): # attribute access
510 config = self._config506 config = self._config
511 if not config[perm]:507 if not config[perm]:
512 raise AttributeError("cannot access {!r}".format((name)))508 raise AttributeError("cannot access %r" % (name,))
513 prefix = config["allow_exposed_attrs"] and config["exposed_prefix"]509 prefix = config["allow_exposed_attrs"] and config["exposed_prefix"]
514 plain = config["allow_all_attrs"]510 plain = config["allow_all_attrs"]
515 plain |= config["allow_exposed_attrs"] and name.startswith(prefix)511 plain |= config["allow_exposed_attrs"] and name.startswith(prefix)
@@ -522,13 +518,18 @@ class Connection(object):
522 return prefix + name518 return prefix + name
523 if plain:519 if plain:
524 return name # chance for better traceback520 return name # chance for better traceback
525 raise AttributeError("cannot access {!r}".format((name)))521 raise AttributeError("cannot access %r" % (name,))
526522
527 def _access_attr(self, obj, name, args, overrider, param, default): # attribute access523 def _access_attr(self, obj, name, args, overrider, param, default): # attribute access
528 if type(name) is bytes:524 if is_py_3k:
529 name = str(name, "utf8")525 if type(name) is bytes:
530 elif type(name) is not str:526 name = str(name, "utf8")
531 raise TypeError("name must be a string")527 elif type(name) is not str:
528 raise TypeError("name must be a string")
529 else:
530 if type(name) not in (str, unicode): # noqa
531 raise TypeError("name must be a string")
532 name = str(name) # IronPython issue #10 + py3k issue
532 accessor = getattr(type(obj), overrider, None)533 accessor = getattr(type(obj), overrider, None)
533 if accessor is None:534 if accessor is None:
534 accessor = default535 accessor = default
@@ -637,7 +638,7 @@ class Connection(object):
637 # since __mro__ is not a safe attribute the request is forwarded using the proxy connection638 # since __mro__ is not a safe attribute the request is forwarded using the proxy connection
638 # relates to issue #346 or tests.test_netref_hierachy.Test_Netref_Hierarchy.test_StandardError639 # relates to issue #346 or tests.test_netref_hierachy.Test_Netref_Hierarchy.test_StandardError
639 conn = obj.____conn__640 conn = obj.____conn__
640 return conn.sync_request(consts.HANDLE_INSPECT, other_id_pack)641 return conn.sync_request(consts.HANDLE_INSPECT, id_pack)
641 # Create a name pack which would be familiar here and see if there is a hit642 # Create a name pack which would be familiar here and see if there is a hit
642 other_id_pack2 = (other_id_pack[0], other_id_pack[1], 0)643 other_id_pack2 = (other_id_pack[0], other_id_pack[1], 0)
643 if other_id_pack[0] in netref.builtin_classes_cache:644 if other_id_pack[0] in netref.builtin_classes_cache:
diff --git a/plainbox/vendor/rpyc/core/service.py b/plainbox/vendor/rpyc/core/service.py
index 6159b75..55526be 100644
--- a/plainbox/vendor/rpyc/core/service.py
+++ b/plainbox/vendor/rpyc/core/service.py
@@ -9,7 +9,7 @@ can interoperate, you're good to go.
9from functools import partial9from functools import partial
1010
11from plainbox.vendor.rpyc.lib import hybridmethod11from plainbox.vendor.rpyc.lib import hybridmethod
12from plainbox.vendor.rpyc.lib.compat import execute12from plainbox.vendor.rpyc.lib.compat import execute, is_py_3k
13from plainbox.vendor.rpyc.core.protocol import Connection13from plainbox.vendor.rpyc.core.protocol import Connection
1414
1515
@@ -137,10 +137,7 @@ class ModuleNamespace(object):
137 return self.__cache[name]137 return self.__cache[name]
138138
139 def __getattr__(self, name):139 def __getattr__(self, name):
140 try:140 return self[name]
141 return self[name]
142 except ImportError:
143 raise AttributeError(name)
144141
145142
146class Slave(object):143class Slave(object):
@@ -218,13 +215,14 @@ class MasterService(Service):
218 @staticmethod215 @staticmethod
219 def _install(conn, slave):216 def _install(conn, slave):
220 modules = ModuleNamespace(slave.getmodule)217 modules = ModuleNamespace(slave.getmodule)
218 builtin = modules.builtins if is_py_3k else modules.__builtin__
221 conn.modules = modules219 conn.modules = modules
222 conn.eval = slave.eval220 conn.eval = slave.eval
223 conn.execute = slave.execute221 conn.execute = slave.execute
224 conn.namespace = slave.namespace222 conn.namespace = slave.namespace
225 conn.builtins = modules.builtins223 conn.builtin = builtin
226 conn.builtin = modules.builtins # TODO: cruft from py2 that requires cleanup elsewhere and CHANGELOG note224 conn.builtins = builtin
227 from rpyc.utils.classic import teleport_function225 from plainbox.vendor.rpyc.utils.classic import teleport_function
228 conn.teleport = partial(teleport_function, conn)226 conn.teleport = partial(teleport_function, conn)
229227
230228
diff --git a/plainbox/vendor/rpyc/core/stream.py b/plainbox/vendor/rpyc/core/stream.py
index 7d3cab7..6c8d790 100644
--- a/plainbox/vendor/rpyc/core/stream.py
+++ b/plainbox/vendor/rpyc/core/stream.py
@@ -8,7 +8,6 @@ import socket
8import errno8import errno
9from plainbox.vendor.rpyc.lib import safe_import, Timeout, socket_backoff_connect9from plainbox.vendor.rpyc.lib import safe_import, Timeout, socket_backoff_connect
10from plainbox.vendor.rpyc.lib.compat import poll, select_error, BYTES_LITERAL, get_exc_errno, maxint # noqa: F40110from plainbox.vendor.rpyc.lib.compat import poll, select_error, BYTES_LITERAL, get_exc_errno, maxint # noqa: F401
11from plainbox.vendor.rpyc.core.consts import STREAM_CHUNK
12win32file = safe_import("win32file")11win32file = safe_import("win32file")
13win32pipe = safe_import("win32pipe")12win32pipe = safe_import("win32pipe")
14win32event = safe_import("win32event")13win32event = safe_import("win32event")
@@ -112,7 +111,7 @@ class SocketStream(Stream):
112 """A stream over a socket"""111 """A stream over a socket"""
113112
114 __slots__ = ("sock",)113 __slots__ = ("sock",)
115 MAX_IO_CHUNK = STREAM_CHUNK114 MAX_IO_CHUNK = 64000 # read/write chunk is 64KB, too large of a value will degrade response for other clients
116115
117 def __init__(self, sock):116 def __init__(self, sock):
118 self.sock = sock117 self.sock = sock
@@ -292,7 +291,7 @@ class PipeStream(Stream):
292 """A stream over two simplex pipes (one used to input, another for output)"""291 """A stream over two simplex pipes (one used to input, another for output)"""
293292
294 __slots__ = ("incoming", "outgoing")293 __slots__ = ("incoming", "outgoing")
295 MAX_IO_CHUNK = STREAM_CHUNK294 MAX_IO_CHUNK = 32000
296295
297 def __init__(self, incoming, outgoing):296 def __init__(self, incoming, outgoing):
298 outgoing.flush()297 outgoing.flush()
@@ -370,7 +369,7 @@ class Win32PipeStream(Stream):
370369
371 __slots__ = ("incoming", "outgoing", "_fileno", "_keepalive")370 __slots__ = ("incoming", "outgoing", "_fileno", "_keepalive")
372 PIPE_BUFFER_SIZE = 130000371 PIPE_BUFFER_SIZE = 130000
373 MAX_IO_CHUNK = STREAM_CHUNK372 MAX_IO_CHUNK = 32000
374373
375 def __init__(self, incoming, outgoing):374 def __init__(self, incoming, outgoing):
376 import msvcrt375 import msvcrt
diff --git a/plainbox/vendor/rpyc/core/vinegar.py b/plainbox/vendor/rpyc/core/vinegar.py
index db1f3e1..b766b58 100644
--- a/plainbox/vendor/rpyc/core/vinegar.py
+++ b/plainbox/vendor/rpyc/core/vinegar.py
@@ -22,6 +22,7 @@ except ImportError:
22from plainbox.vendor.rpyc.core import brine22from plainbox.vendor.rpyc.core import brine
23from plainbox.vendor.rpyc.core import consts23from plainbox.vendor.rpyc.core import consts
24from plainbox.vendor.rpyc import version24from plainbox.vendor.rpyc import version
25from plainbox.vendor.rpyc.lib.compat import is_py_3k
2526
2627
27REMOTE_LINE_START = "\n\n========= Remote Traceback "28REMOTE_LINE_START = "\n\n========= Remote Traceback "
@@ -29,6 +30,13 @@ REMOTE_LINE_END = " =========\n"
29REMOTE_LINE = "{0}({{}}){1}".format(REMOTE_LINE_START, REMOTE_LINE_END)30REMOTE_LINE = "{0}({{}}){1}".format(REMOTE_LINE_START, REMOTE_LINE_END)
3031
3132
33try:
34 BaseException
35except NameError:
36 # python 2.4 compatible
37 BaseException = Exception
38
39
32def dump(typ, val, tb, include_local_traceback, include_local_version):40def dump(typ, val, tb, include_local_traceback, include_local_version):
33 """Dumps the given exceptions info, as returned by ``sys.exc_info()``41 """Dumps the given exceptions info, as returned by ``sys.exc_info()``
3442
@@ -127,15 +135,23 @@ def load(val, import_custom_exceptions, instantiate_custom_exceptions, instantia
127 else:135 else:
128 cls = None136 cls = None
129137
130 if not isinstance(cls, type) or not issubclass(cls, BaseException):138 if is_py_3k:
131 cls = None139 if not isinstance(cls, type) or not issubclass(cls, BaseException):
140 cls = None
141 else:
142 if not isinstance(cls, (type, ClassType)):
143 cls = None
144 elif issubclass(cls, ClassType) and not instantiate_oldstyle_exceptions:
145 cls = None
146 elif not issubclass(cls, BaseException):
147 cls = None
132148
133 if cls is None:149 if cls is None:
134 fullname = "{}.{}".format((modname), (clsname))150 fullname = "%s.%s" % (modname, clsname)
135 # py2: `type()` expects `str` not `unicode`!151 # py2: `type()` expects `str` not `unicode`!
136 fullname = str(fullname)152 fullname = str(fullname)
137 if fullname not in _generic_exceptions_cache:153 if fullname not in _generic_exceptions_cache:
138 fakemodule = {"__module__": "{}/{}".format((__name__), (modname))}154 fakemodule = {"__module__": "%s/%s" % (__name__, modname)}
139 if isinstance(GenericException, ClassType):155 if isinstance(GenericException, ClassType):
140 _generic_exceptions_cache[fullname] = ClassType(fullname, (GenericException,), fakemodule)156 _generic_exceptions_cache[fullname] = ClassType(fullname, (GenericException,), fakemodule)
141 else:157 else:
diff --git a/plainbox/vendor/rpyc/lib/__init__.py b/plainbox/vendor/rpyc/lib/__init__.py
index 6462d36..d96656b 100644
--- a/plainbox/vendor/rpyc/lib/__init__.py
+++ b/plainbox/vendor/rpyc/lib/__init__.py
@@ -19,8 +19,8 @@ class MissingModule(object):
1919
20 def __getattr__(self, name):20 def __getattr__(self, name):
21 if name.startswith("__"): # issue 7121 if name.startswith("__"): # issue 71
22 raise AttributeError("module {!r} not found".format((self.__name)))22 raise AttributeError("module %r not found" % (self.__name,))
23 raise ImportError("module {!r} not found".format((self.__name)))23 raise ImportError("module %r not found" % (self.__name,))
2424
25 def __bool__(self):25 def __bool__(self):
26 return False26 return False
@@ -96,7 +96,7 @@ def spawn_waitready(init, main):
96 return thread, stack.pop()96 return thread, stack.pop()
9797
9898
99class Timeout(object):99class Timeout:
100100
101 def __init__(self, timeout):101 def __init__(self, timeout):
102 if isinstance(timeout, Timeout):102 if isinstance(timeout, Timeout):
@@ -175,7 +175,7 @@ def get_id_pack(obj):
175 else:175 else:
176 name_pack = '{0}.{1}'.format(obj.__class__.__module__, obj.__name__)176 name_pack = '{0}.{1}'.format(obj.__class__.__module__, obj.__name__)
177 elif inspect.ismodule(obj):177 elif inspect.ismodule(obj):
178 name_pack = '{0}.{1}'.format(obj.__module__, obj.__name__)178 name_pack = '{0}.{1}'.format(obj__module__, obj.__name__)
179 print(name_pack)179 print(name_pack)
180 elif hasattr(obj, '__module__'):180 elif hasattr(obj, '__module__'):
181 name_pack = '{0}.{1}'.format(obj.__module__, obj.__name__)181 name_pack = '{0}.{1}'.format(obj.__module__, obj.__name__)
diff --git a/plainbox/vendor/rpyc/lib/compat.py b/plainbox/vendor/rpyc/lib/compat.py
index 63c48fe..35be8fd 100644
--- a/plainbox/vendor/rpyc/lib/compat.py
+++ b/plainbox/vendor/rpyc/lib/compat.py
@@ -7,7 +7,6 @@ import time
77
8is_py_3k = (sys.version_info[0] >= 3)8is_py_3k = (sys.version_info[0] >= 3)
9is_py_gte38 = is_py_3k and (sys.version_info[1] >= 8)9is_py_gte38 = is_py_3k and (sys.version_info[1] >= 8)
10is_py_gte37 = is_py_3k and (sys.version_info[1] >= 7)
1110
1211
13if is_py_3k:12if is_py_3k:
diff --git a/plainbox/vendor/rpyc/utils/authenticators.py b/plainbox/vendor/rpyc/utils/authenticators.py
index 63ee828..0d97882 100644
--- a/plainbox/vendor/rpyc/utils/authenticators.py
+++ b/plainbox/vendor/rpyc/utils/authenticators.py
@@ -71,7 +71,7 @@ class SSLAuthenticator(object):
71 else:71 else:
72 self.cert_reqs = cert_reqs72 self.cert_reqs = cert_reqs
73 if ssl_version is None:73 if ssl_version is None:
74 self.ssl_version = ssl.PROTOCOL_TLS74 self.ssl_version = ssl.PROTOCOL_TLSv1
75 else:75 else:
76 self.ssl_version = ssl_version76 self.ssl_version = ssl_version
7777
diff --git a/plainbox/vendor/rpyc/utils/classic.py b/plainbox/vendor/rpyc/utils/classic.py
index 3903a79..2f97dcf 100644
--- a/plainbox/vendor/rpyc/utils/classic.py
+++ b/plainbox/vendor/rpyc/utils/classic.py
@@ -2,11 +2,10 @@ from __future__ import with_statement
2import sys2import sys
3import os3import os
4import inspect4import inspect
5from plainbox.vendor.rpyc.lib.compat import pickle, execute5from plainbox.vendor.rpyc.lib.compat import pickle, execute, is_py_3k # noqa: F401
6from plainbox.vendor.rpyc.core.service import ClassicService, Slave6from plainbox.vendor.rpyc.core.service import ClassicService, Slave
7from plainbox.vendor.rpyc.utils import factory7from plainbox.vendor.rpyc.utils import factory
8from plainbox.vendor.rpyc.core.service import ModuleNamespace # noqa: F4018from plainbox.vendor.rpyc.core.service import ModuleNamespace # noqa: F401
9from plainbox.vendor.rpyc.core.consts import STREAM_CHUNK
10from contextlib import contextmanager9from contextlib import contextmanager
1110
1211
@@ -172,7 +171,7 @@ def connect_multiprocess(args={}):
172# remoting utilities171# remoting utilities
173# ===============================================================================172# ===============================================================================
174173
175def upload(conn, localpath, remotepath, filter=None, ignore_invalid=False, chunk_size=STREAM_CHUNK):174def upload(conn, localpath, remotepath, filter=None, ignore_invalid=False, chunk_size=16000):
176 """uploads a file or a directory to the given remote path175 """uploads a file or a directory to the given remote path
177176
178 :param localpath: the local file or directory177 :param localpath: the local file or directory
@@ -187,10 +186,10 @@ def upload(conn, localpath, remotepath, filter=None, ignore_invalid=False, chunk
187 upload_file(conn, localpath, remotepath, chunk_size)186 upload_file(conn, localpath, remotepath, chunk_size)
188 else:187 else:
189 if not ignore_invalid:188 if not ignore_invalid:
190 raise ValueError("cannot upload {!r}".format((localpath)))189 raise ValueError("cannot upload %r" % (localpath,))
191190
192191
193def upload_file(conn, localpath, remotepath, chunk_size=STREAM_CHUNK):192def upload_file(conn, localpath, remotepath, chunk_size=16000):
194 with open(localpath, "rb") as lf:193 with open(localpath, "rb") as lf:
195 with conn.builtin.open(remotepath, "wb") as rf:194 with conn.builtin.open(remotepath, "wb") as rf:
196 while True:195 while True:
@@ -200,7 +199,7 @@ def upload_file(conn, localpath, remotepath, chunk_size=STREAM_CHUNK):
200 rf.write(buf)199 rf.write(buf)
201200
202201
203def upload_dir(conn, localpath, remotepath, filter=None, chunk_size=STREAM_CHUNK):202def upload_dir(conn, localpath, remotepath, filter=None, chunk_size=16000):
204 if not conn.modules.os.path.isdir(remotepath):203 if not conn.modules.os.path.isdir(remotepath):
205 conn.modules.os.makedirs(remotepath)204 conn.modules.os.makedirs(remotepath)
206 for fn in os.listdir(localpath):205 for fn in os.listdir(localpath):
@@ -210,7 +209,7 @@ def upload_dir(conn, localpath, remotepath, filter=None, chunk_size=STREAM_CHUNK
210 upload(conn, lfn, rfn, filter=filter, ignore_invalid=True, chunk_size=chunk_size)209 upload(conn, lfn, rfn, filter=filter, ignore_invalid=True, chunk_size=chunk_size)
211210
212211
213def download(conn, remotepath, localpath, filter=None, ignore_invalid=False, chunk_size=STREAM_CHUNK):212def download(conn, remotepath, localpath, filter=None, ignore_invalid=False, chunk_size=16000):
214 """213 """
215 download a file or a directory to the given remote path214 download a file or a directory to the given remote path
216215
@@ -221,15 +220,15 @@ def download(conn, remotepath, localpath, filter=None, ignore_invalid=False, chu
221 :param chunk_size: the IO chunk size220 :param chunk_size: the IO chunk size
222 """221 """
223 if conn.modules.os.path.isdir(remotepath):222 if conn.modules.os.path.isdir(remotepath):
224 download_dir(conn, remotepath, localpath, filter, chunk_size)223 download_dir(conn, remotepath, localpath, filter)
225 elif conn.modules.os.path.isfile(remotepath):224 elif conn.modules.os.path.isfile(remotepath):
226 download_file(conn, remotepath, localpath, chunk_size)225 download_file(conn, remotepath, localpath, chunk_size)
227 else:226 else:
228 if not ignore_invalid:227 if not ignore_invalid:
229 raise ValueError("cannot download {!r}".format((remotepath)))228 raise ValueError("cannot download %r" % (remotepath,))
230229
231230
232def download_file(conn, remotepath, localpath, chunk_size=STREAM_CHUNK):231def download_file(conn, remotepath, localpath, chunk_size=16000):
233 with conn.builtin.open(remotepath, "rb") as rf:232 with conn.builtin.open(remotepath, "rb") as rf:
234 with open(localpath, "wb") as lf:233 with open(localpath, "wb") as lf:
235 while True:234 while True:
@@ -239,17 +238,17 @@ def download_file(conn, remotepath, localpath, chunk_size=STREAM_CHUNK):
239 lf.write(buf)238 lf.write(buf)
240239
241240
242def download_dir(conn, remotepath, localpath, filter=None, chunk_size=STREAM_CHUNK):241def download_dir(conn, remotepath, localpath, filter=None, chunk_size=16000):
243 if not os.path.isdir(localpath):242 if not os.path.isdir(localpath):
244 os.makedirs(localpath)243 os.makedirs(localpath)
245 for fn in conn.modules.os.listdir(remotepath):244 for fn in conn.modules.os.listdir(remotepath):
246 if not filter or filter(fn):245 if not filter or filter(fn):
247 rfn = conn.modules.os.path.join(remotepath, fn)246 rfn = conn.modules.os.path.join(remotepath, fn)
248 lfn = os.path.join(localpath, fn)247 lfn = os.path.join(localpath, fn)
249 download(conn, rfn, lfn, filter=filter, ignore_invalid=True, chunk_size=chunk_size)248 download(conn, rfn, lfn, filter=filter, ignore_invalid=True)
250249
251250
252def upload_package(conn, module, remotepath=None, chunk_size=STREAM_CHUNK):251def upload_package(conn, module, remotepath=None, chunk_size=16000):
253 """252 """
254 uploads a module or a package to the remote party253 uploads a module or a package to the remote party
255254
@@ -385,17 +384,12 @@ def teleport_function(conn, func, globals=None, def_=True):
385 import os384 import os
386 return (os.getpid() + y) * x385 return (os.getpid() + y) * x
387386
388 .. note:: While it is not forbidden to "teleport" functions across different Python
389 versions, it *may* result in errors due to Python bytecode differences. It is
390 recommended to ensure both the client and the server are of the same Python
391 version when using this function.
392
393 :param conn: the RPyC connection387 :param conn: the RPyC connection
394 :param func: the function object to be delivered to the other party388 :param func: the function object to be delivered to the other party
395 """389 """
396 if globals is None:390 if globals is None:
397 globals = conn.namespace391 globals = conn.namespace
398 from rpyc.utils.teleportation import export_function392 from plainbox.vendor.rpyc.utils.teleportation import export_function
399 exported = export_function(func)393 exported = export_function(func)
400 return conn.modules["rpyc.utils.teleportation"].import_function(394 return conn.modules["rpyc.utils.teleportation"].import_function(
401 exported, globals, def_)395 exported, globals, def_)
diff --git a/plainbox/vendor/rpyc/utils/factory.py b/plainbox/vendor/rpyc/utils/factory.py
index aa59a7e..23acc1f 100644
--- a/plainbox/vendor/rpyc/utils/factory.py
+++ b/plainbox/vendor/rpyc/utils/factory.py
@@ -5,7 +5,7 @@ cases)
5from __future__ import with_statement5from __future__ import with_statement
6import socket6import socket
7from contextlib import closing7from contextlib import closing
8from functools import partial8
9import threading9import threading
10try:10try:
11 from thread import interrupt_main11 from thread import interrupt_main
@@ -19,7 +19,7 @@ except ImportError:
1919
20from plainbox.vendor.rpyc.core.channel import Channel20from plainbox.vendor.rpyc.core.channel import Channel
21from plainbox.vendor.rpyc.core.stream import SocketStream, TunneledSocketStream, PipeStream21from plainbox.vendor.rpyc.core.stream import SocketStream, TunneledSocketStream, PipeStream
22from plainbox.vendor.rpyc.core.service import VoidService, MasterService, SlaveService22from plainbox.vendor.rpyc.core.service import VoidService
23from plainbox.vendor.rpyc.utils.registry import UDPRegistryClient23from plainbox.vendor.rpyc.utils.registry import UDPRegistryClient
24from plainbox.vendor.rpyc.lib import safe_import, spawn24from plainbox.vendor.rpyc.lib import safe_import, spawn
25ssl = safe_import("ssl")25ssl = safe_import("ssl")
@@ -29,10 +29,6 @@ class DiscoveryError(Exception):
29 pass29 pass
3030
3131
32class ForbiddenError(Exception):
33 pass
34
35
36# ------------------------------------------------------------------------------32# ------------------------------------------------------------------------------
37# API33# API
38# ------------------------------------------------------------------------------34# ------------------------------------------------------------------------------
@@ -220,26 +216,16 @@ def discover(service_name, host=None, registrar=None, timeout=2):
220 registrar = UDPRegistryClient(timeout=timeout)216 registrar = UDPRegistryClient(timeout=timeout)
221 addrs = registrar.discover(service_name)217 addrs = registrar.discover(service_name)
222 if not addrs:218 if not addrs:
223 raise DiscoveryError("no servers exposing {!r} were found".format((service_name)))219 raise DiscoveryError("no servers exposing %r were found" % (service_name,))
224 if host:220 if host:
225 ips = socket.gethostbyname_ex(host)[2]221 ips = socket.gethostbyname_ex(host)[2]
226 addrs = [(h, p) for h, p in addrs if h in ips]222 addrs = [(h, p) for h, p in addrs if h in ips]
227 if not addrs:223 if not addrs:
228 raise DiscoveryError("no servers exposing {} were found on {}".format((service_name), (host)))224 raise DiscoveryError("no servers exposing %r were found on %r" % (service_name, host))
229 return addrs225 return addrs
230226
231227
232def list_services(registrar=None, timeout=2):228def connect_by_service(service_name, host=None, service=VoidService, config={}):
233 services = ()
234 if registrar is None:
235 registrar = UDPRegistryClient(timeout=timeout)
236 services = registrar.list()
237 if services is None:
238 raise ForbiddenError("Registry doesn't allow listing")
239 return services
240
241
242def connect_by_service(service_name, host=None, registrar=None, timeout=2, service=VoidService, config={}):
243 """create a connection to an arbitrary server that exposes the requested service229 """create a connection to an arbitrary server that exposes the requested service
244230
245 :param service_name: the service to discover231 :param service_name: the service to discover
@@ -254,13 +240,13 @@ def connect_by_service(service_name, host=None, registrar=None, timeout=2, servi
254 # some of which could be dead. We iterate over the list returned and return the first240 # some of which could be dead. We iterate over the list returned and return the first
255 # one we could connect to. If none of the registered servers is responsive we re-throw241 # one we could connect to. If none of the registered servers is responsive we re-throw
256 # the exception242 # the exception
257 addrs = discover(service_name, host=host, registrar=registrar, timeout=timeout)243 addrs = discover(service_name, host=host)
258 for host, port in addrs:244 for host, port in addrs:
259 try:245 try:
260 return connect(host, port, service, config=config)246 return connect(host, port, service, config=config)
261 except socket.error:247 except socket.error:
262 pass248 pass
263 raise DiscoveryError("All services are down: {}".format((addrs)))249 raise DiscoveryError("All services are down: %s" % (addrs,))
264250
265251
266def connect_subproc(args, service=VoidService, config={}):252def connect_subproc(args, service=VoidService, config={}):
@@ -278,27 +264,6 @@ def connect_subproc(args, service=VoidService, config={}):
278 return conn264 return conn
279265
280266
281def _server(listener, remote_service, remote_config, args=None):
282 try:
283 with closing(listener):
284 client = listener.accept()[0]
285 conn = connect_stream(SocketStream(client), service=remote_service, config=remote_config)
286 if isinstance(args, dict):
287 _oldstyle = (MasterService, SlaveService)
288 is_newstyle = isinstance(remote_service, type) and not issubclass(remote_service, _oldstyle)
289 is_newstyle |= not isinstance(remote_service, type) and not isinstance(remote_service, _oldstyle)
290 is_voidservice = isinstance(remote_service, type) and issubclass(remote_service, VoidService)
291 is_voidservice |= not isinstance(remote_service, type) and isinstance(remote_service, VoidService)
292 if is_newstyle and not is_voidservice:
293 conn._local_root.exposed_namespace.update(args)
294 elif not is_voidservice:
295 conn._local_root.namespace.update(args)
296
297 conn.serve_all()
298 except KeyboardInterrupt:
299 interrupt_main()
300
301
302def connect_thread(service=VoidService, config={}, remote_service=VoidService, remote_config={}):267def connect_thread(service=VoidService, config={}, remote_service=VoidService, remote_config={}):
303 """starts an rpyc server on a new thread, bound to an arbitrary port,268 """starts an rpyc server on a new thread, bound to an arbitrary port,
304 and connects to it over a socket.269 and connects to it over a socket.
@@ -311,8 +276,18 @@ def connect_thread(service=VoidService, config={}, remote_service=VoidService, r
311 listener = socket.socket()276 listener = socket.socket()
312 listener.bind(("localhost", 0))277 listener.bind(("localhost", 0))
313 listener.listen(1)278 listener.listen(1)
314 remote_server = partial(_server, listener, remote_service, remote_config)279
315 spawn(remote_server)280 def server(listener=listener):
281 with closing(listener):
282 client = listener.accept()[0]
283 conn = connect_stream(SocketStream(client), service=remote_service,
284 config=remote_config)
285 try:
286 conn.serve_all()
287 except KeyboardInterrupt:
288 interrupt_main()
289
290 spawn(server)
316 host, port = listener.getsockname()291 host, port = listener.getsockname()
317 return connect(host, port, service=service, config=config)292 return connect(host, port, service=service, config=config)
318293
@@ -336,8 +311,19 @@ def connect_multiprocess(service=VoidService, config={}, remote_service=VoidServ
336 listener = socket.socket()311 listener = socket.socket()
337 listener.bind(("localhost", 0))312 listener.bind(("localhost", 0))
338 listener.listen(1)313 listener.listen(1)
339 remote_server = partial(_server, listener, remote_service, remote_config, args)314
340 t = Process(target=remote_server)315 def server(listener=listener, args=args):
316 with closing(listener):
317 client = listener.accept()[0]
318 conn = connect_stream(SocketStream(client), service=remote_service, config=remote_config)
319 try:
320 for k in args:
321 conn._local_root.exposed_namespace[k] = args[k]
322 conn.serve_all()
323 except KeyboardInterrupt:
324 interrupt_main()
325
326 t = Process(target=server)
341 t.start()327 t.start()
342 host, port = listener.getsockname()328 host, port = listener.getsockname()
343 return connect(host, port, service=service, config=config)329 return connect(host, port, service=service, config=config)
diff --git a/plainbox/vendor/rpyc/utils/helpers.py b/plainbox/vendor/rpyc/utils/helpers.py
index 67be083..521e226 100644
--- a/plainbox/vendor/rpyc/utils/helpers.py
+++ b/plainbox/vendor/rpyc/utils/helpers.py
@@ -34,7 +34,7 @@ def buffiter(obj, chunk=10, max_chunk=1000, factor=2):
34 print id, name, dob34 print id, name, dob
35 """35 """
36 if factor < 1:36 if factor < 1:
37 raise ValueError("factor must be >= 1, got {!r}".format((factor)))37 raise ValueError("factor must be >= 1, got %r" % (factor,))
38 it = iter(obj)38 it = iter(obj)
39 count = chunk39 count = chunk
40 while True:40 while True:
@@ -102,7 +102,7 @@ class _Async(object):
102 return asyncreq(self.proxy, HANDLE_CALL, args, tuple(kwargs.items()))102 return asyncreq(self.proxy, HANDLE_CALL, args, tuple(kwargs.items()))
103103
104 def __repr__(self):104 def __repr__(self):
105 return "async_({!r})".format((self.proxy))105 return "async_(%r)" % (self.proxy,)
106106
107107
108_async_proxies_cache = WeakValueDict()108_async_proxies_cache = WeakValueDict()
@@ -145,9 +145,9 @@ def async_(proxy):
145 if pid in _async_proxies_cache:145 if pid in _async_proxies_cache:
146 return _async_proxies_cache[pid]146 return _async_proxies_cache[pid]
147 if not hasattr(proxy, "____conn__") or not hasattr(proxy, "____id_pack__"):147 if not hasattr(proxy, "____conn__") or not hasattr(proxy, "____id_pack__"):
148 raise TypeError("'proxy' must be a Netref: {!r}".format((proxy)))148 raise TypeError("'proxy' must be a Netref: %r", (proxy,))
149 if not callable(proxy):149 if not callable(proxy):
150 raise TypeError("'proxy' must be callable: {!r}".format((proxy)))150 raise TypeError("'proxy' must be callable: %r" % (proxy,))
151 caller = _Async(proxy)151 caller = _Async(proxy)
152 _async_proxies_cache[id(caller)] = _async_proxies_cache[pid] = caller152 _async_proxies_cache[id(caller)] = _async_proxies_cache[pid] = caller
153 return caller153 return caller
@@ -186,7 +186,7 @@ class timed(object):
186 return res186 return res
187187
188 def __repr__(self):188 def __repr__(self):
189 return "timed({!r}, {!r})".format((self.proxy.proxy), (self.timeout))189 return "timed(%r, %r)" % (self.proxy.proxy, self.timeout)
190190
191191
192class BgServingThread(object):192class BgServingThread(object):
diff --git a/plainbox/vendor/rpyc/utils/registry.py b/plainbox/vendor/rpyc/utils/registry.py
index b9a58ff..315d9f3 100644
--- a/plainbox/vendor/rpyc/utils/registry.py
+++ b/plainbox/vendor/rpyc/utils/registry.py
@@ -1,8 +1,13 @@
1"""1"""
2RPyC 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`.2RPyC **registry server** implementation. The registry is much like
3`Avahi <http://en.wikipedia.org/wiki/Avahi_(software)>`_ or
4`Bonjour <http://en.wikipedia.org/wiki/Bonjour_(software)>`_, but tailored to
5the needs of RPyC. Also, neither of them supports (or supported) Windows,
6and Bonjour has a restrictive license. Moreover, they are too "powerful" for
7what RPyC needed and required too complex a setup.
38
4Service registries such as `Avahi <http://en.wikipedia.org/wiki/Avahi_(software)>`_ and9If anyone wants to implement the RPyC registry using Avahi, Bonjour, or any
5`Bonjour <http://en.wikipedia.org/wiki/Bonjour_(software)>`_ are alternatives to the RPyC Registry Server. These alternatives do no support Windows and have more restrictive licensing.10other zeroconf implementation -- I'll be happy to include them.
611
7Refer to :file:`rpyc/scripts/rpyc_registry.py` for more info.12Refer to :file:`rpyc/scripts/rpyc_registry.py` for more info.
8"""13"""
@@ -26,7 +31,7 @@ REGISTRY_PORT = 18811
26class RegistryServer(object):31class RegistryServer(object):
27 """Base registry server"""32 """Base registry server"""
2833
29 def __init__(self, listenersock, pruning_timeout=None, logger=None, allow_listing=False):34 def __init__(self, listenersock, pruning_timeout=None, logger=None):
30 self.sock = listenersock35 self.sock = listenersock
31 self.port = self.sock.getsockname()[1]36 self.port = self.sock.getsockname()[1]
32 self.active = False37 self.active = False
@@ -36,7 +41,6 @@ class RegistryServer(object):
36 self.pruning_timeout = pruning_timeout41 self.pruning_timeout = pruning_timeout
37 if logger is None:42 if logger is None:
38 logger = self._get_logger()43 logger = self._get_logger()
39 self.allow_listing = allow_listing
40 self.logger = logger44 self.logger = logger
4145
42 def _get_logger(self):46 def _get_logger(self):
@@ -75,7 +79,7 @@ class RegistryServer(object):
75 def cmd_query(self, host, name):79 def cmd_query(self, host, name):
76 """implementation of the ``query`` command"""80 """implementation of the ``query`` command"""
77 name = name.upper()81 name = name.upper()
78 self.logger.debug("querying for {!r}".format((name)))82 self.logger.debug("querying for %r", name)
79 if name not in self.services:83 if name not in self.services:
80 self.logger.debug("no such service")84 self.logger.debug("no such service")
81 return ()85 return ()
@@ -85,35 +89,24 @@ class RegistryServer(object):
85 servers = []89 servers = []
86 for addrinfo, t in all_servers:90 for addrinfo, t in all_servers:
87 if t < oldest:91 if t < oldest:
88 self.logger.debug("discarding stale {}:{}".format((addrinfo[0]), (addrinfo[1])))92 self.logger.debug("discarding stale %s:%s", *addrinfo)
89 self._remove_service(name, addrinfo)93 self._remove_service(name, addrinfo)
90 else:94 else:
91 servers.append(addrinfo)95 servers.append(addrinfo)
9296
93 self.logger.debug("replying with {!r}".format((servers)))97 self.logger.debug("replying with %r", servers)
94 return tuple(servers)98 return tuple(servers)
9599
96 def cmd_list(self, host):
97 """implementation for the ``list`` command"""
98 self.logger.debug("querying for services list:")
99 if not self.allow_listing:
100 self.logger.debug("listing is disabled")
101 return None
102 services = tuple(self.services.keys())
103 self.logger.debug("replying with {}".format((services)))
104
105 return services
106
107 def cmd_register(self, host, names, port):100 def cmd_register(self, host, names, port):
108 """implementation of the ``register`` command"""101 """implementation of the ``register`` command"""
109 self.logger.debug("registering {}:{} as {}".format((host), (port), (', '.join(names))))102 self.logger.debug("registering %s:%s as %s", host, port, ", ".join(names))
110 for name in names:103 for name in names:
111 self._add_service(name.upper(), (host, port))104 self._add_service(name.upper(), (host, port))
112 return "OK"105 return "OK"
113106
114 def cmd_unregister(self, host, port):107 def cmd_unregister(self, host, port):
115 """implementation of the ``unregister`` command"""108 """implementation of the ``unregister`` command"""
116 self.logger.debug("unregistering {}:{}".format((host), (port)))109 self.logger.debug("unregistering %s:%s", host, port)
117 for name in list(self.services.keys()):110 for name in list(self.services.keys()):
118 self._remove_service(name, (host, port))111 self._remove_service(name, (host, port))
119 return "OK"112 return "OK"
@@ -135,11 +128,11 @@ class RegistryServer(object):
135 except Exception:128 except Exception:
136 continue129 continue
137 if magic != "RPYC":130 if magic != "RPYC":
138 self.logger.warn("invalid magic: {!r}".format((magic)))131 self.logger.warn("invalid magic: %r", magic)
139 continue132 continue
140 cmdfunc = getattr(self, "cmd_{}".format((cmd.lower())), None)133 cmdfunc = getattr(self, "cmd_%s" % (cmd.lower(),), None)
141 if not cmdfunc:134 if not cmdfunc:
142 self.logger.warn("unknown command: {!r}".format((cmd)))135 self.logger.warn("unknown command: %r", cmd)
143 continue136 continue
144137
145 try:138 try:
@@ -155,8 +148,7 @@ class RegistryServer(object):
155 raise ValueError("server is already running")148 raise ValueError("server is already running")
156 if self.sock is None:149 if self.sock is None:
157 raise ValueError("object disposed")150 raise ValueError("object disposed")
158 addrinfo = self.sock.getsockname()[:2]151 self.logger.debug("server started on %s:%s", *self.sock.getsockname()[:2])
159 self.logger.debug("server started on {}:{}".format((addrinfo[0]), (addrinfo[1])))
160 try:152 try:
161 self.active = True153 self.active = True
162 self._work()154 self._work()
@@ -182,17 +174,17 @@ class UDPRegistryServer(RegistryServer):
182174
183 TIMEOUT = 1.0175 TIMEOUT = 1.0
184176
185 def __init__(self, host="0.0.0.0", port=REGISTRY_PORT, pruning_timeout=None, logger=None, allow_listing=False):177 def __init__(self, host="0.0.0.0", port=REGISTRY_PORT, pruning_timeout=None, logger=None):
186 family, socktype, proto, _, sockaddr = socket.getaddrinfo(host, port, 0,178 family, socktype, proto, _, sockaddr = socket.getaddrinfo(host, port, 0,
187 socket.SOCK_DGRAM)[0]179 socket.SOCK_DGRAM)[0]
188 sock = socket.socket(family, socktype, proto)180 sock = socket.socket(family, socktype, proto)
189 sock.bind(sockaddr)181 sock.bind(sockaddr)
190 sock.settimeout(self.TIMEOUT)182 sock.settimeout(self.TIMEOUT)
191 RegistryServer.__init__(self, sock, pruning_timeout=pruning_timeout,183 RegistryServer.__init__(self, sock, pruning_timeout=pruning_timeout,
192 logger=logger, allow_listing=allow_listing)184 logger=logger)
193185
194 def _get_logger(self):186 def _get_logger(self):
195 return logging.getLogger("REGSRV/UDP/{}".format((self.port)))187 return logging.getLogger("REGSRV/UDP/%d" % (self.port,))
196188
197 def _recv(self):189 def _recv(self):
198 return self.sock.recvfrom(MAX_DGRAM_SIZE)190 return self.sock.recvfrom(MAX_DGRAM_SIZE)
@@ -212,9 +204,10 @@ class TCPRegistryServer(RegistryServer):
212 TIMEOUT = 3.0204 TIMEOUT = 3.0
213205
214 def __init__(self, host="0.0.0.0", port=REGISTRY_PORT, pruning_timeout=None,206 def __init__(self, host="0.0.0.0", port=REGISTRY_PORT, pruning_timeout=None,
215 logger=None, reuse_addr=True, allow_listing=False):207 logger=None, reuse_addr=True):
216208
217 family, socktype, proto, _, sockaddr = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)[0]209 family, socktype, proto, _, sockaddr = socket.getaddrinfo(host, port, 0,
210 socket.SOCK_STREAM)[0]
218 sock = socket.socket(family, socktype, proto)211 sock = socket.socket(family, socktype, proto)
219 if reuse_addr and sys.platform != "win32":212 if reuse_addr and sys.platform != "win32":
220 # warning: reuseaddr is not what you expect on windows!213 # warning: reuseaddr is not what you expect on windows!
@@ -223,11 +216,11 @@ class TCPRegistryServer(RegistryServer):
223 sock.listen(10)216 sock.listen(10)
224 sock.settimeout(self.TIMEOUT)217 sock.settimeout(self.TIMEOUT)
225 RegistryServer.__init__(self, sock, pruning_timeout=pruning_timeout,218 RegistryServer.__init__(self, sock, pruning_timeout=pruning_timeout,
226 logger=logger, allow_listing=allow_listing)219 logger=logger)
227 self._connected_sockets = {}220 self._connected_sockets = {}
228221
229 def _get_logger(self):222 def _get_logger(self):
230 return logging.getLogger("REGSRV/TCP/{}".format((self.port)))223 return logging.getLogger("REGSRV/TCP/%d" % (self.port,))
231224
232 def _recv(self):225 def _recv(self):
233 sock2, _ = self.sock.accept()226 sock2, _ = self.sock.accept()
@@ -274,13 +267,6 @@ class RegistryClient(object):
274 """267 """
275 raise NotImplementedError()268 raise NotImplementedError()
276269
277 def list(self):
278 """
279 Send a query for the full lists of exposed servers
280 :returns: a list of `` service_name ``
281 """
282 raise NotImplementedError()
283
284 def register(self, aliases, port):270 def register(self, aliases, port):
285 """Registers the given service aliases with the given TCP port. This271 """Registers the given service aliases with the given TCP port. This
286 API is intended to be called only by an RPyC server.272 API is intended to be called only by an RPyC server.
@@ -307,7 +293,6 @@ class UDPRegistryClient(RegistryClient):
307 Example::293 Example::
308294
309 registrar = UDPRegistryClient()295 registrar = UDPRegistryClient()
310 list_of_services = registrar.list()
311 list_of_servers = registrar.discover("foo")296 list_of_servers = registrar.discover("foo")
312297
313 .. note::298 .. note::
@@ -349,26 +334,8 @@ class UDPRegistryClient(RegistryClient):
349 servers = brine.load(data)334 servers = brine.load(data)
350 return servers335 return servers
351336
352 def list(self):
353 sock = socket.socket(self.sock_family, socket.SOCK_DGRAM)
354
355 with closing(sock):
356 if self.bcast:
357 sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, True)
358 data = brine.dump(("RPYC", "LIST", ()))
359 sock.sendto(data, (self.ip, self.port))
360 sock.settimeout(self.timeout)
361
362 try:
363 data, _ = sock.recvfrom(MAX_DGRAM_SIZE)
364 except (socket.error, socket.timeout):
365 services = ()
366 else:
367 services = brine.load(data)
368 return services
369
370 def register(self, aliases, port, interface=""):337 def register(self, aliases, port, interface=""):
371 self.logger.info("registering on {}:{}".format((self.ip), (self.port)))338 self.logger.info("registering on %s:%s", self.ip, self.port)
372 sock = socket.socket(self.sock_family, socket.SOCK_DGRAM)339 sock = socket.socket(self.sock_family, socket.SOCK_DGRAM)
373 with closing(sock):340 with closing(sock):
374 sock.bind((interface, 0))341 sock.bind((interface, 0))
@@ -393,14 +360,14 @@ class UDPRegistryClient(RegistryClient):
393 except Exception:360 except Exception:
394 continue361 continue
395 if reply == "OK":362 if reply == "OK":
396 self.logger.info("registry {}:{} acknowledged".format((rip), (rport)))363 self.logger.info("registry %s:%s acknowledged", rip, rport)
397 return True364 return True
398 else:365 else:
399 self.logger.warn("no registry acknowledged")366 self.logger.warn("no registry acknowledged")
400 return False367 return False
401368
402 def unregister(self, port):369 def unregister(self, port):
403 self.logger.info("unregistering from {}:{}".format((self.ip), (self.port)))370 self.logger.info("unregistering from %s:%s", self.ip, self.port)
404 sock = socket.socket(self.sock_family, socket.SOCK_DGRAM)371 sock = socket.socket(self.sock_family, socket.SOCK_DGRAM)
405 with closing(sock):372 with closing(sock):
406 if self.bcast:373 if self.bcast:
@@ -416,7 +383,6 @@ class TCPRegistryClient(RegistryClient):
416 Example::383 Example::
417384
418 registrar = TCPRegistryClient("localhost")385 registrar = TCPRegistryClient("localhost")
419 list_of_services = registrar.list()
420 list_of_servers = registrar.discover("foo")386 list_of_servers = registrar.discover("foo")
421387
422 .. note::388 .. note::
@@ -446,24 +412,8 @@ class TCPRegistryClient(RegistryClient):
446 servers = brine.load(data)412 servers = brine.load(data)
447 return servers413 return servers
448414
449 def list(self):
450 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
451 with closing(sock):
452 sock.settimeout(self.timeout)
453 data = brine.dump(("RPYC", "LIST", ()))
454 sock.connect((self.ip, self.port))
455 sock.send(data)
456
457 try:
458 data = sock.recv(MAX_DGRAM_SIZE)
459 except (socket.error, socket.timeout):
460 servers = ()
461 else:
462 servers = brine.load(data)
463 return servers
464
465 def register(self, aliases, port, interface=""):415 def register(self, aliases, port, interface=""):
466 self.logger.info("registering on {}:{}".format((self.ip), (self.port)))416 self.logger.info("registering on %s:%s", self.ip, self.port)
467 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)417 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
468 with closing(sock):418 with closing(sock):
469 sock.bind((interface, 0))419 sock.bind((interface, 0))
@@ -486,12 +436,12 @@ class TCPRegistryClient(RegistryClient):
486 self.logger.warn("received corrupted data from registry")436 self.logger.warn("received corrupted data from registry")
487 return False437 return False
488 if reply == "OK":438 if reply == "OK":
489 self.logger.info("registry {}:{} acknowledged".format((self.ip), (self.port)))439 self.logger.info("registry %s:%s acknowledged", self.ip, self.port)
490440
491 return True441 return True
492442
493 def unregister(self, port):443 def unregister(self, port):
494 self.logger.info("unregistering from {}:{}".format((self.ip), (self.port)))444 self.logger.info("unregistering from %s:%s", self.ip, self.port)
495 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)445 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
496 with closing(sock):446 with closing(sock):
497 sock.settimeout(self.timeout)447 sock.settimeout(self.timeout)
diff --git a/plainbox/vendor/rpyc/utils/server.py b/plainbox/vendor/rpyc/utils/server.py
index 2e73ecb..d50e118 100644
--- a/plainbox/vendor/rpyc/utils/server.py
+++ b/plainbox/vendor/rpyc/utils/server.py
@@ -26,8 +26,8 @@ class Server(object):
26 """Base server implementation26 """Base server implementation
2727
28 :param service: the :class:`~rpyc.core.service.Service` to expose28 :param service: the :class:`~rpyc.core.service.Service` to expose
29 :param hostname: the host to bind to. By default, the 'wildcard address' is used to listen on all interfaces.29 :param hostname: the host to bind to. Default is IPADDR_ANY, but you may
30 if not properly secured, the server can receive traffic from unintended or even malicious sources.30 want to restrict it only to ``localhost`` in some setups
31 :param ipv6: whether to create an IPv6 or IPv4 socket. The default is IPv431 :param ipv6: whether to create an IPv6 or IPv4 socket. The default is IPv4
32 :param port: the TCP port to bind to32 :param port: the TCP port to bind to
33 :param backlog: the socket's backlog (passed to ``listen()``)33 :param backlog: the socket's backlog (passed to ``listen()``)
@@ -47,9 +47,9 @@ class Server(object):
47 on embedded platforms with limited battery)47 on embedded platforms with limited battery)
48 """48 """
4949
50 def __init__(self, service, hostname=None, ipv6=False, port=0,50 def __init__(self, service, hostname="", ipv6=False, port=0,
51 backlog=socket.SOMAXCONN, reuse_addr=True, authenticator=None, registrar=None,51 backlog=socket.SOMAXCONN, reuse_addr=True, authenticator=None, registrar=None,
52 auto_register=None, protocol_config=None, logger=None, listener_timeout=0.5,52 auto_register=None, protocol_config={}, logger=None, listener_timeout=0.5,
53 socket_path=None):53 socket_path=None):
54 self.active = False54 self.active = False
55 self._closed = False55 self._closed = False
@@ -60,15 +60,11 @@ class Server(object):
60 self.auto_register = bool(registrar)60 self.auto_register = bool(registrar)
61 else:61 else:
62 self.auto_register = auto_register62 self.auto_register = auto_register
63
64 if protocol_config is None:
65 protocol_config = {}
66
67 self.protocol_config = protocol_config63 self.protocol_config = protocol_config
68 self.clients = set()64 self.clients = set()
6965
70 if socket_path is not None:66 if socket_path is not None:
71 if hostname is not None or port != 0 or ipv6 is not False:67 if hostname != "" or port != 0 or ipv6 is not False:
72 raise ValueError("socket_path is mutually exclusive with: hostname, port, ipv6")68 raise ValueError("socket_path is mutually exclusive with: hostname, port, ipv6")
73 self.listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)69 self.listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
74 self.listener.bind(socket_path)70 self.listener.bind(socket_path)
@@ -76,18 +72,20 @@ class Server(object):
76 self.host, self.port = "", socket_path72 self.host, self.port = "", socket_path
77 else:73 else:
78 if ipv6:74 if ipv6:
79 family = socket.AF_INET675 if hostname == "localhost" and sys.platform != "win32":
76 # on windows, you should bind to localhost even for ipv6
77 hostname = "localhost6"
78 self.listener = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
80 else:79 else:
81 family = socket.AF_INET80 self.listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
82 self.listener = socket.socket(family, socket.SOCK_STREAM)
83 address = socket.getaddrinfo(hostname, port, family=family, type=socket.SOCK_STREAM, proto=socket.IPPROTO_TCP, flags=socket.AI_PASSIVE)[0][-1]
8481
85 if reuse_addr and sys.platform != "win32":82 if reuse_addr and sys.platform != "win32":
86 # warning: reuseaddr is not what you'd expect on windows!83 # warning: reuseaddr is not what you'd expect on windows!
87 # it allows you to bind an already bound port, resulting in84 # it allows you to bind an already bound port, resulting in
88 # "unexpected behavior" (quoting MSDN)85 # "unexpected behavior" (quoting MSDN)
89 self.listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)86 self.listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
90 self.listener.bind(address)87
88 self.listener.bind((hostname, port))
91 self.listener.settimeout(listener_timeout)89 self.listener.settimeout(listener_timeout)
9290
93 # hack for IPv6 (the tuple can be longer than 2)91 # hack for IPv6 (the tuple can be longer than 2)
@@ -95,7 +93,7 @@ class Server(object):
95 self.host, self.port = sockname[0], sockname[1]93 self.host, self.port = sockname[0], sockname[1]
9694
97 if logger is None:95 if logger is None:
98 logger = logging.getLogger("{}/{}".format((self.service.get_service_name()), (self.port)))96 logger = logging.getLogger("%s/%s" % (self.service.get_service_name(), self.port))
99 self.logger = logger97 self.logger = logger
100 if "logger" not in self.protocol_config:98 if "logger" not in self.protocol_config:
101 self.protocol_config["logger"] = self.logger99 self.protocol_config["logger"] = self.logger
@@ -153,7 +151,7 @@ class Server(object):
153 return151 return
154152
155 sock.setblocking(True)153 sock.setblocking(True)
156 self.logger.info("accepted {} with fd {}".format((addrinfo), (sock.fileno())))154 self.logger.info("accepted %s with fd %s", addrinfo, sock.fileno())
157 self.clients.add(sock)155 self.clients.add(sock)
158 self._accept_method(sock)156 self._accept_method(sock)
159157
@@ -171,10 +169,10 @@ class Server(object):
171 try:169 try:
172 sock2, credentials = self.authenticator(sock)170 sock2, credentials = self.authenticator(sock)
173 except AuthenticationError:171 except AuthenticationError:
174 self.logger.info("{} failed to authenticate... rejecting connection".format((addrinfo)))172 self.logger.info("%s failed to authenticate, rejecting connection", addrinfo)
175 return173 return
176 else:174 else:
177 self.logger.info("{} authenticated successfully".format((addrinfo)))175 self.logger.info("%s authenticated successfully", addrinfo)
178 else:176 else:
179 credentials = None177 credentials = None
180 sock2 = sock178 sock2 = sock
@@ -194,16 +192,16 @@ class Server(object):
194 def _serve_client(self, sock, credentials):192 def _serve_client(self, sock, credentials):
195 addrinfo = sock.getpeername()193 addrinfo = sock.getpeername()
196 if credentials:194 if credentials:
197 self.logger.info("welcome {} ({!r})".format((addrinfo), (credentials)))195 self.logger.info("welcome %s (%r)", addrinfo, credentials)
198 else:196 else:
199 self.logger.info("welcome {}".format((addrinfo)))197 self.logger.info("welcome %s", addrinfo)
200 try:198 try:
201 config = dict(self.protocol_config, credentials=credentials,199 config = dict(self.protocol_config, credentials=credentials,
202 endpoints=(sock.getsockname(), addrinfo), logger=self.logger)200 endpoints=(sock.getsockname(), addrinfo), logger=self.logger)
203 conn = self.service._connect(Channel(SocketStream(sock)), config)201 conn = self.service._connect(Channel(SocketStream(sock)), config)
204 self._handle_connection(conn)202 self._handle_connection(conn)
205 finally:203 finally:
206 self.logger.info("goodbye {}".format((addrinfo)))204 self.logger.info("goodbye %s", addrinfo)
207205
208 def _handle_connection(self, conn):206 def _handle_connection(self, conn):
209 """This methoed should implement the server's logic."""207 """This methoed should implement the server's logic."""
@@ -212,7 +210,7 @@ class Server(object):
212 def _bg_register(self):210 def _bg_register(self):
213 interval = self.registrar.REREGISTER_INTERVAL211 interval = self.registrar.REREGISTER_INTERVAL
214 self.logger.info("started background auto-register thread "212 self.logger.info("started background auto-register thread "
215 "(interval = {})".format((interval)))213 "(interval = %s)", interval)
216 tnext = 0214 tnext = 0
217 try:215 try:
218 while self.active:216 while self.active:
@@ -247,11 +245,12 @@ class Server(object):
247 # Note that for AF_UNIX the following won't work (but we are safe245 # Note that for AF_UNIX the following won't work (but we are safe
248 # since we already saved the socket_path into self.port):246 # since we already saved the socket_path into self.port):
249 self.port = self.listener.getsockname()[1]247 self.port = self.listener.getsockname()[1]
250 self.logger.info("server started on [{}]:{}".format((self.host), (self.port)))248 self.logger.info("server started on [%s]:%s", self.host, self.port)
251 self.active = True249 self.active = True
252250
253 def _register(self):251 def _register(self):
254 if self.auto_register:252 if self.auto_register:
253 self.auto_register = False
255 spawn(self._bg_register)254 spawn(self._bg_register)
256255
257 def start(self):256 def start(self):
@@ -341,7 +340,7 @@ class ThreadPoolServer(Server):
341 self.workers = []340 self.workers = []
342 for i in range(self.nbthreads):341 for i in range(self.nbthreads):
343 t = spawn(self._serve_clients)342 t = spawn(self._serve_clients)
344 t.setName("Worker{}".format((i)))343 t.setName('Worker%i' % i)
345 self.workers.append(t)344 self.workers.append(t)
346 # setup a thread for polling inactive connections345 # setup a thread for polling inactive connections
347 self.polling_thread = spawn(self._poll_inactive_clients)346 self.polling_thread = spawn(self._poll_inactive_clients)
@@ -382,7 +381,7 @@ class ThreadPoolServer(Server):
382 pass381 pass
383382
384 # close connection383 # close connection
385 self.logger.info("Closing connection for fd {}".format((fd)))384 self.logger.info("Closing connection for fd %d", fd)
386 if conn:385 if conn:
387 conn.close()386 conn.close()
388387
@@ -420,7 +419,7 @@ class ThreadPoolServer(Server):
420 except Exception:419 except Exception:
421 ex = sys.exc_info()[1]420 ex = sys.exc_info()[1]
422 # "Caught exception in Worker thread" message421 # "Caught exception in Worker thread" message
423 self.logger.warning("Failed to poll clients, caught exception : {}".format((ex)))422 self.logger.warning("Failed to poll clients, caught exception : %s", str(ex))
424 # wait a bit so that we do not loop too fast in case of error423 # wait a bit so that we do not loop too fast in case of error
425 time.sleep(0.2)424 time.sleep(0.2)
426425
@@ -468,7 +467,7 @@ class ThreadPoolServer(Server):
468 time.sleep(0.2)467 time.sleep(0.2)
469468
470 def _authenticate_and_build_connection(self, sock):469 def _authenticate_and_build_connection(self, sock):
471 '''Authenticate a client and if it succeeds, wraps the socket in a connection object.470 '''Authenticate a client and if it succees, wraps the socket in a connection object.
472 Note that this code is cut and paste from the rpyc internals and may have to be471 Note that this code is cut and paste from the rpyc internals and may have to be
473 changed if rpyc evolves'''472 changed if rpyc evolves'''
474 # authenticate473 # authenticate
@@ -477,27 +476,27 @@ class ThreadPoolServer(Server):
477 else:476 else:
478 credentials = None477 credentials = None
479 # build a connection478 # build a connection
480 addrinfo = sock.getpeername()479 h, p = sock.getpeername()
481 config = dict(self.protocol_config, credentials=credentials, connid="{}".format(addrinfo),480 config = dict(self.protocol_config, credentials=credentials, connid="%s:%d" % (h, p),
482 endpoints=(sock.getsockname(), addrinfo))481 endpoints=(sock.getsockname(), (h, p)))
483 return sock, self.service._connect(Channel(SocketStream(sock)), config)482 return sock, self.service._connect(Channel(SocketStream(sock)), config)
484483
485 def _accept_method(self, sock):484 def _accept_method(self, sock):
486 '''Implementation of the accept method : only pushes the work to the internal queue.485 '''Implementation of the accept method : only pushes the work to the internal queue.
487 In case the queue is full, raises an AsynResultTimeout error'''486 In case the queue is full, raises an AsynResultTimeout error'''
488 try:487 try:
489 addrinfo = None488 h, p = None, None
490 # authenticate and build connection object489 # authenticate and build connection object
491 sock, conn = self._authenticate_and_build_connection(sock)490 sock, conn = self._authenticate_and_build_connection(sock)
492 # put the connection in the active queue491 # put the connection in the active queue
493 addrinfo = sock.getpeername()492 h, p = sock.getpeername()
494 fd = conn.fileno()493 fd = conn.fileno()
495 self.logger.debug("Created connection to {addrinfo} with fd {fd}")494 self.logger.debug("Created connection to %s:%d with fd %d", h, p, fd)
496 self.fd_to_conn[fd] = conn495 self.fd_to_conn[fd] = conn
497 self._add_inactive_connection(fd)496 self._add_inactive_connection(fd)
498 self.clients.clear()497 self.clients.clear()
499 except Exception:498 except Exception:
500 err_msg = "Failed to serve client for {}, caught exception".format(addrinfo)499 err_msg = "Failed to serve client for {}:{}, caught exception".format(h, p)
501 self.logger.exception(err_msg)500 self.logger.exception(err_msg)
502 sock.close()501 sock.close()
503502
@@ -564,6 +563,7 @@ class GeventServer(Server):
564563
565 def _register(self):564 def _register(self):
566 if self.auto_register:565 if self.auto_register:
566 self.auto_register = False
567 gevent.spawn(self._bg_register)567 gevent.spawn(self._bg_register)
568568
569 def _accept_method(self, sock):569 def _accept_method(self, sock):
diff --git a/plainbox/vendor/rpyc/utils/teleportation.py b/plainbox/vendor/rpyc/utils/teleportation.py
index a06af70..451c93d 100644
--- a/plainbox/vendor/rpyc/utils/teleportation.py
+++ b/plainbox/vendor/rpyc/utils/teleportation.py
@@ -1,21 +1,48 @@
1import opcode1import opcode
22import sys
3from plainbox.vendor.rpyc.lib.compat import is_py_gte383try:
4 import __builtin__
5except ImportError:
6 import builtins as __builtin__ # noqa: F401
7from plainbox.vendor.rpyc.lib.compat import is_py_3k, is_py_gte38
4from types import CodeType, FunctionType8from types import CodeType, FunctionType
5from plainbox.vendor.rpyc.core import brine, netref9from plainbox.vendor.rpyc.core import brine
6from dis import _unpack_opargs10from plainbox.vendor.rpyc.core import netref
711
8CODEOBJ_MAGIC = "MAg1c J0hNNzo0hn ZqhuBP17LQk8"12CODEOBJ_MAGIC = "MAg1c J0hNNzo0hn ZqhuBP17LQk8"
913
1014
11# NOTE: dislike this kind of hacking on the level of implementation details,15# NOTE: dislike this kind of hacking on the level of implementation details,
12# should search for a more reliable/future-proof way:16# should search for a more reliable/future-proof way:
13CODE_HAVEARG_SIZE = 317CODE_HAVEARG_SIZE = 2 if sys.version_info >= (3, 6) else 3
18try:
19 from dis import _unpack_opargs
20except ImportError:
21 # COPIED from 3.5's `dis.py`, this should hopefully be correct for <=3.5:
22 def _unpack_opargs(code):
23 extended_arg = 0
24 n = len(code)
25 i = 0
26 while i < n:
27 op = code[i]
28 offset = i
29 i = i + 1
30 arg = None
31 if op >= opcode.HAVE_ARGUMENT:
32 arg = code[i] + code[i + 1] * 256 + extended_arg
33 extended_arg = 0
34 i = i + 2
35 if op == opcode.EXTENDED_ARG:
36 extended_arg = arg * 65536
37 yield (offset, op, arg)
1438
1539
16def decode_codeobj(codeobj):40def decode_codeobj(codeobj):
17 # adapted from dis.dis41 # adapted from dis.dis
18 codestr = codeobj.co_code42 if is_py_3k:
43 codestr = codeobj.co_code
44 else:
45 codestr = [ord(ch) for ch in codeobj.co_code]
19 free = None46 free = None
20 for i, op, oparg in _unpack_opargs(codestr):47 for i, op, oparg in _unpack_opargs(codestr):
21 opname = opcode.opname[op]48 opname = opcode.opname[op]
@@ -46,7 +73,7 @@ def _export_codeobj(cobj):
46 elif isinstance(const, CodeType):73 elif isinstance(const, CodeType):
47 consts2.append(_export_codeobj(const))74 consts2.append(_export_codeobj(const))
48 else:75 else:
49 raise TypeError(f"Cannot export a function with non-brinable constants: {const!r}")76 raise TypeError("Cannot export a function with non-brinable constants: %r" % (const,))
5077
51 if is_py_gte38:78 if is_py_gte38:
52 # Constructor was changed in 3.8 to support "advanced" programming styles79 # Constructor was changed in 3.8 to support "advanced" programming styles
@@ -54,41 +81,50 @@ def _export_codeobj(cobj):
54 cobj.co_stacksize, cobj.co_flags, cobj.co_code, tuple(consts2), cobj.co_names, cobj.co_varnames,81 cobj.co_stacksize, cobj.co_flags, cobj.co_code, tuple(consts2), cobj.co_names, cobj.co_varnames,
55 cobj.co_filename, cobj.co_name, cobj.co_firstlineno, cobj.co_lnotab, cobj.co_freevars,82 cobj.co_filename, cobj.co_name, cobj.co_firstlineno, cobj.co_lnotab, cobj.co_freevars,
56 cobj.co_cellvars)83 cobj.co_cellvars)
57 else:84 elif is_py_3k:
58 exported = (cobj.co_argcount, cobj.co_kwonlyargcount, cobj.co_nlocals, cobj.co_stacksize, cobj.co_flags,85 exported = (cobj.co_argcount, cobj.co_kwonlyargcount, cobj.co_nlocals, cobj.co_stacksize, cobj.co_flags,
59 cobj.co_code, tuple(consts2), cobj.co_names, cobj.co_varnames, cobj.co_filename,86 cobj.co_code, tuple(consts2), cobj.co_names, cobj.co_varnames, cobj.co_filename,
60 cobj.co_name, cobj.co_firstlineno, cobj.co_lnotab, cobj.co_freevars, cobj.co_cellvars)87 cobj.co_name, cobj.co_firstlineno, cobj.co_lnotab, cobj.co_freevars, cobj.co_cellvars)
88 else:
89 exported = (cobj.co_argcount, cobj.co_nlocals, cobj.co_stacksize, cobj.co_flags,
90 cobj.co_code, tuple(consts2), cobj.co_names, cobj.co_varnames, cobj.co_filename,
91 cobj.co_name, cobj.co_firstlineno, cobj.co_lnotab, cobj.co_freevars, cobj.co_cellvars)
92
61 assert brine.dumpable(exported)93 assert brine.dumpable(exported)
62 return (CODEOBJ_MAGIC, exported)94 return (CODEOBJ_MAGIC, exported)
6395
6496
65def export_function(func):97def export_function(func):
66 closure = func.__closure__98 if is_py_3k:
67 code = func.__code__99 func_closure = func.__closure__
68 defaults = func.__defaults__100 func_code = func.__code__
69 kwdefaults = func.__kwdefaults__101 func_defaults = func.__defaults__
70 if kwdefaults is not None:102 else:
71 kwdefaults = tuple(kwdefaults.items())103 func_closure = func.func_closure
72104 func_code = func.func_code
73 if closure:105 func_defaults = func.func_defaults
106
107 if func_closure:
74 raise TypeError("Cannot export a function closure")108 raise TypeError("Cannot export a function closure")
75 if not brine.dumpable(defaults):109 if not brine.dumpable(func_defaults):
76 raise TypeError("Cannot export a function with non-brinable defaults (__defaults__)")110 raise TypeError("Cannot export a function with non-brinable defaults (func_defaults)")
77 if not brine.dumpable(kwdefaults):
78 raise TypeError("Cannot export a function with non-brinable defaults (__kwdefaults__)")
79111
80 return func.__name__, func.__module__, defaults, kwdefaults, _export_codeobj(code)[1]112 return func.__name__, func.__module__, func_defaults, _export_codeobj(func_code)[1]
81113
82114
83def _import_codetup(codetup):115def _import_codetup(codetup):
84 # Handle tuples sent from 3.8 as well as 3 < version < 3.8.116 if is_py_3k:
85 if len(codetup) == 16:117 # Handle tuples sent from 3.8 as well as 3 < version < 3.8.
86 (argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames,118 if len(codetup) == 16:
87 filename, name, firstlineno, lnotab, freevars, cellvars) = codetup119 (argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames,
120 filename, name, firstlineno, lnotab, freevars, cellvars) = codetup
121 else:
122 (argcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames,
123 filename, name, firstlineno, lnotab, freevars, cellvars) = codetup
124 posonlyargcount = 0
88 else:125 else:
89 (argcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames,126 (argcount, nlocals, stacksize, flags, code, consts, names, varnames,
90 filename, name, firstlineno, lnotab, freevars, cellvars) = codetup127 filename, name, firstlineno, lnotab, freevars, cellvars) = codetup
91 posonlyargcount = 0
92128
93 consts2 = []129 consts2 = []
94 for const in consts:130 for const in consts:
@@ -100,14 +136,17 @@ def _import_codetup(codetup):
100 if is_py_gte38:136 if is_py_gte38:
101 codetup = (argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames,137 codetup = (argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames,
102 filename, name, firstlineno, lnotab, freevars, cellvars)138 filename, name, firstlineno, lnotab, freevars, cellvars)
103 else:139 elif is_py_3k:
104 codetup = (argcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, filename, name,140 codetup = (argcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, filename, name,
105 firstlineno, lnotab, freevars, cellvars)141 firstlineno, lnotab, freevars, cellvars)
142 else:
143 codetup = (argcount, nlocals, stacksize, flags, code, consts, names, varnames, filename, name, firstlineno,
144 lnotab, freevars, cellvars)
106 return CodeType(*codetup)145 return CodeType(*codetup)
107146
108147
109def import_function(functup, globals=None, def_=True):148def import_function(functup, globals=None, def_=True):
110 name, modname, defaults, kwdefaults, codetup = functup149 name, modname, defaults, codetup = functup
111 if globals is None:150 if globals is None:
112 try:151 try:
113 mod = __import__(modname, None, None, "*")152 mod = __import__(modname, None, None, "*")
@@ -116,13 +155,11 @@ def import_function(functup, globals=None, def_=True):
116 globals = mod.__dict__155 globals = mod.__dict__
117 # function globals must be real dicts, sadly:156 # function globals must be real dicts, sadly:
118 if isinstance(globals, netref.BaseNetref):157 if isinstance(globals, netref.BaseNetref):
119 from rpyc.utils.classic import obtain158 from plainbox.vendor.rpyc.utils.classic import obtain
120 globals = obtain(globals)159 globals = obtain(globals)
121 globals.setdefault('__builtins__', __builtins__)160 globals.setdefault('__builtins__', __builtins__)
122 codeobj = _import_codetup(codetup)161 codeobj = _import_codetup(codetup)
123 funcobj = FunctionType(codeobj, globals, name, defaults)162 funcobj = FunctionType(codeobj, globals, name, defaults)
124 if kwdefaults is not None:
125 funcobj.__kwdefaults__ = {t[0]: t[1] for t in kwdefaults}
126 if def_:163 if def_:
127 globals[name] = funcobj164 globals[name] = funcobj
128 return funcobj165 return funcobj
diff --git a/plainbox/vendor/rpyc/utils/zerodeploy.py b/plainbox/vendor/rpyc/utils/zerodeploy.py
index a246e64..bbc4922 100644
--- a/plainbox/vendor/rpyc/utils/zerodeploy.py
+++ b/plainbox/vendor/rpyc/utils/zerodeploy.py
@@ -55,7 +55,7 @@ $EXTRA_SETUP$
55t = ServerCls(SlaveService, hostname = "localhost", port = 0, reuse_addr = True, logger = logger)55t = ServerCls(SlaveService, hostname = "localhost", port = 0, reuse_addr = True, logger = logger)
56thd = t._start_in_thread()56thd = t._start_in_thread()
5757
58sys.stdout.write(f"{t.port}\n")58sys.stdout.write("%s\n" % (t.port,))
59sys.stdout.flush()59sys.stdout.flush()
6060
61try:61try:
@@ -111,7 +111,7 @@ class DeployedServer(object):
111 major = sys.version_info[0]111 major = sys.version_info[0]
112 minor = sys.version_info[1]112 minor = sys.version_info[1]
113 cmd = None113 cmd = None
114 for opt in [f"python{major}.{minor}", f"python{major}"]:114 for opt in ["python%s.%s" % (major, minor), "python%s" % (major,)]:
115 try:115 try:
116 cmd = remote_machine[opt]116 cmd = remote_machine[opt]
117 except CommandNotFound:117 except CommandNotFound:
@@ -155,26 +155,15 @@ class DeployedServer(object):
155 if self.proc is not None:155 if self.proc is not None:
156 try:156 try:
157 self.proc.terminate()157 self.proc.terminate()
158 self.proc.communicate()
159 except Exception:158 except Exception:
160 pass159 pass
161 self.proc = None160 self.proc = None
162 if self.tun is not None:161 if self.tun is not None:
163 try:162 try:
164 self.tun._session.proc.terminate()
165 self.tun._session.proc.communicate()
166 self.tun.close()163 self.tun.close()
167 except Exception:164 except Exception:
168 pass165 pass
169 self.tun = None166 self.tun = None
170 if self.remote_machine is not None:
171 try:
172 self.remote_machine._session.proc.terminate()
173 self.remote_machine._session.proc.communicate()
174 self.remote_machine.close()
175 except Exception:
176 pass
177 self.remote_machine = None
178 if self._tmpdir_ctx is not None:167 if self._tmpdir_ctx is not None:
179 try:168 try:
180 self._tmpdir_ctx.__exit__(None, None, None)169 self._tmpdir_ctx.__exit__(None, None, None)
diff --git a/plainbox/vendor/rpyc/version.py b/plainbox/vendor/rpyc/version.py
index 3028d79..d7618e4 100644
--- a/plainbox/vendor/rpyc/version.py
+++ b/plainbox/vendor/rpyc/version.py
@@ -1,3 +1,3 @@
1version = (5, 1, 0)1version = (4, 1, 4)
2version_string = ".".join(map(str, version))2version_string = ".".join(map(str, version))
3release_date = "2022-02-26"3release_date = "2020.1.30"

Subscribers

People subscribed via source and target branches