Merge ~sylvain-pineau/checkbox-ng:remote_resume_after_reboot_V2 into checkbox-ng:master

Proposed by Sylvain Pineau
Status: Merged
Approved by: Sylvain Pineau
Approved revision: ec339fae4f096036203cf1b579c56ac71054b04f
Merged at revision: 321808ffa9f1a1bf05f6eeacb43835b01cb61891
Proposed branch: ~sylvain-pineau/checkbox-ng:remote_resume_after_reboot_V2
Merge into: checkbox-ng:master
Diff against target: 436 lines (+216/-22)
5 files modified
checkbox_ng/launcher/remote.py (+48/-5)
checkbox_ng/urwid_ui.py (+55/-0)
plainbox/impl/session/assistant.py (+2/-1)
plainbox/impl/session/remote_assistant.py (+54/-14)
plainbox/impl/session/restart.py (+57/-2)
Reviewer Review Type Date Requested Status
Jonathan Cave (community) Approve
Sylvain Pineau (community) Needs Resubmitting
Maciej Kisielewski (community) Approve
Review via email: mp+362852@code.launchpad.net

Description of the change

Improved version of https://code.launchpad.net/~sylvain-pineau/checkbox-ng/+git/checkbox-ng/+merge/362546

There's no Resumed state, the resume event info happening on the slave is sent via the payload data.
the run_jobs method gets a new argument to process it to essentially display the resumed job id and its outcome.

On slave side, the new version of this MR saves the launcher settings in order to set them again when resuming (i.g auto_retry parameters). It gets also support for jobs altering the result via $CHECKBOX_DATA/__result.

One bonus since I bumped the API, stderr streams are colored in RED.

Tested using the same env/methodology as the previous MR (and to see how the new one evolved).

Warning: I still consider this version as a first set of commits to fully support reboots/autorestart/noreturn jobs. rebooting once works fine but for jobs like 30 suspends + 3 reboots handled by the job itself it won't as the service will start on boot and try to resume the session. For such complex scenario disabling the systemd unit and activating it again via the __respawn way is probably the best solution but deserves its own MR.

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

Amazing work, huge +1.
I’d only change that reraising. Good to land after testing that we in fact don’t need that try/except block

review: Approve
Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

I've removed the re raised exception. tested, ctrl-c lead exactly to the same screen. Good catch!

review: Needs Resubmitting
Revision history for this message
Jonathan Cave (jocave) wrote :

One tiny flake warning and a question if I may...

review: Needs Information
Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

flake error fixed.

Regarding your question, of course /tmp will go away if you reboot your development system. But the debug mode was put in place to actually avoid having to reboot the system. Interrupting the slave with a CTRL-C will do the same (i.e going through all the strategy methods, prime&diffuse). So yes, the debug mode is not meant to validate the restart strategy with "real" reboots. It's just a convenience way to simulate them and speed up the debugging/development of features related to UC restarts on your development machine (i.e your laptop running classic)

Revision history for this message
Jonathan Cave (jocave) wrote :

