Merge lp:~sylvain-pineau/checkbox/checkbox-ng-sa into lp:checkbox
- checkbox-ng-sa
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Maciej Kisielewski |
Approved revision: | 4125 |
Merged at revision: | 4122 |
Proposed branch: | lp:~sylvain-pineau/checkbox/checkbox-ng-sa |
Merge into: | lp:checkbox |
Diff against target: |
748 lines (+637/-5) 6 files modified
checkbox-ng/checkbox_ng/commands/sru.py (+1/-1) checkbox-ng/checkbox_ng/misc.py (+22/-0) checkbox-ng/checkbox_ng/ui.py (+4/-1) checkbox-ng/launchers/checkbox-cli2 (+596/-0) plainbox/plainbox/impl/session/assistant.py (+11/-3) plainbox/plainbox/impl/session/restart.py (+3/-0) |
To merge this branch: | bzr merge lp:~sylvain-pineau/checkbox/checkbox-ng-sa |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Sylvain Pineau (community) | Needs Resubmitting | ||
Maciej Kisielewski | Needs Fixing | ||
Review via email: mp+275925@code.launchpad.net |
Commit message
Description of the change
checkbox-cli rewritten using plainbox SessionAssistant as a standalone tool (called checkbox-cli2 to avoid conflicts)
Zygmunt Krynicki (zyga) wrote : | # |
Sylvain Pineau (sylvain-pineau) wrote : | # |
Ready for review, I put the app in the launcher folder to keep it in checkbox-ng for packaging needs
Maciej Kisielewski (kissiel) wrote : | # |
My only -1 is on the `._export_results`. I really believe that talking to plainbox internals behind SessionAssistant could be avoided. (See inline comment).
Building tree (create_
But after understanding how the code works I guess that's the cleanest way (reading-wise) of doing it, and the list of categories in the root node *should* be small.
Super minor thing: rev 4121 ‘aa’ typo in commit msg
- 4121. By Sylvain Pineau
-
plainbox:
session: assistant: Reset running_job_name when resuming with autorestart When a session is resumed in autorestart mode, the running_job_name meta
data has to be reset and local self._metadata returned instead of the initial
self._resume_candidates[ session_ id].metadata. - 4122. By Sylvain Pineau
-
plainbox:
session: assistant: Create a checkpoint when calling use_job_result() - 4123. By Sylvain Pineau
-
plainbox:
session: restart: Append $SHELL to xdg desktop file command To avoid closing the terminal when the resume command is over.
- 4124. By Sylvain Pineau
-
plainbox:
session: assistant: Set FLAG_SUBMITTED after calling transport.send() SessionAssistant export_
to_transport( ) should set the flag FLAG_SUBMITTED if
the call to transport.send() succeeded. - 4125. By Sylvain Pineau
-
checkbox-
ng:checkbox- cli2: New CLI application based on Session Assistant Not a true launcher but a standalone application aimed to replace checkbox-cli.
It relies on plainbox new Session Assistant to perform all low level operations.The main differences between this version and current checkbox-cli are:
- support for restart API
- jobs are grouped by categories units (as opposed to Job.via links) in test
selection screens.
Sylvain Pineau (sylvain-pineau) wrote : | # |
_export_results fixed to use sa export to stream. and rev 4121 typo
Sylvain Pineau (sylvain-pineau) wrote : | # |
Time to fix ppa packaging now
Preview Diff
1 | === modified file 'checkbox-ng/checkbox_ng/commands/sru.py' | |||
2 | --- checkbox-ng/checkbox_ng/commands/sru.py 2015-09-07 13:40:15 +0000 | |||
3 | +++ checkbox-ng/checkbox_ng/commands/sru.py 2015-12-02 09:12:28 +0000 | |||
4 | @@ -128,7 +128,7 @@ | |||
5 | 128 | 128 | ||
6 | 129 | def _collect_info(self, rc, sa): | 129 | def _collect_info(self, rc, sa): |
7 | 130 | sa.select_providers('*') | 130 | sa.select_providers('*') |
9 | 131 | sa.start_new_session(_("Hardware Collection Session")) | 131 | sa.start_new_session(_("SRU Session")) |
10 | 132 | sa.select_test_plan(self.config.test_plan) | 132 | sa.select_test_plan(self.config.test_plan) |
11 | 133 | sa.bootstrap() | 133 | sa.bootstrap() |
12 | 134 | for job_id in sa.get_static_todo_list(): | 134 | for job_id in sa.get_static_todo_list(): |
13 | 135 | 135 | ||
14 | === modified file 'checkbox-ng/checkbox_ng/misc.py' | |||
15 | --- checkbox-ng/checkbox_ng/misc.py 2015-10-13 13:41:17 +0000 | |||
16 | +++ checkbox-ng/checkbox_ng/misc.py 2015-12-02 09:12:28 +0000 | |||
17 | @@ -145,6 +145,28 @@ | |||
18 | 145 | builder.auto_add_job(job) | 145 | builder.auto_add_job(job) |
19 | 146 | return builder.root_node | 146 | return builder.root_node |
20 | 147 | 147 | ||
21 | 148 | @classmethod | ||
22 | 149 | def create_simple_tree(cls, sa, job_list): | ||
23 | 150 | """ | ||
24 | 151 | Build a rooted JobTreeNode from a job list. | ||
25 | 152 | |||
26 | 153 | :argument sa: | ||
27 | 154 | A session assistant object | ||
28 | 155 | :argument job_list: | ||
29 | 156 | List of jobs to consider for building the tree. | ||
30 | 157 | """ | ||
31 | 158 | root_node = cls() | ||
32 | 159 | for job in job_list: | ||
33 | 160 | cat_name = sa.get_category(job.category_id).tr_name() | ||
34 | 161 | matches = [n for n in root_node.categories if n.name == cat_name] | ||
35 | 162 | if not matches: | ||
36 | 163 | node = cls(cat_name) | ||
37 | 164 | root_node.add_category(node) | ||
38 | 165 | else: | ||
39 | 166 | node = matches[0] | ||
40 | 167 | node.add_job(job) | ||
41 | 168 | return root_node | ||
42 | 169 | |||
43 | 148 | 170 | ||
44 | 149 | class TreeBuilder: | 171 | class TreeBuilder: |
45 | 150 | 172 | ||
46 | 151 | 173 | ||
47 | === modified file 'checkbox-ng/checkbox_ng/ui.py' | |||
48 | --- checkbox-ng/checkbox_ng/ui.py 2015-06-10 15:04:50 +0000 | |||
49 | +++ checkbox-ng/checkbox_ng/ui.py 2015-12-02 09:12:28 +0000 | |||
50 | @@ -77,12 +77,13 @@ | |||
51 | 77 | """ | 77 | """ |
52 | 78 | Display the appropriate menu and return the selected options | 78 | Display the appropriate menu and return the selected options |
53 | 79 | """ | 79 | """ |
55 | 80 | def __init__(self, title, menu, selection=[0]): | 80 | def __init__(self, title, menu, selection=[0], multiple_allowed=True): |
56 | 81 | self.image = TextImage(Size(0, 0)) | 81 | self.image = TextImage(Size(0, 0)) |
57 | 82 | self.title = title | 82 | self.title = title |
58 | 83 | self.menu = menu | 83 | self.menu = menu |
59 | 84 | self.option_count = len(menu) | 84 | self.option_count = len(menu) |
60 | 85 | self.position = 0 # Zero-based index of the selected menu option | 85 | self.position = 0 # Zero-based index of the selected menu option |
61 | 86 | self.multiple_allowed = multiple_allowed | ||
62 | 86 | if self.option_count: | 87 | if self.option_count: |
63 | 87 | self.selection = selection | 88 | self.selection = selection |
64 | 88 | else: | 89 | else: |
65 | @@ -110,6 +111,8 @@ | |||
66 | 110 | self.selection.remove(self.position) | 111 | self.selection.remove(self.position) |
67 | 111 | elif self.position < self.option_count: | 112 | elif self.position < self.option_count: |
68 | 112 | self.selection.append(self.position) | 113 | self.selection.append(self.position) |
69 | 114 | if not self.multiple_allowed: | ||
70 | 115 | self.selection = [self.position] | ||
71 | 113 | self.repaint(event) | 116 | self.repaint(event) |
72 | 114 | return self.image | 117 | return self.image |
73 | 115 | 118 | ||
74 | 116 | 119 | ||
75 | === added file 'checkbox-ng/launchers/checkbox-cli2' | |||
76 | --- checkbox-ng/launchers/checkbox-cli2 1970-01-01 00:00:00 +0000 | |||
77 | +++ checkbox-ng/launchers/checkbox-cli2 2015-12-02 09:12:28 +0000 | |||
78 | @@ -0,0 +1,596 @@ | |||
79 | 1 | #!/usr/bin/env python3 | ||
80 | 2 | # This file is part of Checkbox. | ||
81 | 3 | # | ||
82 | 4 | # Copyright 2015 Canonical Ltd. | ||
83 | 5 | # Written by: | ||
84 | 6 | # Sylvain Pineau <sylvain.pineau@canonical.com> | ||
85 | 7 | # | ||
86 | 8 | # Checkbox is free software: you can redistribute it and/or modify | ||
87 | 9 | # it under the terms of the GNU General Public License version 3, | ||
88 | 10 | # as published by the Free Software Foundation. | ||
89 | 11 | # | ||
90 | 12 | # Checkbox is distributed in the hope that it will be useful, | ||
91 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
92 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
93 | 15 | # GNU General Public License for more details. | ||
94 | 16 | # | ||
95 | 17 | # You should have received a copy of the GNU General Public License | ||
96 | 18 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
97 | 19 | |||
98 | 20 | """ | ||
99 | 21 | Checkbox CLI Application. | ||
100 | 22 | |||
101 | 23 | WARNING: this is not a launcher interpreter. | ||
102 | 24 | """ | ||
103 | 25 | |||
104 | 26 | from argparse import SUPPRESS | ||
105 | 27 | from shutil import copyfileobj | ||
106 | 28 | import gettext | ||
107 | 29 | import io | ||
108 | 30 | import json | ||
109 | 31 | import logging | ||
110 | 32 | import os | ||
111 | 33 | import sys | ||
112 | 34 | |||
113 | 35 | from guacamole import Command | ||
114 | 36 | from guacamole.core import Ingredient | ||
115 | 37 | from guacamole.ingredients import ansi | ||
116 | 38 | from guacamole.ingredients import argparse | ||
117 | 39 | from guacamole.ingredients import cmdtree | ||
118 | 40 | from guacamole.recipes.cmd import CommandRecipe | ||
119 | 41 | |||
120 | 42 | # TODO: use public APIs here | ||
121 | 43 | from plainbox.abc import IJobResult | ||
122 | 44 | from plainbox.i18n import ngettext | ||
123 | 45 | from plainbox.i18n import pgettext as C_ | ||
124 | 46 | from plainbox.impl.commands.inv_run import seconds_to_human_duration | ||
125 | 47 | from plainbox.impl.commands.inv_run import Action | ||
126 | 48 | from plainbox.impl.commands.inv_run import ActionUI | ||
127 | 49 | from plainbox.impl.commands.inv_run import NormalUI | ||
128 | 50 | from plainbox.impl.commands.inv_run import ReRunJob | ||
129 | 51 | from plainbox.impl.color import Colorizer | ||
130 | 52 | from plainbox.impl.exporter import ByteStringStreamTranslator | ||
131 | 53 | from plainbox.impl.ingredients import CanonicalCrashIngredient | ||
132 | 54 | from plainbox.impl.ingredients import RenderingContextIngredient | ||
133 | 55 | from plainbox.impl.ingredients import SessionAssistantIngredient | ||
134 | 56 | from plainbox.impl.result import tr_outcome | ||
135 | 57 | from plainbox.impl.result import JobResultBuilder | ||
136 | 58 | from plainbox.impl.result import MemoryJobResult | ||
137 | 59 | from plainbox.impl.session.assistant import SA_RESTARTABLE | ||
138 | 60 | from plainbox.impl.session.jobs import InhibitionCause | ||
139 | 61 | from plainbox.vendor.textland import get_display | ||
140 | 62 | |||
141 | 63 | from checkbox_ng.misc import SelectableJobTreeNode | ||
142 | 64 | from checkbox_ng.ui import ScrollableTreeNode | ||
143 | 65 | from checkbox_ng.ui import ShowMenu | ||
144 | 66 | from checkbox_ng.ui import ShowRerun | ||
145 | 67 | |||
146 | 68 | |||
147 | 69 | _ = gettext.gettext | ||
148 | 70 | |||
149 | 71 | _logger = logging.getLogger("checkbox-cli") | ||
150 | 72 | |||
151 | 73 | |||
152 | 74 | class DisplayIngredient(Ingredient): | ||
153 | 75 | |||
154 | 76 | """Ingredient that adds a Textland display to guacamole.""" | ||
155 | 77 | |||
156 | 78 | def late_init(self, context): | ||
157 | 79 | """Add a DisplayIngredient as ``display`` to the guacamole context.""" | ||
158 | 80 | context.display = get_display() | ||
159 | 81 | |||
160 | 82 | |||
161 | 83 | class CheckboxCommandRecipe(CommandRecipe): | ||
162 | 84 | |||
163 | 85 | """A recipe for using Checkbox-enhanced commands.""" | ||
164 | 86 | |||
165 | 87 | def get_ingredients(self): | ||
166 | 88 | """Get a list of ingredients for guacamole.""" | ||
167 | 89 | return [ | ||
168 | 90 | cmdtree.CommandTreeBuilder(self.command), | ||
169 | 91 | cmdtree.CommandTreeDispatcher(), | ||
170 | 92 | argparse.ParserIngredient(), | ||
171 | 93 | CanonicalCrashIngredient(), | ||
172 | 94 | ansi.ANSIIngredient(), | ||
173 | 95 | SessionAssistantIngredient(), | ||
174 | 96 | RenderingContextIngredient(), | ||
175 | 97 | DisplayIngredient() | ||
176 | 98 | ] | ||
177 | 99 | |||
178 | 100 | |||
179 | 101 | class CheckboxUI(NormalUI): | ||
180 | 102 | |||
181 | 103 | def considering_job(self, job, job_state): | ||
182 | 104 | pass | ||
183 | 105 | |||
184 | 106 | |||
185 | 107 | class CheckboxCommand(Command): | ||
186 | 108 | |||
187 | 109 | """ | ||
188 | 110 | A command with Checkbox-enhanced ingredients. | ||
189 | 111 | |||
190 | 112 | This command has two additional items in the guacamole execution context, | ||
191 | 113 | the :class:`DisplayIngredient` object ``display`` and the | ||
192 | 114 | :class:`SessionAssistant` object ``sa``. | ||
193 | 115 | """ | ||
194 | 116 | |||
195 | 117 | bug_report_url = "https://bugs.launchpad.net/checkbox-ng/+filebug" | ||
196 | 118 | |||
197 | 119 | def main(self, argv=None, exit=True): | ||
198 | 120 | """ | ||
199 | 121 | Shortcut for running a command. | ||
200 | 122 | |||
201 | 123 | See :meth:`guacamole.recipes.Recipe.main()` for details. | ||
202 | 124 | """ | ||
203 | 125 | return CheckboxCommandRecipe(self).main(argv, exit) | ||
204 | 126 | |||
205 | 127 | |||
206 | 128 | class checkbox_cli(CheckboxCommand): | ||
207 | 129 | |||
208 | 130 | """Tool to run Checkbox jobs interactively from the command line.""" | ||
209 | 131 | |||
210 | 132 | app_id = 'checkbox-cli' | ||
211 | 133 | |||
212 | 134 | def get_sa_api_version(self): | ||
213 | 135 | return '0.99' | ||
214 | 136 | |||
215 | 137 | def get_sa_api_flags(self): | ||
216 | 138 | return (SA_RESTARTABLE,) | ||
217 | 139 | |||
218 | 140 | def register_arguments(self, parser): | ||
219 | 141 | """Method called to register command line arguments.""" | ||
220 | 142 | parser.add_argument( | ||
221 | 143 | '-t', '--test-plan', action="store", metavar=_("TEST-PLAN-ID"), | ||
222 | 144 | default=None, | ||
223 | 145 | # TRANSLATORS: this is in imperative form | ||
224 | 146 | help=_("load the specified test plan")) | ||
225 | 147 | parser.add_argument( | ||
226 | 148 | '--secure_id', metavar="SECURE_ID", | ||
227 | 149 | help=_("Canonical hardware identifier (optional)")) | ||
228 | 150 | parser.add_argument( | ||
229 | 151 | '--non-interactive', action='store_true', | ||
230 | 152 | help=_("skip tests that require interactivity")) | ||
231 | 153 | parser.add_argument( | ||
232 | 154 | '--dont-suppress-output', action="store_true", default=False, | ||
233 | 155 | help=_("don't suppress the output of certain job plugin types")) | ||
234 | 156 | parser.add_argument( | ||
235 | 157 | '--staging', action='store_true', default=False, | ||
236 | 158 | # Hide staging from help message (See pad.lv/1350005) | ||
237 | 159 | help=SUPPRESS) | ||
238 | 160 | parser.add_argument( | ||
239 | 161 | '--resume', dest='session_id', metavar="SESSION_ID", | ||
240 | 162 | help=SUPPRESS) | ||
241 | 163 | |||
242 | 164 | def invoked(self, ctx): | ||
243 | 165 | """Method called when the command is invoked.""" | ||
244 | 166 | self.ctx = ctx | ||
245 | 167 | self.transport = self._create_transport() | ||
246 | 168 | self.C = Colorizer() | ||
247 | 169 | try: | ||
248 | 170 | self._do_normal_sequence() | ||
249 | 171 | self._export_results() | ||
250 | 172 | if ctx.args.secure_id: | ||
251 | 173 | self._send_results() | ||
252 | 174 | ctx.sa.finalize_session() | ||
253 | 175 | except KeyboardInterrupt: | ||
254 | 176 | return 1 | ||
255 | 177 | |||
256 | 178 | def _export_results(self): | ||
257 | 179 | if self.is_interactive: | ||
258 | 180 | print(self.C.header(_("Results"))) | ||
259 | 181 | # This requires a bit more finesse, as exporters output bytes | ||
260 | 182 | # and stdout needs a string. | ||
261 | 183 | translating_stream = ByteStringStreamTranslator( | ||
262 | 184 | sys.stdout, "utf-8") | ||
263 | 185 | self.ctx.sa.export_to_stream( | ||
264 | 186 | '2013.com.canonical.plainbox::text', (), translating_stream) | ||
265 | 187 | base_dir = os.path.join( | ||
266 | 188 | os.getenv( | ||
267 | 189 | 'XDG_DATA_HOME', os.path.expanduser("~/.local/share/")), | ||
268 | 190 | "checkbox-ng") | ||
269 | 191 | if not os.path.exists(base_dir): | ||
270 | 192 | os.makedirs(base_dir) | ||
271 | 193 | exp_options = ['with-sys-info', 'with-summary', 'with-job-description', | ||
272 | 194 | 'with-text-attachments', 'with-certification-status', | ||
273 | 195 | 'with-job-defs', 'with-io-log', 'with-comments'] | ||
274 | 196 | exporters = [ | ||
275 | 197 | '2013.com.canonical.plainbox::hexr', | ||
276 | 198 | '2013.com.canonical.plainbox::html', | ||
277 | 199 | '2013.com.canonical.plainbox::xlsx', | ||
278 | 200 | '2013.com.canonical.plainbox::json', | ||
279 | 201 | ] | ||
280 | 202 | print() | ||
281 | 203 | for unit_name in exporters: | ||
282 | 204 | results_path = self.ctx.sa.export_to_file(unit_name, exp_options, | ||
283 | 205 | base_dir) | ||
284 | 206 | print("file://{}".format(results_path)) | ||
285 | 207 | |||
286 | 208 | def _send_results(self): | ||
287 | 209 | print() | ||
288 | 210 | print(_("Sending hardware report to Canonical Certification")) | ||
289 | 211 | print(_("Server URL is: {0}").format(self.transport.url)) | ||
290 | 212 | result = self.ctx.sa.export_to_transport( | ||
291 | 213 | "2013.com.canonical.plainbox::hexr", self.transport) | ||
292 | 214 | if 'url' in result: | ||
293 | 215 | print(result['url']) | ||
294 | 216 | |||
295 | 217 | def _create_transport(self): | ||
296 | 218 | if self.ctx.args.secure_id: | ||
297 | 219 | return self.ctx.sa.get_canonical_certification_transport( | ||
298 | 220 | self.ctx.args.secure_id, staging=self.ctx.args.staging) | ||
299 | 221 | |||
300 | 222 | def _get_interactively_picked_testplans(self): | ||
301 | 223 | test_plan_ids = self.ctx.sa.get_test_plans() | ||
302 | 224 | test_plan_names = [self.ctx.sa.get_test_plan(tp_id).name for tp_id in | ||
303 | 225 | test_plan_ids] | ||
304 | 226 | try: | ||
305 | 227 | selected_index = self.ctx.display.run( | ||
306 | 228 | ShowMenu(_("Select test plan"), | ||
307 | 229 | test_plan_names, [], | ||
308 | 230 | multiple_allowed=False))[0] | ||
309 | 231 | except IndexError: | ||
310 | 232 | return None | ||
311 | 233 | return test_plan_ids[selected_index] | ||
312 | 234 | |||
313 | 235 | def _interactively_pick_jobs_to_run(self): | ||
314 | 236 | job_list = [self.ctx.sa.get_job(job_id) for job_id in | ||
315 | 237 | self.ctx.sa.get_static_todo_list()] | ||
316 | 238 | tree = SelectableJobTreeNode.create_simple_tree(self.ctx.sa, job_list) | ||
317 | 239 | title = _('Choose tests to run on your system:') | ||
318 | 240 | self.ctx.display.run(ScrollableTreeNode(tree, title)) | ||
319 | 241 | # NOTE: tree.selection is correct but ordered badly. To retain | ||
320 | 242 | # the original ordering we should just treat it as a mask and | ||
321 | 243 | # use it to filter jobs from get_static_todo_list. | ||
322 | 244 | wanted_set = frozenset([job.id for job in tree.selection]) | ||
323 | 245 | job_id_list = [job_id for job_id in self.ctx.sa.get_static_todo_list() | ||
324 | 246 | if job_id in wanted_set] | ||
325 | 247 | self.ctx.sa.use_alternate_selection(job_id_list) | ||
326 | 248 | |||
327 | 249 | @property | ||
328 | 250 | def is_interactive(self): | ||
329 | 251 | """ | ||
330 | 252 | Flag indicating that this is an interactive invocation. | ||
331 | 253 | |||
332 | 254 | We can then interact with the user when we encounter OUTCOME_UNDECIDED. | ||
333 | 255 | """ | ||
334 | 256 | return (sys.stdin.isatty() and sys.stdout.isatty() and not | ||
335 | 257 | self.ctx.args.non_interactive) | ||
336 | 258 | |||
337 | 259 | def _get_ui_for_job(self, job): | ||
338 | 260 | if self.ctx.args.dont_suppress_output is False and job.plugin in ( | ||
339 | 261 | 'local', 'resource', 'attachment'): | ||
340 | 262 | return CheckboxUI(self.C.c, show_cmd_output=False) | ||
341 | 263 | else: | ||
342 | 264 | return CheckboxUI(self.C.c, show_cmd_output=True) | ||
343 | 265 | |||
344 | 266 | def _run_single_job_with_ui_loop(self, job, ui): | ||
345 | 267 | print(self.C.header(job.tr_summary(), fill='-')) | ||
346 | 268 | print(_("ID: {0}").format(job.id)) | ||
347 | 269 | print(_("Category: {0}").format( | ||
348 | 270 | self.ctx.sa.get_job_state(job.id).effective_category_id)) | ||
349 | 271 | comments = "" | ||
350 | 272 | while True: | ||
351 | 273 | if job.plugin in ('user-interact', 'user-interact-verify', | ||
352 | 274 | 'user-verify', 'manual'): | ||
353 | 275 | ui.notify_about_purpose(job) | ||
354 | 276 | if (self.is_interactive and | ||
355 | 277 | job.plugin in ('user-interact', | ||
356 | 278 | 'user-interact-verify', | ||
357 | 279 | 'manual')): | ||
358 | 280 | ui.notify_about_steps(job) | ||
359 | 281 | if job.plugin == 'manual': | ||
360 | 282 | cmd = 'run' | ||
361 | 283 | else: | ||
362 | 284 | cmd = ui.wait_for_interaction_prompt(job) | ||
363 | 285 | if cmd == 'run' or cmd is None: | ||
364 | 286 | result_builder = self.ctx.sa.run_job(job.id, ui, False) | ||
365 | 287 | elif cmd == 'comment': | ||
366 | 288 | new_comment = input(self.C.BLUE( | ||
367 | 289 | _('Please enter your comments:') + '\n')) | ||
368 | 290 | if new_comment: | ||
369 | 291 | comments += new_comment + '\n' | ||
370 | 292 | continue | ||
371 | 293 | elif cmd == 'skip': | ||
372 | 294 | result_builder = JobResultBuilder( | ||
373 | 295 | outcome=IJobResult.OUTCOME_SKIP, | ||
374 | 296 | comments=_("Explicitly skipped before" | ||
375 | 297 | " execution")) | ||
376 | 298 | if comments != "": | ||
377 | 299 | result_builder.comments = comments | ||
378 | 300 | break | ||
379 | 301 | elif cmd == 'quit': | ||
380 | 302 | raise SystemExit() | ||
381 | 303 | else: | ||
382 | 304 | result_builder = self.ctx.sa.run_job(job.id, ui, False) | ||
383 | 305 | else: | ||
384 | 306 | if 'noreturn' in job.get_flag_set(): | ||
385 | 307 | ui.noreturn_job() | ||
386 | 308 | result_builder = self.ctx.sa.run_job(job.id, ui, False) | ||
387 | 309 | if (self.is_interactive and | ||
388 | 310 | result_builder.outcome == IJobResult.OUTCOME_UNDECIDED): | ||
389 | 311 | try: | ||
390 | 312 | if comments != "": | ||
391 | 313 | result_builder.comments = comments | ||
392 | 314 | ui.notify_about_verification(job) | ||
393 | 315 | self._interaction_callback(job, result_builder) | ||
394 | 316 | except ReRunJob: | ||
395 | 317 | self.ctx.sa.use_job_result(job.id, | ||
396 | 318 | result_builder.get_result()) | ||
397 | 319 | continue | ||
398 | 320 | break | ||
399 | 321 | return result_builder | ||
400 | 322 | |||
401 | 323 | def _pick_action_cmd(self, action_list, prompt=None): | ||
402 | 324 | return ActionUI(action_list, prompt).run() | ||
403 | 325 | |||
404 | 326 | def _interaction_callback(self, job, result_builder, | ||
405 | 327 | prompt=None, allowed_outcome=None): | ||
406 | 328 | result = result_builder.get_result() | ||
407 | 329 | if prompt is None: | ||
408 | 330 | prompt = _("Select an outcome or an action: ") | ||
409 | 331 | if allowed_outcome is None: | ||
410 | 332 | allowed_outcome = [IJobResult.OUTCOME_PASS, | ||
411 | 333 | IJobResult.OUTCOME_FAIL, | ||
412 | 334 | IJobResult.OUTCOME_SKIP] | ||
413 | 335 | allowed_actions = [ | ||
414 | 336 | Action('c', _('add a comment'), 'set-comments') | ||
415 | 337 | ] | ||
416 | 338 | if IJobResult.OUTCOME_PASS in allowed_outcome: | ||
417 | 339 | allowed_actions.append( | ||
418 | 340 | Action('p', _('set outcome to {0}').format( | ||
419 | 341 | self.C.GREEN(C_('set outcome to <pass>', 'pass'))), | ||
420 | 342 | 'set-pass')) | ||
421 | 343 | if IJobResult.OUTCOME_FAIL in allowed_outcome: | ||
422 | 344 | allowed_actions.append( | ||
423 | 345 | Action('f', _('set outcome to {0}').format( | ||
424 | 346 | self.C.RED(C_('set outcome to <fail>', 'fail'))), | ||
425 | 347 | 'set-fail')) | ||
426 | 348 | if IJobResult.OUTCOME_SKIP in allowed_outcome: | ||
427 | 349 | allowed_actions.append( | ||
428 | 350 | Action('s', _('set outcome to {0}').format( | ||
429 | 351 | self.C.YELLOW(C_('set outcome to <skip>', 'skip'))), | ||
430 | 352 | 'set-skip')) | ||
431 | 353 | if job.command is not None: | ||
432 | 354 | allowed_actions.append( | ||
433 | 355 | Action('r', _('re-run this job'), 're-run')) | ||
434 | 356 | if result.return_code is not None: | ||
435 | 357 | if result.return_code == 0: | ||
436 | 358 | suggested_outcome = IJobResult.OUTCOME_PASS | ||
437 | 359 | else: | ||
438 | 360 | suggested_outcome = IJobResult.OUTCOME_FAIL | ||
439 | 361 | allowed_actions.append( | ||
440 | 362 | Action('', _('set suggested outcome [{0}]').format( | ||
441 | 363 | tr_outcome(suggested_outcome)), 'set-suggested')) | ||
442 | 364 | while result.outcome not in allowed_outcome: | ||
443 | 365 | print(_("Please decide what to do next:")) | ||
444 | 366 | print(" " + _("outcome") + ": {0}".format( | ||
445 | 367 | self.C.result(result))) | ||
446 | 368 | if result.comments is None: | ||
447 | 369 | print(" " + _("comments") + ": {0}".format( | ||
448 | 370 | C_("none comment", "none"))) | ||
449 | 371 | else: | ||
450 | 372 | print(" " + _("comments") + ": {0}".format( | ||
451 | 373 | self.C.CYAN(result.comments, bright=False))) | ||
452 | 374 | cmd = self._pick_action_cmd(allowed_actions) | ||
453 | 375 | if cmd == 'set-pass': | ||
454 | 376 | result_builder.outcome = IJobResult.OUTCOME_PASS | ||
455 | 377 | elif cmd == 'set-fail': | ||
456 | 378 | result_builder.outcome = IJobResult.OUTCOME_FAIL | ||
457 | 379 | elif cmd == 'set-skip' or cmd is None: | ||
458 | 380 | result_builder.outcome = IJobResult.OUTCOME_SKIP | ||
459 | 381 | elif cmd == 'set-suggested': | ||
460 | 382 | result_builder.outcome = suggested_outcome | ||
461 | 383 | elif cmd == 'set-comments': | ||
462 | 384 | new_comment = input(self.C.BLUE( | ||
463 | 385 | _('Please enter your comments:') + '\n')) | ||
464 | 386 | if new_comment: | ||
465 | 387 | result_builder.add_comment(new_comment) | ||
466 | 388 | elif cmd == 're-run': | ||
467 | 389 | raise ReRunJob | ||
468 | 390 | result = result_builder.get_result() | ||
469 | 391 | |||
470 | 392 | def _run_jobs(self, jobs_to_run): | ||
471 | 393 | estimated_time = 0 | ||
472 | 394 | for job_id in jobs_to_run: | ||
473 | 395 | job = self.ctx.sa.get_job(job_id) | ||
474 | 396 | if (job.estimated_duration is not None | ||
475 | 397 | and estimated_time is not None): | ||
476 | 398 | estimated_time += job.estimated_duration | ||
477 | 399 | else: | ||
478 | 400 | estimated_time = None | ||
479 | 401 | for job_no, job_id in enumerate(jobs_to_run, start=1): | ||
480 | 402 | print(self.C.header( | ||
481 | 403 | _('Running job {} / {}. Estimated time left: {}').format( | ||
482 | 404 | job_no, len(jobs_to_run), | ||
483 | 405 | seconds_to_human_duration(max(0, estimated_time)) | ||
484 | 406 | if estimated_time is not None else _("unknown")), | ||
485 | 407 | fill='-')) | ||
486 | 408 | job = self.ctx.sa.get_job(job_id) | ||
487 | 409 | builder = self._run_single_job_with_ui_loop( | ||
488 | 410 | job, self._get_ui_for_job(job)) | ||
489 | 411 | result = builder.get_result() | ||
490 | 412 | self.ctx.sa.use_job_result(job_id, result) | ||
491 | 413 | if (job.estimated_duration is not None | ||
492 | 414 | and estimated_time is not None): | ||
493 | 415 | estimated_time -= job.estimated_duration | ||
494 | 416 | |||
495 | 417 | def _get_rerun_candidates(self): | ||
496 | 418 | """Get all the tests that might be selected for rerunning.""" | ||
497 | 419 | def rerun_predicate(job_state): | ||
498 | 420 | return job_state.result.outcome in ( | ||
499 | 421 | IJobResult.OUTCOME_FAIL, IJobResult.OUTCOME_CRASH, | ||
500 | 422 | IJobResult.OUTCOME_NOT_SUPPORTED, IJobResult.OUTCOME_SKIP) | ||
501 | 423 | rerun_candidates = [] | ||
502 | 424 | todo_list = self.ctx.sa.get_static_todo_list() | ||
503 | 425 | job_states = {job_id: self.ctx.sa.get_job_state(job_id) for job_id | ||
504 | 426 | in todo_list} | ||
505 | 427 | for job_id, job_state in job_states.items(): | ||
506 | 428 | if rerun_predicate(job_state): | ||
507 | 429 | rerun_candidates.append(self.ctx.sa.get_job(job_id)) | ||
508 | 430 | return rerun_candidates | ||
509 | 431 | |||
510 | 432 | def _maybe_rerun_jobs(self): | ||
511 | 433 | # create a list of jobs that qualify for rerunning | ||
512 | 434 | rerun_candidates = self._get_rerun_candidates() | ||
513 | 435 | # bail-out early if no job qualifies for rerunning | ||
514 | 436 | if not rerun_candidates: | ||
515 | 437 | return False | ||
516 | 438 | tree = SelectableJobTreeNode.create_simple_tree(self.ctx.sa, | ||
517 | 439 | rerun_candidates) | ||
518 | 440 | # nothing to select in root node and categories - bailing out | ||
519 | 441 | if not tree.jobs and not tree._categories: | ||
520 | 442 | return False | ||
521 | 443 | # deselect all by default | ||
522 | 444 | tree.set_descendants_state(False) | ||
523 | 445 | self.ctx.display.run(ShowRerun(tree, _("Select jobs to re-run"))) | ||
524 | 446 | wanted_set = frozenset(tree.selection) | ||
525 | 447 | if not wanted_set: | ||
526 | 448 | # nothing selected - nothing to run | ||
527 | 449 | return False | ||
528 | 450 | rerun_candidates = [] | ||
529 | 451 | # include resource jobs that selected jobs depend on | ||
530 | 452 | resources_to_rerun = [] | ||
531 | 453 | for job in wanted_set: | ||
532 | 454 | job_state = self.ctx.sa.get_job_state(job.id) | ||
533 | 455 | for inhibitor in job_state.readiness_inhibitor_list: | ||
534 | 456 | if inhibitor.cause == InhibitionCause.FAILED_DEP: | ||
535 | 457 | resources_to_rerun.append(inhibitor.related_job) | ||
536 | 458 | # reset outcome of jobs that are selected for re-running | ||
537 | 459 | for job in list(wanted_set) + resources_to_rerun: | ||
538 | 460 | self.ctx.sa.get_job_state(job.id).result = MemoryJobResult({}) | ||
539 | 461 | rerun_candidates.append(job.id) | ||
540 | 462 | self._run_jobs(rerun_candidates) | ||
541 | 463 | return True | ||
542 | 464 | |||
543 | 465 | def _do_normal_sequence(self): | ||
544 | 466 | self.ctx.sa.select_providers("*") | ||
545 | 467 | self.ctx.sa.configure_application_restart( | ||
546 | 468 | lambda session_id: [ | ||
547 | 469 | 'sh', '-c', ' '.join([ | ||
548 | 470 | os.path.abspath(__file__), | ||
549 | 471 | "--resume", session_id]) | ||
550 | 472 | ]) | ||
551 | 473 | resumed = self._maybe_resume_session() | ||
552 | 474 | if not resumed: | ||
553 | 475 | print(_("Preparing...")) | ||
554 | 476 | self.ctx.sa.start_new_session(_("Checkbox CLI Session")) | ||
555 | 477 | testplan_id = None | ||
556 | 478 | if self.ctx.args.test_plan: | ||
557 | 479 | if self.ctx.args.test_plan in self.ctx.sa.get_test_plans(): | ||
558 | 480 | testplan_id = self.ctx.args.test_plan | ||
559 | 481 | elif self.is_interactive: | ||
560 | 482 | testplan_id = self._get_interactively_picked_testplans() | ||
561 | 483 | if not testplan_id: | ||
562 | 484 | self.ctx.rc.reset() | ||
563 | 485 | self.ctx.rc.bg = 'red' | ||
564 | 486 | self.ctx.rc.fg = 'bright_white' | ||
565 | 487 | self.ctx.rc.bold = 1 | ||
566 | 488 | self.ctx.rc.para(_("Test plan not found!")) | ||
567 | 489 | raise SystemExit(1) | ||
568 | 490 | self.ctx.sa.select_test_plan(testplan_id) | ||
569 | 491 | self.ctx.sa.update_app_blob(json.dumps( | ||
570 | 492 | {'testplan_id': testplan_id, }).encode("UTF-8")) | ||
571 | 493 | self.ctx.sa.bootstrap() | ||
572 | 494 | if self.is_interactive: | ||
573 | 495 | self._interactively_pick_jobs_to_run() | ||
574 | 496 | self._run_jobs(self.ctx.sa.get_dynamic_todo_list()) | ||
575 | 497 | if self.is_interactive: | ||
576 | 498 | while True: | ||
577 | 499 | if self._maybe_rerun_jobs(): | ||
578 | 500 | continue | ||
579 | 501 | else: | ||
580 | 502 | break | ||
581 | 503 | |||
582 | 504 | def _handle_last_job_after_resume(self, metadata): | ||
583 | 505 | last_job = metadata.running_job_name | ||
584 | 506 | if last_job is None: | ||
585 | 507 | return | ||
586 | 508 | print(_("Previous session run tried to execute job: {}").format( | ||
587 | 509 | last_job)) | ||
588 | 510 | cmd = self._pick_action_cmd([ | ||
589 | 511 | Action('s', _("skip that job"), 'skip'), | ||
590 | 512 | Action('p', _("mark it as passed and continue"), 'pass'), | ||
591 | 513 | Action('f', _("mark it as failed and continue"), 'fail'), | ||
592 | 514 | Action('r', _("run it again"), 'run'), | ||
593 | 515 | ], _("What do you want to do with that job?")) | ||
594 | 516 | if cmd == 'skip' or cmd is None: | ||
595 | 517 | result = MemoryJobResult({ | ||
596 | 518 | 'outcome': IJobResult.OUTCOME_SKIP, | ||
597 | 519 | 'comments': _("Skipped after resuming execution") | ||
598 | 520 | }) | ||
599 | 521 | elif cmd == 'pass': | ||
600 | 522 | result = MemoryJobResult({ | ||
601 | 523 | 'outcome': IJobResult.OUTCOME_PASS, | ||
602 | 524 | 'comments': _("Passed after resuming execution") | ||
603 | 525 | }) | ||
604 | 526 | elif cmd == 'fail': | ||
605 | 527 | result = MemoryJobResult({ | ||
606 | 528 | 'outcome': IJobResult.OUTCOME_FAIL, | ||
607 | 529 | 'comments': _("Failed after resuming execution") | ||
608 | 530 | }) | ||
609 | 531 | elif cmd == 'run': | ||
610 | 532 | result = None | ||
611 | 533 | if result: | ||
612 | 534 | self.ctx.sa.use_job_result(last_job, result) | ||
613 | 535 | |||
614 | 536 | def _maybe_resume_session(self): | ||
615 | 537 | # Try to use the first session that can be resumed if the user agrees | ||
616 | 538 | resume_candidates = list(self.ctx.sa.get_resumable_sessions()) | ||
617 | 539 | resumed = False | ||
618 | 540 | if resume_candidates: | ||
619 | 541 | if self.ctx.args.session_id: | ||
620 | 542 | for candidate in resume_candidates: | ||
621 | 543 | if candidate.id == self.args.session_id: | ||
622 | 544 | resume_candidates = (candidate, ) | ||
623 | 545 | break | ||
624 | 546 | else: | ||
625 | 547 | raise RuntimeError("Requested session is not resumable!") | ||
626 | 548 | elif self.is_interactive: | ||
627 | 549 | print(self.C.header(_("Resume Incomplete Session"))) | ||
628 | 550 | print(ngettext( | ||
629 | 551 | "There is {0} incomplete session that might be resumed", | ||
630 | 552 | "There are {0} incomplete sessions that might be resumed", | ||
631 | 553 | len(resume_candidates) | ||
632 | 554 | ).format(len(resume_candidates))) | ||
633 | 555 | else: | ||
634 | 556 | return | ||
635 | 557 | for candidate in resume_candidates: | ||
636 | 558 | if self.ctx.args.session_id: | ||
637 | 559 | cmd = 'resume' | ||
638 | 560 | else: | ||
639 | 561 | # Skip sessions that the user doesn't want to resume | ||
640 | 562 | cmd = self._pick_action_cmd([ | ||
641 | 563 | Action('r', _("resume this session"), 'resume'), | ||
642 | 564 | Action('n', _("next session"), 'next'), | ||
643 | 565 | Action('c', _("create new session"), 'create') | ||
644 | 566 | ], _("Do you want to resume session {0!a}?").format( | ||
645 | 567 | candidate.id)) | ||
646 | 568 | if cmd == 'next': | ||
647 | 569 | continue | ||
648 | 570 | elif cmd == 'create' or cmd is None: | ||
649 | 571 | break | ||
650 | 572 | elif cmd == 'resume': | ||
651 | 573 | metadata = self.ctx.sa.resume_session(candidate.id) | ||
652 | 574 | app_blob = json.loads(metadata.app_blob.decode("UTF-8")) | ||
653 | 575 | test_plan_id = app_blob['testplan_id'] | ||
654 | 576 | # FIXME selecting again the testplan on resume resets both | ||
655 | 577 | # the static and dynamic todo lists. | ||
656 | 578 | # We're then saving the selection from the saved run_list | ||
657 | 579 | # by accessing the session private context object. | ||
658 | 580 | selected_id_list = [job.id for job in | ||
659 | 581 | self.ctx.sa._context.state.run_list] | ||
660 | 582 | self.ctx.sa.select_test_plan(test_plan_id) | ||
661 | 583 | self.ctx.sa.bootstrap() | ||
662 | 584 | self.ctx.sa.use_alternate_selection(selected_id_list) | ||
663 | 585 | # If we resumed maybe not rerun the same, probably broken | ||
664 | 586 | # job | ||
665 | 587 | self._handle_last_job_after_resume(metadata) | ||
666 | 588 | self._run_jobs(self.ctx.sa.get_dynamic_todo_list()) | ||
667 | 589 | resumed = True | ||
668 | 590 | # Finally ignore other sessions that can be resumed | ||
669 | 591 | break | ||
670 | 592 | return resumed | ||
671 | 593 | |||
672 | 594 | |||
673 | 595 | if __name__ == '__main__': | ||
674 | 596 | checkbox_cli().main() | ||
675 | 0 | 597 | ||
676 | === modified file 'plainbox/plainbox/impl/session/assistant.py' | |||
677 | --- plainbox/plainbox/impl/session/assistant.py 2015-11-04 14:25:10 +0000 | |||
678 | +++ plainbox/plainbox/impl/session/assistant.py 2015-12-02 09:12:28 +0000 | |||
679 | @@ -551,6 +551,8 @@ | |||
680 | 551 | io_log_filename=self._runner.get_record_path_for_job(job), | 551 | io_log_filename=self._runner.get_record_path_for_job(job), |
681 | 552 | ).get_result() | 552 | ).get_result() |
682 | 553 | self._context.state.update_job_result(job, result) | 553 | self._context.state.update_job_result(job, result) |
683 | 554 | self._metadata.running_job_name = None | ||
684 | 555 | self._manager.checkpoint() | ||
685 | 554 | if self._restart_strategy is not None: | 556 | if self._restart_strategy is not None: |
686 | 555 | self._restart_strategy.diffuse_application_restart(self._app_id) | 557 | self._restart_strategy.diffuse_application_restart(self._app_id) |
687 | 556 | self.session_available(self._manager.storage.id) | 558 | self.session_available(self._manager.storage.id) |
688 | @@ -558,7 +560,7 @@ | |||
689 | 558 | UsageExpectation.of(self).allowed_calls = { | 560 | UsageExpectation.of(self).allowed_calls = { |
690 | 559 | self.select_test_plan: "to save test plan selection", | 561 | self.select_test_plan: "to save test plan selection", |
691 | 560 | } | 562 | } |
693 | 561 | return self._resume_candidates[session_id].metadata | 563 | return self._metadata |
694 | 562 | 564 | ||
695 | 563 | @raises(UnexpectedMethodCall) | 565 | @raises(UnexpectedMethodCall) |
696 | 564 | def get_resumable_sessions(self) -> 'Tuple[str, SessionMetaData]': | 566 | def get_resumable_sessions(self) -> 'Tuple[str, SessionMetaData]': |
697 | @@ -1187,6 +1189,7 @@ | |||
698 | 1187 | UsageExpectation.of(self).enforce() | 1189 | UsageExpectation.of(self).enforce() |
699 | 1188 | job = self._context.get_unit(job_id, 'job') | 1190 | job = self._context.get_unit(job_id, 'job') |
700 | 1189 | self._context.state.update_job_result(job, result) | 1191 | self._context.state.update_job_result(job, result) |
701 | 1192 | self._manager.checkpoint() | ||
702 | 1190 | # Set up expectations so that run_job() and use_job_result() must be | 1193 | # Set up expectations so that run_job() and use_job_result() must be |
703 | 1191 | # called in pairs and applications cannot just forget and call | 1194 | # called in pairs and applications cannot just forget and call |
704 | 1192 | # run_job() all the time. | 1195 | # run_job() all the time. |
705 | @@ -1284,7 +1287,11 @@ | |||
706 | 1284 | exported_stream = io.BytesIO() | 1287 | exported_stream = io.BytesIO() |
707 | 1285 | exporter.dump_from_session_manager(self._manager, exported_stream) | 1288 | exporter.dump_from_session_manager(self._manager, exported_stream) |
708 | 1286 | exported_stream.seek(0) | 1289 | exported_stream.seek(0) |
710 | 1287 | return transport.send(exported_stream) | 1290 | result = transport.send(exported_stream) |
711 | 1291 | if SessionMetaData.FLAG_SUBMITTED not in self._metadata.flags: | ||
712 | 1292 | self._metadata.flags.add(SessionMetaData.FLAG_SUBMITTED) | ||
713 | 1293 | self._manager.checkpoint() | ||
714 | 1294 | return result | ||
715 | 1288 | 1295 | ||
716 | 1289 | @raises(KeyError, OSError) | 1296 | @raises(KeyError, OSError) |
717 | 1290 | def export_to_file( | 1297 | def export_to_file( |
718 | @@ -1312,7 +1319,8 @@ | |||
719 | 1312 | When there is a problem when writing the output. | 1319 | When there is a problem when writing the output. |
720 | 1313 | """ | 1320 | """ |
721 | 1314 | UsageExpectation.of(self).enforce() | 1321 | UsageExpectation.of(self).enforce() |
723 | 1315 | exporter = self._manager.create_exporter(exporter_id, option_list) | 1322 | exporter = self._manager.create_exporter(exporter_id, option_list, |
724 | 1323 | strict=False) | ||
725 | 1316 | timestamp = datetime.datetime.utcnow().isoformat() | 1324 | timestamp = datetime.datetime.utcnow().isoformat() |
726 | 1317 | path = os.path.join(dir_path, ''.join( | 1325 | path = os.path.join(dir_path, ''.join( |
727 | 1318 | ['submission_', timestamp, '.', exporter.unit.file_extension])) | 1326 | ['submission_', timestamp, '.', exporter.unit.file_extension])) |
728 | 1319 | 1327 | ||
729 | === modified file 'plainbox/plainbox/impl/session/restart.py' | |||
730 | --- plainbox/plainbox/impl/session/restart.py 2015-11-04 14:25:10 +0000 | |||
731 | +++ plainbox/plainbox/impl/session/restart.py 2015-12-02 09:12:28 +0000 | |||
732 | @@ -75,6 +75,7 @@ | |||
733 | 75 | The command callback | 75 | The command callback |
734 | 76 | """ | 76 | """ |
735 | 77 | self.config = config = PlainBoxConfigParser() | 77 | self.config = config = PlainBoxConfigParser() |
736 | 78 | self.app_terminal = app_terminal | ||
737 | 78 | section = 'Desktop Entry' | 79 | section = 'Desktop Entry' |
738 | 79 | config.add_section(section) | 80 | config.add_section(section) |
739 | 80 | config.set(section, 'Type', 'Application') | 81 | config.set(section, 'Type', 'Application') |
740 | @@ -99,6 +100,8 @@ | |||
741 | 99 | 100 | ||
742 | 100 | def prime_application_restart(self, app_id: str, cmd: str) -> None: | 101 | def prime_application_restart(self, app_id: str, cmd: str) -> None: |
743 | 101 | filename = self.get_desktop_filename(app_id) | 102 | filename = self.get_desktop_filename(app_id) |
744 | 103 | if self.app_terminal: | ||
745 | 104 | cmd += ';$SHELL' | ||
746 | 102 | self.config.set('Desktop Entry', 'Exec', cmd) | 105 | self.config.set('Desktop Entry', 'Exec', cmd) |
747 | 103 | os.makedirs(os.path.dirname(filename), exist_ok=True) | 106 | os.makedirs(os.path.dirname(filename), exist_ok=True) |
748 | 104 | with open(filename, 'wt') as stream: | 107 | with open(filename, 'wt') as stream: |
Hey. Thanks for sending this. I made several small comments below. I like the direction this is going but I'd like to overall streamline the code. I realize we cherry picked functions from various locations but now we have a chance to simplify and unify this so that it is easier to understand.
I would also like to know exactly which features we aim to support here (in other words, this is not a launcher interpreter). It would be good if we could write this down in the docstring of the module.