diff --git a/CHANGES.md b/CHANGES.md index 4f7ce126..fdd96498 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,23 @@ development source code and as such may not be routinely kept up to date. # __NEXT__ +## Improvements + +* `nextstrain build --aws-batch --attach …` no longer offers to cancel (via + Control-C) or detach (via Control-Z) from the job if it's already complete. + Instead, Control-C will exit the program without delay and without trying to + cancel the job. + ([#253][]) + +* `nextstrain build` now supports a `--no-logs` option to suppress the fetching + and printing of job logs when attaching to a completed AWS Batch build. As + log fetching can often take longer than a selective download of the results + (i.e. via `--download`), this is a time (and terminal scrollback) saver when + all you want are a few of the results files. + ([#253][]) + +[#253]: https://github.com/nextstrain/cli/pull/253 + ## Bug fixes * An error message that's printed by `nextstrain remote upload` when unknown diff --git a/nextstrain/cli/command/build.py b/nextstrain/cli/command/build.py index 26050bdc..00680206 100644 --- a/nextstrain/cli/command/build.py +++ b/nextstrain/cli/command/build.py @@ -98,6 +98,22 @@ def register_parser(subparser): dest = "download", action = "store_false") + # A --logs option doesn't make much sense right now for most of our + # runtimes, but I can see how it might in the future. So we're ready if + # that future comes to pass, set up --no-logs as if there's a --logs option + # enabled by default. This also avoids a double negative in conditions, + # e.g. avoids writing "if not opts.no_logs". + # -trs, 9 Feb 2023 + parser.set_defaults(logs = True) + parser.add_argument( + "--no-logs", + help = "Do not show the log messages of the remote build. " + "Currently only supported when also using --aws-batch. " + "Default is to show all log messages, even when attaching to a completed build." + f"{SKIP_AUTO_DEFAULT_IN_HELP}", + dest = "logs", + action = "store_false") + # Positional parameters parser.add_argument( "directory", diff --git a/nextstrain/cli/runner/aws_batch/__init__.py b/nextstrain/cli/runner/aws_batch/__init__.py index bfc74de8..94aa8c68 100644 --- a/nextstrain/cli/runner/aws_batch/__init__.py +++ b/nextstrain/cli/runner/aws_batch/__init__.py @@ -169,31 +169,34 @@ def run(opts, argv, working_volume = None, extra_env = {}, cpus: int = None, mem return detach(job, local_workdir) - # Setup signal handler for Ctrl-Z. Only Unix systems support SIGTSTP, so - # we guard this non-essential feature. - SIGTSTP = getattr(signal, "SIGTSTP", None) + # Don't setup signal handlers for jobs that are already complete. + if not job.is_complete: + # Setup signal handler for Ctrl-Z. Only Unix systems support SIGTSTP, so + # we guard this non-essential feature. + SIGTSTP = getattr(signal, "SIGTSTP", None) - if SIGTSTP: - def handler(sig, frame): - exit(detach(job, local_workdir)) - signal.signal(SIGTSTP, handler) + if SIGTSTP: + def handler(sig, frame): + exit(detach(job, local_workdir)) + signal.signal(SIGTSTP, handler) - # Watch job status and tail logs. - print_stage("Watching job status") + print_stage("Watching job status") - if SIGTSTP: - control_hints = """ - Press Control-C twice within %d seconds to cancel this job, - Control-Z to detach from it. - """ % (CTRL_C_CONFIRMATION_TIMEOUT,) - else: - control_hints = """ - Press Control-C twice within %d seconds to cancel this job. - """ % (CTRL_C_CONFIRMATION_TIMEOUT,) + if SIGTSTP: + control_hints = """ + Press Control-C twice within %d seconds to cancel this job, + Control-Z to detach from it. + """ % (CTRL_C_CONFIRMATION_TIMEOUT,) + else: + control_hints = """ + Press Control-C twice within %d seconds to cancel this job. + """ % (CTRL_C_CONFIRMATION_TIMEOUT,) - print(dedent(control_hints)) + print(dedent(control_hints)) + + # Watch job status and tail logs. log_watcher = None stop_sent = False ctrl_c_time = 0 @@ -215,8 +218,9 @@ def handler(sig, frame): if job.is_running and not log_watcher: # Transitioned from waiting → running, so kick off the log watcher. - log_watcher = job.log_watcher(consumer = print_job_log) - log_watcher.start() + if opts.logs: + log_watcher = job.log_watcher(consumer = print_job_log) + log_watcher.start() elif job.is_complete: if log_watcher: @@ -226,8 +230,9 @@ def handler(sig, frame): else: # The watcher never started, so we probably missed the # transition to running. Display the whole log now! - for entry in job.log_entries(): - print_job_log(entry) + if opts.logs: + for entry in job.log_entries(): + print_job_log(entry) print_stage( "Job %s after %0.1f minutes" % (job.status, job.elapsed_time / 60), @@ -240,7 +245,8 @@ def handler(sig, frame): except KeyboardInterrupt as interrupt: print() - if not stop_sent: + # Don't try to cancel a job that's already complete. + if not job.is_complete and not stop_sent: now = int(time()) if now - ctrl_c_time > CTRL_C_CONFIRMATION_TIMEOUT: