Merge ~sylvain-pineau/checkbox-ng:remote_resume_after_reboot_V2 into checkbox-ng:master
- Git
- lp:~sylvain-pineau/checkbox-ng
- remote_resume_after_reboot_V2
- Merge into master
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) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jonathan Cave (community) | Approve | ||
Sylvain Pineau (community) | Needs Resubmitting | ||
Maciej Kisielewski (community) | Approve | ||
Review via email:
|
Commit message
Description of the change
Improved version of https:/
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_
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/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Sylvain Pineau (sylvain-pineau) wrote : | # |
I've removed the re raised exception. tested, ctrl-c lead exactly to the same screen. Good catch!
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Jonathan Cave (jocave) wrote : | # |
One tiny flake warning and a question if I may...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Jonathan Cave (jocave) wrote : | # |
Understood, thanks.
Preview Diff
1 | diff --git a/checkbox_ng/launcher/remote.py b/checkbox_ng/launcher/remote.py | |||
2 | index 03c600c..b884a4f 100644 | |||
3 | --- a/checkbox_ng/launcher/remote.py | |||
4 | +++ b/checkbox_ng/launcher/remote.py | |||
5 | @@ -44,11 +44,13 @@ from plainbox.impl.color import Colorizer | |||
6 | 44 | from plainbox.impl.launcher import DefaultLauncherDefinition | 44 | from plainbox.impl.launcher import DefaultLauncherDefinition |
7 | 45 | from plainbox.impl.secure.sudo_broker import SudoProvider | 45 | from plainbox.impl.secure.sudo_broker import SudoProvider |
8 | 46 | from plainbox.impl.session.remote_assistant import RemoteSessionAssistant | 46 | from plainbox.impl.session.remote_assistant import RemoteSessionAssistant |
9 | 47 | from plainbox.impl.session.restart import RemoteSnappyRestartStrategy | ||
10 | 47 | from plainbox.vendor import rpyc | 48 | from plainbox.vendor import rpyc |
11 | 48 | from plainbox.vendor.rpyc.utils.server import ThreadedServer | 49 | from plainbox.vendor.rpyc.utils.server import ThreadedServer |
12 | 49 | from checkbox_ng.urwid_ui import test_plan_browser | 50 | from checkbox_ng.urwid_ui import test_plan_browser |
13 | 50 | from checkbox_ng.urwid_ui import CategoryBrowser | 51 | from checkbox_ng.urwid_ui import CategoryBrowser |
14 | 51 | from checkbox_ng.urwid_ui import interrupt_dialog | 52 | from checkbox_ng.urwid_ui import interrupt_dialog |
15 | 53 | from checkbox_ng.urwid_ui import resume_dialog | ||
16 | 52 | from checkbox_ng.launcher.run import NormalUI | 54 | from checkbox_ng.launcher.run import NormalUI |
17 | 53 | from checkbox_ng.launcher.stages import MainLoopStage | 55 | from checkbox_ng.launcher.stages import MainLoopStage |
18 | 54 | from checkbox_ng.launcher.stages import ReportsStage | 56 | from checkbox_ng.launcher.stages import ReportsStage |
19 | @@ -82,7 +84,10 @@ class SimpleUI(NormalUI, MainLoopStage): | |||
20 | 82 | print(SimpleUI.C.header(header, fill='-')) | 84 | print(SimpleUI.C.header(header, fill='-')) |
21 | 83 | 85 | ||
22 | 84 | def green_text(text, end='\n'): | 86 | def green_text(text, end='\n'): |
24 | 85 | print(SimpleUI.C.GREEN(text), end) | 87 | print(SimpleUI.C.GREEN(text), end=end) |
25 | 88 | |||
26 | 89 | def red_text(text, end='\n'): | ||
27 | 90 | print(SimpleUI.C.RED(text), end=end) | ||
28 | 86 | 91 | ||
29 | 87 | def horiz_line(): | 92 | def horiz_line(): |
30 | 88 | print(SimpleUI.C.WHITE('-' * 80)) | 93 | print(SimpleUI.C.WHITE('-' * 80)) |
31 | @@ -124,7 +129,22 @@ class RemoteSlave(Command): | |||
32 | 124 | 129 | ||
33 | 125 | SessionAssistantSlave.session_assistant = RemoteSessionAssistant( | 130 | SessionAssistantSlave.session_assistant = RemoteSessionAssistant( |
34 | 126 | lambda s: [sys.argv[0] + ' remote-service --resume']) | 131 | lambda s: [sys.argv[0] + ' remote-service --resume']) |
36 | 127 | if ctx.args.resume: | 132 | snap_data = os.getenv('SNAP_DATA') |
37 | 133 | remote_restart_stragegy_debug = os.getenv('REMOTE_RESTART_DEBUG') | ||
38 | 134 | if snap_data or remote_restart_stragegy_debug: | ||
39 | 135 | if remote_restart_stragegy_debug: | ||
40 | 136 | strategy = RemoteSnappyRestartStrategy(debug=True) | ||
41 | 137 | else: | ||
42 | 138 | strategy = RemoteSnappyRestartStrategy() | ||
43 | 139 | if os.path.exists(strategy.session_resume_filename): | ||
44 | 140 | with open(strategy.session_resume_filename, 'rt') as f: | ||
45 | 141 | session_id = f.readline() | ||
46 | 142 | try: | ||
47 | 143 | SessionAssistantSlave.session_assistant.resume_by_id( | ||
48 | 144 | session_id) | ||
49 | 145 | except StopIteration: | ||
50 | 146 | print("Couldn't resume the session") | ||
51 | 147 | elif ctx.args.resume: | ||
52 | 128 | try: | 148 | try: |
53 | 129 | SessionAssistantSlave.session_assistant.resume_last() | 149 | SessionAssistantSlave.session_assistant.resume_last() |
54 | 130 | except StopIteration: | 150 | except StopIteration: |
55 | @@ -236,7 +256,8 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage): | |||
56 | 236 | 'idle': self.new_session, | 256 | 'idle': self.new_session, |
57 | 237 | 'running': self.wait_and_continue, | 257 | 'running': self.wait_and_continue, |
58 | 238 | 'finalizing': self.finish_session, | 258 | 'finalizing': self.finish_session, |
60 | 239 | 'testsselected': self.run_jobs, | 259 | 'testsselected': partial( |
61 | 260 | self.run_jobs, resumed_session_info=payload), | ||
62 | 240 | 'bootstrapping': self.restart, | 261 | 'bootstrapping': self.restart, |
63 | 241 | 'bootstrapped': partial( | 262 | 'bootstrapped': partial( |
64 | 242 | self.select_jobs, all_jobs=payload), | 263 | self.select_jobs, all_jobs=payload), |
65 | @@ -363,6 +384,7 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage): | |||
66 | 363 | return True | 384 | return True |
67 | 364 | 385 | ||
68 | 365 | def finish_session(self): | 386 | def finish_session(self): |
69 | 387 | print(self.C.header("Results")) | ||
70 | 366 | if self.launcher.local_submission: | 388 | if self.launcher.local_submission: |
71 | 367 | # Disable SIGINT while we save local results | 389 | # Disable SIGINT while we save local results |
72 | 368 | tmp_sig = signal.signal(signal.SIGINT, signal.SIG_IGN) | 390 | tmp_sig = signal.signal(signal.SIGINT, signal.SIG_IGN) |
73 | @@ -379,7 +401,24 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage): | |||
74 | 379 | self.wait_for_job() | 401 | self.wait_for_job() |
75 | 380 | self.run_jobs() | 402 | self.run_jobs() |
76 | 381 | 403 | ||
78 | 382 | def run_jobs(self): | 404 | def _handle_last_job_after_resume(self, resumed_session_info): |
79 | 405 | if self.launcher.ui_type == 'silent': | ||
80 | 406 | time.sleep(20) | ||
81 | 407 | else: | ||
82 | 408 | resume_dialog(10) | ||
83 | 409 | jobs_repr = self.sa.get_jobs_repr([resumed_session_info['last_job']]) | ||
84 | 410 | job = jobs_repr[-1] | ||
85 | 411 | SimpleUI.header(job['name']) | ||
86 | 412 | print(_("ID: {0}").format(job['id'])) | ||
87 | 413 | print(_("Category: {0}").format(job['category_name'])) | ||
88 | 414 | SimpleUI.horiz_line() | ||
89 | 415 | print( | ||
90 | 416 | _("Outcome") + ": " + | ||
91 | 417 | SimpleUI.C.result(self.sa.get_job_result(job['id']))) | ||
92 | 418 | |||
93 | 419 | def run_jobs(self, resumed_session_info=None): | ||
94 | 420 | if resumed_session_info: | ||
95 | 421 | self._handle_last_job_after_resume(resumed_session_info) | ||
96 | 383 | _logger.info("master: Running jobs.") | 422 | _logger.info("master: Running jobs.") |
97 | 384 | jobs = self.sa.get_session_progress() | 423 | jobs = self.sa.get_session_progress() |
98 | 385 | _logger.debug("master: Jobs to be run:\n%s", | 424 | _logger.debug("master: Jobs to be run:\n%s", |
99 | @@ -406,7 +445,11 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage): | |||
100 | 406 | while True: | 445 | while True: |
101 | 407 | state, payload = self.sa.monitor_job() | 446 | state, payload = self.sa.monitor_job() |
102 | 408 | if payload and not self._is_bootstrapping: | 447 | if payload and not self._is_bootstrapping: |
104 | 409 | SimpleUI.green_text(payload, end='') | 448 | for stream, line in payload: |
105 | 449 | if stream == 'stderr': | ||
106 | 450 | SimpleUI.red_text(line, end='') | ||
107 | 451 | else: | ||
108 | 452 | SimpleUI.green_text(line, end='') | ||
109 | 410 | if state == 'running': | 453 | if state == 'running': |
110 | 411 | time.sleep(0.5) | 454 | time.sleep(0.5) |
111 | 412 | while True: | 455 | while True: |
112 | diff --git a/checkbox_ng/urwid_ui.py b/checkbox_ng/urwid_ui.py | |||
113 | index 8820b6b..992a471 100644 | |||
114 | --- a/checkbox_ng/urwid_ui.py | |||
115 | +++ b/checkbox_ng/urwid_ui.py | |||
116 | @@ -22,6 +22,8 @@ | |||
117 | 22 | ============================================================ | 22 | ============================================================ |
118 | 23 | """ | 23 | """ |
119 | 24 | 24 | ||
120 | 25 | import time | ||
121 | 26 | |||
122 | 25 | from gettext import gettext as _ | 27 | from gettext import gettext as _ |
123 | 26 | import urwid | 28 | import urwid |
124 | 27 | 29 | ||
125 | @@ -621,6 +623,59 @@ def interrupt_dialog(host): | |||
126 | 621 | return None | 623 | return None |
127 | 622 | 624 | ||
128 | 623 | 625 | ||
129 | 626 | class CountdownWidget(urwid.BigText): | ||
130 | 627 | |||
131 | 628 | def __init__(self, duration): | ||
132 | 629 | self._started = time.time() | ||
133 | 630 | self._duration = duration | ||
134 | 631 | self.set_text('{0:.1f}'.format(duration)) | ||
135 | 632 | self.font = urwid.HalfBlock6x5Font() | ||
136 | 633 | super().__init__(self.get_text()[0], self.font) | ||
137 | 634 | |||
138 | 635 | def update(self): | ||
139 | 636 | remaining = self._duration + self._started - time.time() | ||
140 | 637 | if remaining <= 0: | ||
141 | 638 | remaining = 0 | ||
142 | 639 | text = '{0:.1f}'.format(remaining) | ||
143 | 640 | self.set_text(text) | ||
144 | 641 | print('\33]2;Auto resume remote session in %s\007' % text, end='') | ||
145 | 642 | if remaining: | ||
146 | 643 | return True | ||
147 | 644 | else: | ||
148 | 645 | raise urwid.ExitMainLoop | ||
149 | 646 | |||
150 | 647 | |||
151 | 648 | def resume_dialog(duration): | ||
152 | 649 | palette = [ | ||
153 | 650 | ('body', 'light gray', 'black', 'standout'), | ||
154 | 651 | ('header', 'black', 'light gray', 'bold'), | ||
155 | 652 | ('buttnf', 'black', 'light gray'), | ||
156 | 653 | ('buttn', 'light gray', 'black', 'bold'), | ||
157 | 654 | ('foot', 'light gray', 'black'), | ||
158 | 655 | ('start', 'dark green,bold', 'black'), | ||
159 | 656 | ] | ||
160 | 657 | footer_text = [ | ||
161 | 658 | ('Press '), ('<CTRL + C>'), | ||
162 | 659 | (" to open the cancellation menu")] | ||
163 | 660 | timer = CountdownWidget(duration) | ||
164 | 661 | timer_pad = urwid.Padding(timer, align='center', width='clip') | ||
165 | 662 | timer_fill = urwid.Filler(timer_pad) | ||
166 | 663 | title = _("Checkbox slave is about to resume the session!") | ||
167 | 664 | header = urwid.AttrWrap(urwid.Padding(urwid.Text(title), left=1), 'header') | ||
168 | 665 | footer = urwid.AttrWrap( | ||
169 | 666 | urwid.Padding(urwid.Text(footer_text), left=1), 'foot') | ||
170 | 667 | frame = urwid.Frame(urwid.AttrWrap(urwid.LineBox(timer_fill), 'body'), | ||
171 | 668 | header=header, footer=footer) | ||
172 | 669 | |||
173 | 670 | def update_timer(loop, timer): | ||
174 | 671 | if timer.update(): | ||
175 | 672 | loop.set_alarm_in(0.1, update_timer, timer) | ||
176 | 673 | |||
177 | 674 | loop = urwid.MainLoop(frame, palette) | ||
178 | 675 | update_timer(loop, timer) | ||
179 | 676 | loop.run() | ||
180 | 677 | |||
181 | 678 | |||
182 | 624 | def add_widget(id, widget): | 679 | def add_widget(id, widget): |
183 | 625 | """Add the widget for a given id.""" | 680 | """Add the widget for a given id.""" |
184 | 626 | _widget_cache[id] = widget | 681 | _widget_cache[id] = widget |
185 | diff --git a/plainbox/impl/session/assistant.py b/plainbox/impl/session/assistant.py | |||
186 | index 1f67655..9e0c652 100644 | |||
187 | --- a/plainbox/impl/session/assistant.py | |||
188 | +++ b/plainbox/impl/session/assistant.py | |||
189 | @@ -675,7 +675,6 @@ class SessionAssistant: | |||
190 | 675 | io_log_filename=self._runner.get_record_path_for_job(job), | 675 | io_log_filename=self._runner.get_record_path_for_job(job), |
191 | 676 | ).get_result() | 676 | ).get_result() |
192 | 677 | self._context.state.update_job_result(job, result) | 677 | self._context.state.update_job_result(job, result) |
193 | 678 | self._metadata.running_job_name = None | ||
194 | 679 | self._manager.checkpoint() | 678 | self._manager.checkpoint() |
195 | 680 | if self._restart_strategy is not None: | 679 | if self._restart_strategy is not None: |
196 | 681 | self._restart_strategy.diffuse_application_restart(self._app_id) | 680 | self._restart_strategy.diffuse_application_restart(self._app_id) |
197 | @@ -686,6 +685,8 @@ class SessionAssistant: | |||
198 | 686 | else: | 685 | else: |
199 | 687 | UsageExpectation.of(self).allowed_calls = { | 686 | UsageExpectation.of(self).allowed_calls = { |
200 | 688 | self.select_test_plan: "to save test plan selection", | 687 | self.select_test_plan: "to save test plan selection", |
201 | 688 | self.use_alternate_configuration: ( | ||
202 | 689 | "use an alternate configuration system"), | ||
203 | 689 | } | 690 | } |
204 | 690 | return self._metadata | 691 | return self._metadata |
205 | 691 | 692 | ||
206 | diff --git a/plainbox/impl/session/remote_assistant.py b/plainbox/impl/session/remote_assistant.py | |||
207 | index abd6233..96786d1 100644 | |||
208 | --- a/plainbox/impl/session/remote_assistant.py | |||
209 | +++ b/plainbox/impl/session/remote_assistant.py | |||
210 | @@ -42,7 +42,7 @@ from checkbox_ng.launcher.run import SilentUI | |||
211 | 42 | 42 | ||
212 | 43 | _ = gettext.gettext | 43 | _ = gettext.gettext |
213 | 44 | 44 | ||
215 | 45 | _logger = logging.getLogger("plainbox.session.assistant2") | 45 | _logger = logging.getLogger("plainbox.session.remote_assistant") |
216 | 46 | 46 | ||
217 | 47 | Interaction = namedtuple('Interaction', ['kind', 'message', 'extra']) | 47 | Interaction = namedtuple('Interaction', ['kind', 'message', 'extra']) |
218 | 48 | 48 | ||
219 | @@ -74,7 +74,8 @@ class BufferedUI(SilentUI): | |||
220 | 74 | self.clear_buffers() | 74 | self.clear_buffers() |
221 | 75 | 75 | ||
222 | 76 | def got_program_output(self, stream_name, line): | 76 | def got_program_output(self, stream_name, line): |
224 | 77 | self._queue.put(line.decode(sys.stdout.encoding, 'replace')) | 77 | self._queue.put( |
225 | 78 | (stream_name, line.decode(sys.stdout.encoding, 'replace'))) | ||
226 | 78 | self._whole_queue.put(line) | 79 | self._whole_queue.put(line) |
227 | 79 | 80 | ||
228 | 80 | def whole_output(self): | 81 | def whole_output(self): |
229 | @@ -85,9 +86,9 @@ class BufferedUI(SilentUI): | |||
230 | 85 | 86 | ||
231 | 86 | def get_output(self): | 87 | def get_output(self): |
232 | 87 | """Returns all the output queued up since previous call.""" | 88 | """Returns all the output queued up since previous call.""" |
234 | 88 | output = '' | 89 | output = [] |
235 | 89 | while not self._queue.empty(): | 90 | while not self._queue.empty(): |
237 | 90 | output += self._queue.get() | 91 | output.append(self._queue.get()) |
238 | 91 | return output | 92 | return output |
239 | 92 | 93 | ||
240 | 93 | def clear_buffers(self): | 94 | def clear_buffers(self): |
241 | @@ -126,7 +127,7 @@ class BackgroundExecutor(Thread): | |||
242 | 126 | class RemoteSessionAssistant(): | 127 | class RemoteSessionAssistant(): |
243 | 127 | """Remote execution enabling wrapper for the SessionAssistant""" | 128 | """Remote execution enabling wrapper for the SessionAssistant""" |
244 | 128 | 129 | ||
246 | 129 | REMOTE_API_VERSION = 4 | 130 | REMOTE_API_VERSION = 5 |
247 | 130 | 131 | ||
248 | 131 | def __init__(self, cmd_callback): | 132 | def __init__(self, cmd_callback): |
249 | 132 | _logger.debug("__init__()") | 133 | _logger.debug("__init__()") |
250 | @@ -154,6 +155,7 @@ class RemoteSessionAssistant(): | |||
251 | 154 | self._jobs_count = 0 | 155 | self._jobs_count = 0 |
252 | 155 | self._job_index = 0 | 156 | self._job_index = 0 |
253 | 156 | self._currently_running_job = None # XXX: yuck! | 157 | self._currently_running_job = None # XXX: yuck! |
254 | 158 | self._last_job = None | ||
255 | 157 | self._current_comments = "" | 159 | self._current_comments = "" |
256 | 158 | self._last_response = None | 160 | self._last_response = None |
257 | 159 | self._normal_user = '' | 161 | self._normal_user = '' |
258 | @@ -234,6 +236,8 @@ class RemoteSessionAssistant(): | |||
259 | 234 | 236 | ||
260 | 235 | self._sa.update_app_blob(json.dumps( | 237 | self._sa.update_app_blob(json.dumps( |
261 | 236 | {'description': session_desc, }).encode("UTF-8")) | 238 | {'description': session_desc, }).encode("UTF-8")) |
262 | 239 | self._sa.update_app_blob(json.dumps( | ||
263 | 240 | {'launcher': configuration['launcher'], }).encode("UTF-8")) | ||
264 | 237 | 241 | ||
265 | 238 | self._session_id = self._sa.get_session_id() | 242 | self._session_id = self._sa.get_session_id() |
266 | 239 | tps = self._sa.get_test_plans() | 243 | tps = self._sa.get_test_plans() |
267 | @@ -393,6 +397,8 @@ class RemoteSessionAssistant(): | |||
268 | 393 | payload = ( | 397 | payload = ( |
269 | 394 | self._job_index, self._jobs_count, self._currently_running_job | 398 | self._job_index, self._jobs_count, self._currently_running_job |
270 | 395 | ) | 399 | ) |
271 | 400 | if self._state == TestsSelected and not self._currently_running_job: | ||
272 | 401 | payload = {'last_job': self._last_job} | ||
273 | 396 | elif self._state == Started: | 402 | elif self._state == Started: |
274 | 397 | payload = self._available_testplans | 403 | payload = self._available_testplans |
275 | 398 | elif self._state == Interacting: | 404 | elif self._state == Interacting: |
276 | @@ -491,6 +497,9 @@ class RemoteSessionAssistant(): | |||
277 | 491 | self._state = TestsSelected | 497 | self._state = TestsSelected |
278 | 492 | return candidates | 498 | return candidates |
279 | 493 | 499 | ||
280 | 500 | def get_job_result(self, job_id): | ||
281 | 501 | return self._sa.get_job_state(job_id).result | ||
282 | 502 | |||
283 | 494 | def get_jobs_repr(self, job_ids, offset=0): | 503 | def get_jobs_repr(self, job_ids, offset=0): |
284 | 495 | """ | 504 | """ |
285 | 496 | Translate jobs into a {'field': 'val'} representations. | 505 | Translate jobs into a {'field': 'val'} representations. |
286 | @@ -535,24 +544,55 @@ class RemoteSessionAssistant(): | |||
287 | 535 | return test_info_list | 544 | return test_info_list |
288 | 536 | 545 | ||
289 | 537 | def resume_last(self): | 546 | def resume_last(self): |
290 | 547 | last = next(self._sa.get_resumable_sessions()) | ||
291 | 548 | self.resume_by_id(last.id) | ||
292 | 549 | |||
293 | 550 | def resume_by_id(self, session_id): | ||
294 | 538 | self._launcher = DefaultLauncherDefinition() | 551 | self._launcher = DefaultLauncherDefinition() |
295 | 539 | self._sa.select_providers(*self._launcher.providers) | 552 | self._sa.select_providers(*self._launcher.providers) |
298 | 540 | last = next(self._sa.get_resumable_sessions()) | 553 | resume_candidates = list(self._sa.get_resumable_sessions()) |
299 | 541 | meta = self._sa.resume_session(last.id) | 554 | _logger.warning("Resuming session: %r", session_id) |
300 | 555 | meta = self._sa.resume_session(session_id) | ||
301 | 542 | app_blob = json.loads(meta.app_blob.decode("UTF-8")) | 556 | app_blob = json.loads(meta.app_blob.decode("UTF-8")) |
302 | 557 | launcher = app_blob['launcher'] | ||
303 | 558 | self._launcher.read_string(launcher) | ||
304 | 559 | self._sa.use_alternate_configuration(self._launcher) | ||
305 | 543 | test_plan_id = app_blob['testplan_id'] | 560 | test_plan_id = app_blob['testplan_id'] |
306 | 544 | self._sa.select_test_plan(test_plan_id) | 561 | self._sa.select_test_plan(test_plan_id) |
307 | 545 | self._sa.bootstrap() | 562 | self._sa.bootstrap() |
309 | 546 | result = MemoryJobResult({ | 563 | self._last_job = meta.running_job_name |
310 | 564 | |||
311 | 565 | result_dict = { | ||
312 | 547 | 'outcome': IJobResult.OUTCOME_PASS, | 566 | 'outcome': IJobResult.OUTCOME_PASS, |
317 | 548 | 'comments': _("Passed after resuming execution") | 567 | 'comments': _("Automatically passed after resuming execution"), |
318 | 549 | }) | 568 | } |
319 | 550 | last_job = meta.running_job_name | 569 | result_path = os.path.join( |
320 | 551 | if last_job: | 570 | self._sa.get_session_dir(), 'CHECKBOX_DATA', '__result') |
321 | 571 | if os.path.exists(result_path): | ||
322 | 572 | try: | ||
323 | 573 | with open(result_path, 'rt') as f: | ||
324 | 574 | result_dict = json.load(f) | ||
325 | 575 | # the only really important field in the result is | ||
326 | 576 | # 'outcome' so let's make sure it doesn't contain | ||
327 | 577 | # anything stupid | ||
328 | 578 | if result_dict.get('outcome') not in [ | ||
329 | 579 | 'pass', 'fail', 'skip']: | ||
330 | 580 | result_dict['outcome'] = IJobResult.OUTCOME_PASS | ||
331 | 581 | except json.JSONDecodeError as e: | ||
332 | 582 | pass | ||
333 | 583 | result = MemoryJobResult(result_dict) | ||
334 | 584 | if self._last_job: | ||
335 | 552 | try: | 585 | try: |
337 | 553 | self._sa.use_job_result(last_job, result) | 586 | self._sa.use_job_result(self._last_job, result) |
338 | 554 | except KeyError: | 587 | except KeyError: |
340 | 555 | raise SystemExit(last_job) | 588 | raise SystemExit(self._last_job) |
341 | 589 | |||
342 | 590 | if self._launcher.auto_retry: | ||
343 | 591 | for job_id in [job.id for job in self.get_auto_retry_candidates()]: | ||
344 | 592 | job_state = self._sa.get_job_state(job_id) | ||
345 | 593 | job_state.attempts = self._launcher.max_attempts - len( | ||
346 | 594 | job_state.result_history) | ||
347 | 595 | |||
348 | 556 | self._state = TestsSelected | 596 | self._state = TestsSelected |
349 | 557 | 597 | ||
350 | 558 | def finalize_session(self): | 598 | def finalize_session(self): |
351 | diff --git a/plainbox/impl/session/restart.py b/plainbox/impl/session/restart.py | |||
352 | index eaa1f4d..0b497e5 100644 | |||
353 | --- a/plainbox/impl/session/restart.py | |||
354 | +++ b/plainbox/impl/session/restart.py | |||
355 | @@ -197,6 +197,37 @@ class SnappyRestartStrategy(IRestartStrategy): | |||
356 | 197 | subprocess.call(['sudo', 'rm', filename]) | 197 | subprocess.call(['sudo', 'rm', filename]) |
357 | 198 | 198 | ||
358 | 199 | 199 | ||
359 | 200 | class RemoteSnappyRestartStrategy(IRestartStrategy): | ||
360 | 201 | |||
361 | 202 | """ | ||
362 | 203 | Remote Restart strategy for checkbox snaps. | ||
363 | 204 | """ | ||
364 | 205 | |||
365 | 206 | def __init__(self, debug=False): | ||
366 | 207 | self.debug = debug | ||
367 | 208 | self.session_resume_filename = self.get_session_resume_filename() | ||
368 | 209 | |||
369 | 210 | def get_session_resume_filename(self) -> str: | ||
370 | 211 | if self.debug: | ||
371 | 212 | return '/tmp/session_resume' | ||
372 | 213 | snap_data = os.getenv('SNAP_DATA') | ||
373 | 214 | return os.path.join(snap_data, 'session_resume') | ||
374 | 215 | |||
375 | 216 | def prime_application_restart(self, app_id: str, | ||
376 | 217 | session_id: str, cmd: str) -> None: | ||
377 | 218 | with open(self.session_resume_filename, 'wt') as f: | ||
378 | 219 | f.write(session_id) | ||
379 | 220 | |||
380 | 221 | def diffuse_application_restart(self, app_id: str) -> None: | ||
381 | 222 | try: | ||
382 | 223 | os.remove(self.session_resume_filename) | ||
383 | 224 | except OSError as exc: | ||
384 | 225 | if exc.errno == errno.ENOENT: | ||
385 | 226 | pass | ||
386 | 227 | else: | ||
387 | 228 | raise | ||
388 | 229 | |||
389 | 230 | |||
390 | 200 | def detect_restart_strategy() -> IRestartStrategy: | 231 | def detect_restart_strategy() -> IRestartStrategy: |
391 | 201 | """ | 232 | """ |
392 | 202 | Detect the restart strategy for the current environment. | 233 | Detect the restart strategy for the current environment. |
393 | @@ -206,17 +237,41 @@ def detect_restart_strategy() -> IRestartStrategy: | |||
394 | 206 | :raises LookupError: | 237 | :raises LookupError: |
395 | 207 | When no such object can be found. | 238 | When no such object can be found. |
396 | 208 | """ | 239 | """ |
397 | 240 | # XXX: RemoteSnappyRestartStrategy debug | ||
398 | 241 | remote_restart_stragegy_debug = os.getenv('REMOTE_RESTART_DEBUG') | ||
399 | 242 | if remote_restart_stragegy_debug: | ||
400 | 243 | return RemoteSnappyRestartStrategy(debug=True) | ||
401 | 209 | # If we are running as a confined Snappy app this variable will have been | 244 | # If we are running as a confined Snappy app this variable will have been |
402 | 210 | # set by the launcher script | 245 | # set by the launcher script |
403 | 211 | if on_ubuntucore(): | 246 | if on_ubuntucore(): |
406 | 212 | return SnappyRestartStrategy() | 247 | try: |
407 | 213 | 248 | slave_status = subprocess.check_output( | |
408 | 249 | ['snapctl', 'get', 'slave'], universal_newlines=True).rstrip() | ||
409 | 250 | if slave_status == 'disabled': | ||
410 | 251 | return SnappyRestartStrategy() | ||
411 | 252 | else: | ||
412 | 253 | return RemoteSnappyRestartStrategy() | ||
413 | 254 | except subprocess.CalledProcessError: | ||
414 | 255 | return SnappyRestartStrategy() | ||
415 | 256 | |||
416 | 257 | # Classic + remote service enabled | ||
417 | 258 | snap_data = os.getenv('SNAP_DATA') | ||
418 | 259 | if snap_data: | ||
419 | 260 | try: | ||
420 | 261 | slave_status = subprocess.check_output( | ||
421 | 262 | ['snapctl', 'get', 'slave'], universal_newlines=True).rstrip() | ||
422 | 263 | if slave_status == 'enabled': | ||
423 | 264 | return RemoteSnappyRestartStrategy() | ||
424 | 265 | except subprocess.CalledProcessError: | ||
425 | 266 | pass | ||
426 | 267 | |||
427 | 214 | if os.path.isdir('/etc/xdg/autostart'): | 268 | if os.path.isdir('/etc/xdg/autostart'): |
428 | 215 | # NOTE: Assume this is a terminal application | 269 | # NOTE: Assume this is a terminal application |
429 | 216 | return XDGRestartStrategy(app_terminal=True) | 270 | return XDGRestartStrategy(app_terminal=True) |
430 | 217 | 271 | ||
431 | 218 | raise LookupError("Unable to find appropriate strategy.""") | 272 | raise LookupError("Unable to find appropriate strategy.""") |
432 | 219 | 273 | ||
433 | 274 | |||
434 | 220 | def get_strategy_by_name(name: str) -> type: | 275 | def get_strategy_by_name(name: str) -> type: |
435 | 221 | """ | 276 | """ |
436 | 222 | Get restart strategy class identified by a string. | 277 | Get restart strategy class identified by a string. |
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