Understood, thanks.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/checkbox_ng/launcher/remote.py b/checkbox_ng/launcher/remote.py
index 03c600c..b884a4f 100644
--- a/checkbox_ng/launcher/remote.py
+++ b/checkbox_ng/launcher/remote.py
@@ -44,11 +44,13 @@ from plainbox.impl.color import Colorizer
44from plainbox.impl.launcher import DefaultLauncherDefinition44from plainbox.impl.launcher import DefaultLauncherDefinition
45from plainbox.impl.secure.sudo_broker import SudoProvider45from plainbox.impl.secure.sudo_broker import SudoProvider
46from plainbox.impl.session.remote_assistant import RemoteSessionAssistant46from plainbox.impl.session.remote_assistant import RemoteSessionAssistant
47from plainbox.impl.session.restart import RemoteSnappyRestartStrategy
47from plainbox.vendor import rpyc48from plainbox.vendor import rpyc
48from plainbox.vendor.rpyc.utils.server import ThreadedServer49from plainbox.vendor.rpyc.utils.server import ThreadedServer
49from checkbox_ng.urwid_ui import test_plan_browser50from checkbox_ng.urwid_ui import test_plan_browser
50from checkbox_ng.urwid_ui import CategoryBrowser51from checkbox_ng.urwid_ui import CategoryBrowser
51from checkbox_ng.urwid_ui import interrupt_dialog52from checkbox_ng.urwid_ui import interrupt_dialog
53from checkbox_ng.urwid_ui import resume_dialog
52from checkbox_ng.launcher.run import NormalUI54from checkbox_ng.launcher.run import NormalUI
53from checkbox_ng.launcher.stages import MainLoopStage55from checkbox_ng.launcher.stages import MainLoopStage
54from checkbox_ng.launcher.stages import ReportsStage56from checkbox_ng.launcher.stages import ReportsStage
@@ -82,7 +84,10 @@ class SimpleUI(NormalUI, MainLoopStage):
82 print(SimpleUI.C.header(header, fill='-'))84 print(SimpleUI.C.header(header, fill='-'))
8385
84 def green_text(text, end='\n'):86 def green_text(text, end='\n'):
85 print(SimpleUI.C.GREEN(text), end)87 print(SimpleUI.C.GREEN(text), end=end)
88
89 def red_text(text, end='\n'):
90 print(SimpleUI.C.RED(text), end=end)
8691
87 def horiz_line():92 def horiz_line():
88 print(SimpleUI.C.WHITE('-' * 80))93 print(SimpleUI.C.WHITE('-' * 80))
@@ -124,7 +129,22 @@ class RemoteSlave(Command):
124129
125 SessionAssistantSlave.session_assistant = RemoteSessionAssistant(130 SessionAssistantSlave.session_assistant = RemoteSessionAssistant(
126 lambda s: [sys.argv[0] + ' remote-service --resume'])131 lambda s: [sys.argv[0] + ' remote-service --resume'])
127 if ctx.args.resume:132 snap_data = os.getenv('SNAP_DATA')
133 remote_restart_stragegy_debug = os.getenv('REMOTE_RESTART_DEBUG')
134 if snap_data or remote_restart_stragegy_debug:
135 if remote_restart_stragegy_debug:
136 strategy = RemoteSnappyRestartStrategy(debug=True)
137 else:
138 strategy = RemoteSnappyRestartStrategy()
139 if os.path.exists(strategy.session_resume_filename):
140 with open(strategy.session_resume_filename, 'rt') as f:
141 session_id = f.readline()
142 try:
143 SessionAssistantSlave.session_assistant.resume_by_id(
144 session_id)
145 except StopIteration:
146 print("Couldn't resume the session")
147 elif ctx.args.resume:
128 try:148 try:
129 SessionAssistantSlave.session_assistant.resume_last()149 SessionAssistantSlave.session_assistant.resume_last()
130 except StopIteration:150 except StopIteration:
@@ -236,7 +256,8 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
236 'idle': self.new_session,256 'idle': self.new_session,
237 'running': self.wait_and_continue,257 'running': self.wait_and_continue,
238 'finalizing': self.finish_session,258 'finalizing': self.finish_session,
239 'testsselected': self.run_jobs,259 'testsselected': partial(
260 self.run_jobs, resumed_session_info=payload),
240 'bootstrapping': self.restart,261 'bootstrapping': self.restart,
241 'bootstrapped': partial(262 'bootstrapped': partial(
242 self.select_jobs, all_jobs=payload),263 self.select_jobs, all_jobs=payload),
@@ -363,6 +384,7 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
363 return True384 return True
364385
365 def finish_session(self):386 def finish_session(self):
387 print(self.C.header("Results"))
366 if self.launcher.local_submission:388 if self.launcher.local_submission:
367 # Disable SIGINT while we save local results389 # Disable SIGINT while we save local results
368 tmp_sig = signal.signal(signal.SIGINT, signal.SIG_IGN)390 tmp_sig = signal.signal(signal.SIGINT, signal.SIG_IGN)
@@ -379,7 +401,24 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
379 self.wait_for_job()401 self.wait_for_job()
380 self.run_jobs()402 self.run_jobs()
381403
382 def run_jobs(self):404 def _handle_last_job_after_resume(self, resumed_session_info):
405 if self.launcher.ui_type == 'silent':
406 time.sleep(20)
407 else:
408 resume_dialog(10)
409 jobs_repr = self.sa.get_jobs_repr([resumed_session_info['last_job']])
410 job = jobs_repr[-1]
411 SimpleUI.header(job['name'])
412 print(_("ID: {0}").format(job['id']))
413 print(_("Category: {0}").format(job['category_name']))
414 SimpleUI.horiz_line()
415 print(
416 _("Outcome") + ": " +
417 SimpleUI.C.result(self.sa.get_job_result(job['id'])))
418
419 def run_jobs(self, resumed_session_info=None):
420 if resumed_session_info:
421 self._handle_last_job_after_resume(resumed_session_info)
383 _logger.info("master: Running jobs.")422 _logger.info("master: Running jobs.")
384 jobs = self.sa.get_session_progress()423 jobs = self.sa.get_session_progress()
385 _logger.debug("master: Jobs to be run:\n%s",424 _logger.debug("master: Jobs to be run:\n%s",
@@ -406,7 +445,11 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
406 while True:445 while True:
407 state, payload = self.sa.monitor_job()446 state, payload = self.sa.monitor_job()
408 if payload and not self._is_bootstrapping:447 if payload and not self._is_bootstrapping:
409 SimpleUI.green_text(payload, end='')448 for stream, line in payload:
449 if stream == 'stderr':
450 SimpleUI.red_text(line, end='')
451 else:
452 SimpleUI.green_text(line, end='')
410 if state == 'running':453 if state == 'running':
411 time.sleep(0.5)454 time.sleep(0.5)
412 while True:455 while True:
diff --git a/checkbox_ng/urwid_ui.py b/checkbox_ng/urwid_ui.py
index 8820b6b..992a471 100644
--- a/checkbox_ng/urwid_ui.py
+++ b/checkbox_ng/urwid_ui.py
@@ -22,6 +22,8 @@
22============================================================22============================================================
23"""23"""
2424
25import time
26
25from gettext import gettext as _27from gettext import gettext as _
26import urwid28import urwid
2729
@@ -621,6 +623,59 @@ def interrupt_dialog(host):
621 return None623 return None
622624
623625
626class CountdownWidget(urwid.BigText):
627
628 def __init__(self, duration):
629 self._started = time.time()
630 self._duration = duration
631 self.set_text('{0:.1f}'.format(duration))
632 self.font = urwid.HalfBlock6x5Font()
633 super().__init__(self.get_text()[0], self.font)
634
635 def update(self):
636 remaining = self._duration + self._started - time.time()
637 if remaining <= 0:
638 remaining = 0
639 text = '{0:.1f}'.format(remaining)
640 self.set_text(text)
641 print('\33]2;Auto resume remote session in %s\007' % text, end='')
642 if remaining:
643 return True
644 else:
645 raise urwid.ExitMainLoop
646
647
648def resume_dialog(duration):
649 palette = [
650 ('body', 'light gray', 'black', 'standout'),
651 ('header', 'black', 'light gray', 'bold'),
652 ('buttnf', 'black', 'light gray'),
653 ('buttn', 'light gray', 'black', 'bold'),
654 ('foot', 'light gray', 'black'),
655 ('start', 'dark green,bold', 'black'),
656 ]
657 footer_text = [
658 ('Press '), ('<CTRL + C>'),
659 (" to open the cancellation menu")]
660 timer = CountdownWidget(duration)
661 timer_pad = urwid.Padding(timer, align='center', width='clip')
662 timer_fill = urwid.Filler(timer_pad)
663 title = _("Checkbox slave is about to resume the session!")
664 header = urwid.AttrWrap(urwid.Padding(urwid.Text(title), left=1), 'header')
665 footer = urwid.AttrWrap(
666 urwid.Padding(urwid.Text(footer_text), left=1), 'foot')
667 frame = urwid.Frame(urwid.AttrWrap(urwid.LineBox(timer_fill), 'body'),
668 header=header, footer=footer)
669
670 def update_timer(loop, timer):
671 if timer.update():
672 loop.set_alarm_in(0.1, update_timer, timer)
673
674 loop = urwid.MainLoop(frame, palette)
675 update_timer(loop, timer)
676 loop.run()
677
678
624def add_widget(id, widget):679def add_widget(id, widget):
625 """Add the widget for a given id."""680 """Add the widget for a given id."""
626 _widget_cache[id] = widget681 _widget_cache[id] = widget
diff --git a/plainbox/impl/session/assistant.py b/plainbox/impl/session/assistant.py
index 1f67655..9e0c652 100644
--- a/plainbox/impl/session/assistant.py
+++ b/plainbox/impl/session/assistant.py
@@ -675,7 +675,6 @@ class SessionAssistant:
675 io_log_filename=self._runner.get_record_path_for_job(job),675 io_log_filename=self._runner.get_record_path_for_job(job),
676 ).get_result()676 ).get_result()
677 self._context.state.update_job_result(job, result)677 self._context.state.update_job_result(job, result)
678 self._metadata.running_job_name = None
679 self._manager.checkpoint()678 self._manager.checkpoint()
680 if self._restart_strategy is not None:679 if self._restart_strategy is not None:
681 self._restart_strategy.diffuse_application_restart(self._app_id)680 self._restart_strategy.diffuse_application_restart(self._app_id)
@@ -686,6 +685,8 @@ class SessionAssistant:
686 else:685 else:
687 UsageExpectation.of(self).allowed_calls = {686 UsageExpectation.of(self).allowed_calls = {
688 self.select_test_plan: "to save test plan selection",687 self.select_test_plan: "to save test plan selection",
688 self.use_alternate_configuration: (
689 "use an alternate configuration system"),
689 }690 }
690 return self._metadata691 return self._metadata
691692
diff --git a/plainbox/impl/session/remote_assistant.py b/plainbox/impl/session/remote_assistant.py
index abd6233..96786d1 100644
--- a/plainbox/impl/session/remote_assistant.py
+++ b/plainbox/impl/session/remote_assistant.py
@@ -42,7 +42,7 @@ from checkbox_ng.launcher.run import SilentUI
4242
43_ = gettext.gettext43_ = gettext.gettext
4444
45_logger = logging.getLogger("plainbox.session.assistant2")45_logger = logging.getLogger("plainbox.session.remote_assistant")
4646
47Interaction = namedtuple('Interaction', ['kind', 'message', 'extra'])47Interaction = namedtuple('Interaction', ['kind', 'message', 'extra'])
4848
@@ -74,7 +74,8 @@ class BufferedUI(SilentUI):
74 self.clear_buffers()74 self.clear_buffers()
7575
76 def got_program_output(self, stream_name, line):76 def got_program_output(self, stream_name, line):
77 self._queue.put(line.decode(sys.stdout.encoding, 'replace'))77 self._queue.put(
78 (stream_name, line.decode(sys.stdout.encoding, 'replace')))
78 self._whole_queue.put(line)79 self._whole_queue.put(line)
7980
80 def whole_output(self):81 def whole_output(self):
@@ -85,9 +86,9 @@ class BufferedUI(SilentUI):
8586
86 def get_output(self):87 def get_output(self):
87 """Returns all the output queued up since previous call."""88 """Returns all the output queued up since previous call."""
88 output = ''89 output = []
89 while not self._queue.empty():90 while not self._queue.empty():
90 output += self._queue.get()91 output.append(self._queue.get())
91 return output92 return output
9293
93 def clear_buffers(self):94 def clear_buffers(self):
@@ -126,7 +127,7 @@ class BackgroundExecutor(Thread):
126class RemoteSessionAssistant():127class RemoteSessionAssistant():
127 """Remote execution enabling wrapper for the SessionAssistant"""128 """Remote execution enabling wrapper for the SessionAssistant"""
128129
129 REMOTE_API_VERSION = 4130 REMOTE_API_VERSION = 5
130131
131 def __init__(self, cmd_callback):132 def __init__(self, cmd_callback):
132 _logger.debug("__init__()")133 _logger.debug("__init__()")
@@ -154,6 +155,7 @@ class RemoteSessionAssistant():
154 self._jobs_count = 0155 self._jobs_count = 0
155 self._job_index = 0156 self._job_index = 0
156 self._currently_running_job = None # XXX: yuck!157 self._currently_running_job = None # XXX: yuck!
158 self._last_job = None
157 self._current_comments = ""159 self._current_comments = ""
158 self._last_response = None160 self._last_response = None
159 self._normal_user = ''161 self._normal_user = ''
@@ -234,6 +236,8 @@ class RemoteSessionAssistant():
234236
235 self._sa.update_app_blob(json.dumps(237 self._sa.update_app_blob(json.dumps(
236 {'description': session_desc, }).encode("UTF-8"))238 {'description': session_desc, }).encode("UTF-8"))
239 self._sa.update_app_blob(json.dumps(
240 {'launcher': configuration['launcher'], }).encode("UTF-8"))
237241
238 self._session_id = self._sa.get_session_id()242 self._session_id = self._sa.get_session_id()
239 tps = self._sa.get_test_plans()243 tps = self._sa.get_test_plans()
@@ -393,6 +397,8 @@ class RemoteSessionAssistant():
393 payload = (397 payload = (
394 self._job_index, self._jobs_count, self._currently_running_job398 self._job_index, self._jobs_count, self._currently_running_job
395 )399 )
400 if self._state == TestsSelected and not self._currently_running_job:
401 payload = {'last_job': self._last_job}
396 elif self._state == Started:402 elif self._state == Started:
397 payload = self._available_testplans403 payload = self._available_testplans
398 elif self._state == Interacting:404 elif self._state == Interacting:
@@ -491,6 +497,9 @@ class RemoteSessionAssistant():
491 self._state = TestsSelected497 self._state = TestsSelected
492 return candidates498 return candidates
493499
500 def get_job_result(self, job_id):
501 return self._sa.get_job_state(job_id).result
502
494 def get_jobs_repr(self, job_ids, offset=0):503 def get_jobs_repr(self, job_ids, offset=0):
495 """504 """
496 Translate jobs into a {'field': 'val'} representations.505 Translate jobs into a {'field': 'val'} representations.
@@ -535,24 +544,55 @@ class RemoteSessionAssistant():
535 return test_info_list544 return test_info_list
536545
537 def resume_last(self):546 def resume_last(self):
547 last = next(self._sa.get_resumable_sessions())
548 self.resume_by_id(last.id)
549
550 def resume_by_id(self, session_id):
538 self._launcher = DefaultLauncherDefinition()551 self._launcher = DefaultLauncherDefinition()
539 self._sa.select_providers(*self._launcher.providers)552 self._sa.select_providers(*self._launcher.providers)
540 last = next(self._sa.get_resumable_sessions())553 resume_candidates = list(self._sa.get_resumable_sessions())
541 meta = self._sa.resume_session(last.id)554 _logger.warning("Resuming session: %r", session_id)
555 meta = self._sa.resume_session(session_id)
542 app_blob = json.loads(meta.app_blob.decode("UTF-8"))556 app_blob = json.loads(meta.app_blob.decode("UTF-8"))
557 launcher = app_blob['launcher']
558 self._launcher.read_string(launcher)
559 self._sa.use_alternate_configuration(self._launcher)
543 test_plan_id = app_blob['testplan_id']560 test_plan_id = app_blob['testplan_id']
544 self._sa.select_test_plan(test_plan_id)561 self._sa.select_test_plan(test_plan_id)
545 self._sa.bootstrap()562 self._sa.bootstrap()
546 result = MemoryJobResult({563 self._last_job = meta.running_job_name
564
565 result_dict = {
547 'outcome': IJobResult.OUTCOME_PASS,566 'outcome': IJobResult.OUTCOME_PASS,
548 'comments': _("Passed after resuming execution")567 'comments': _("Automatically passed after resuming execution"),
549 })568 }
550 last_job = meta.running_job_name569 result_path = os.path.join(
551 if last_job:570 self._sa.get_session_dir(), 'CHECKBOX_DATA', '__result')
571 if os.path.exists(result_path):
572 try:
573 with open(result_path, 'rt') as f:
574 result_dict = json.load(f)
575 # the only really important field in the result is
576 # 'outcome' so let's make sure it doesn't contain
577 # anything stupid
578 if result_dict.get('outcome') not in [
579 'pass', 'fail', 'skip']:
580 result_dict['outcome'] = IJobResult.OUTCOME_PASS
581 except json.JSONDecodeError as e:
582 pass
583 result = MemoryJobResult(result_dict)
584 if self._last_job:
552 try:585 try:
553 self._sa.use_job_result(last_job, result)586 self._sa.use_job_result(self._last_job, result)
554 except KeyError:587 except KeyError:
555 raise SystemExit(last_job)588 raise SystemExit(self._last_job)
589
590 if self._launcher.auto_retry:
591 for job_id in [job.id for job in self.get_auto_retry_candidates()]:
592 job_state = self._sa.get_job_state(job_id)
593 job_state.attempts = self._launcher.max_attempts - len(
594 job_state.result_history)
595
556 self._state = TestsSelected596 self._state = TestsSelected
557597
558 def finalize_session(self):598 def finalize_session(self):
diff --git a/plainbox/impl/session/restart.py b/plainbox/impl/session/restart.py
index eaa1f4d..0b497e5 100644
--- a/plainbox/impl/session/restart.py
+++ b/plainbox/impl/session/restart.py
@@ -197,6 +197,37 @@ class SnappyRestartStrategy(IRestartStrategy):
197 subprocess.call(['sudo', 'rm', filename])197 subprocess.call(['sudo', 'rm', filename])
198198
199199
200class RemoteSnappyRestartStrategy(IRestartStrategy):
201
202 """
203 Remote Restart strategy for checkbox snaps.
204 """
205
206 def __init__(self, debug=False):
207 self.debug = debug
208 self.session_resume_filename = self.get_session_resume_filename()
209
210 def get_session_resume_filename(self) -> str:
211 if self.debug:
212 return '/tmp/session_resume'
213 snap_data = os.getenv('SNAP_DATA')
214 return os.path.join(snap_data, 'session_resume')
215
216 def prime_application_restart(self, app_id: str,
217 session_id: str, cmd: str) -> None:
218 with open(self.session_resume_filename, 'wt') as f:
219 f.write(session_id)
220
221 def diffuse_application_restart(self, app_id: str) -> None:
222 try:
223 os.remove(self.session_resume_filename)
224 except OSError as exc:
225 if exc.errno == errno.ENOENT:
226 pass
227 else:
228 raise
229
230
200def detect_restart_strategy() -> IRestartStrategy:231def detect_restart_strategy() -> IRestartStrategy:
201 """232 """
202 Detect the restart strategy for the current environment.233 Detect the restart strategy for the current environment.
@@ -206,17 +237,41 @@ def detect_restart_strategy() -> IRestartStrategy:
206 :raises LookupError:237 :raises LookupError:
207 When no such object can be found.238 When no such object can be found.
208 """239 """
240 # XXX: RemoteSnappyRestartStrategy debug
241 remote_restart_stragegy_debug = os.getenv('REMOTE_RESTART_DEBUG')
242 if remote_restart_stragegy_debug:
243 return RemoteSnappyRestartStrategy(debug=True)
209 # If we are running as a confined Snappy app this variable will have been244 # If we are running as a confined Snappy app this variable will have been
210 # set by the launcher script245 # set by the launcher script
211 if on_ubuntucore():246 if on_ubuntucore():
212 return SnappyRestartStrategy()247 try:
213 248 slave_status = subprocess.check_output(
249 ['snapctl', 'get', 'slave'], universal_newlines=True).rstrip()
250 if slave_status == 'disabled':
251 return SnappyRestartStrategy()
252 else:
253 return RemoteSnappyRestartStrategy()
254 except subprocess.CalledProcessError:
255 return SnappyRestartStrategy()
256
257 # Classic + remote service enabled
258 snap_data = os.getenv('SNAP_DATA')
259 if snap_data:
260 try:
261 slave_status = subprocess.check_output(
262 ['snapctl', 'get', 'slave'], universal_newlines=True).rstrip()
263 if slave_status == 'enabled':
264 return RemoteSnappyRestartStrategy()
265 except subprocess.CalledProcessError:
266 pass
267
214 if os.path.isdir('/etc/xdg/autostart'):268 if os.path.isdir('/etc/xdg/autostart'):
215 # NOTE: Assume this is a terminal application269 # NOTE: Assume this is a terminal application
216 return XDGRestartStrategy(app_terminal=True)270 return XDGRestartStrategy(app_terminal=True)
217271
218 raise LookupError("Unable to find appropriate strategy.""")272 raise LookupError("Unable to find appropriate strategy.""")
219273
274
220def get_strategy_by_name(name: str) -> type:275def get_strategy_by_name(name: str) -> type:
221 """276 """
222 Get restart strategy class identified by a string.277 Get restart strategy class identified by a string.

Subscribers

People subscribed via source and target branches