Skip to content

Improve user experience of interactive mode #136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: fkloss/numpy2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Moved documentation from GitHub Pages to Read the Docs. This allows to more easily
manage docs for different versions.
- Interactive command `show_job` now expects job ID as argument.

### Added
- Support for Numpy 2.
- Tab-completion and history for interactive mode.
- `help` command in interactive mode.


## [3.0.0] - 2024-08-19
Expand Down
21 changes: 13 additions & 8 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,16 @@ get some information about finished and running jobs, as well as to stop running

To enter the command prompt, press ESC. You should now see the following prompt:

::
.. code-block:: text

Enter command, e.g. list_jobs, list_running_jobs, list_successful_jobs,
list_idle_jobs, show_job, stop_remaining_jobs
>>>
============= COMMAND MODE =============
Type 'help' or '?' to list commands.
Press enter with empty line to exit command mode.

You now may enter one of the listed commands or simply press Enter to leave the prompt
without executing a command.
>>>

You now may enter one of the commands listed below (you can use tab-completion). To
leave the command mode, simply press Enter without a command.

.. important::

Expand All @@ -72,6 +74,11 @@ Commands
name collision with an actual config value, this should be fine and much easier than
adding a dedicated directive.

.. confval:: help

Show help. Without argument, a list of all commands is shown. Specify a command as
argument to see the help for that command.

.. confval:: list_jobs

List IDs of all jobs that have been submitted so far (including finished ones).
Expand Down Expand Up @@ -99,5 +106,3 @@ Commands

This will not stop submission of new jobs. If you want to stop cluster_utils
completely, press Ctrl + C instead.


121 changes: 86 additions & 35 deletions src/cluster_utils/server/user_interaction.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
from __future__ import annotations

import cmd
import logging
import select
import sys
import termios
import textwrap
import tty

from .utils import log_and_print, make_red


class InteractiveMode:
class InteractiveMode(cmd.Cmd):
intro = textwrap.dedent(
"""
============= COMMAND MODE =============
Type 'help' or '?' to list commands.
Press enter with empty line to exit command mode.
"""
)
prompt = ">>> "

def __init__(self, cluster_interface, comm_server):
super().__init__()

self.cluster_interface = cluster_interface
self.comm_server = comm_server
self.input_to_fn_dict = {
"list_jobs": self.list_jobs,
"list_running_jobs": self.list_running_jobs,
"list_successful_jobs": self.list_successful_jobs,
"list_idle_jobs": self.list_idle_jobs,
"show_job": self.show_job,
"stop_remaining_jobs": self.stop_remaining_jobs,
}

def __enter__(self):
self.old_settings = termios.tcgetattr(sys.stdin)
Expand All @@ -29,32 +36,66 @@ def __enter__(self):
def __exit__(self, _type, _value, _traceback):
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings)

def list_jobs(self):
self.print("List of all jobs:")
self.print([job.id for job in self.cluster_interface.jobs])
def do_list_jobs(self, _):
"""List IDs of all jobs that have been submitted so far.

This includes jobs that have finished already.
"""
self.columnize([str(job.id) for job in self.cluster_interface.jobs])
return True

def list_running_jobs(self):
self.print("List of running jobs:")
self.print([job.id for job in self.cluster_interface.running_jobs])
def do_list_running_jobs(self, _):
"""List IDs of all jobs that are currently running."""
self.columnize([str(job.id) for job in self.cluster_interface.running_jobs])
return True

def list_successful_jobs(self):
self.print("List of successful jobs:")
self.print([job.id for job in self.cluster_interface.successful_jobs])
def do_list_successful_jobs(self, _):
"""List IDs of all jobs that finished successfully."""
self.columnize([str(job.id) for job in self.cluster_interface.successful_jobs])
return True

def list_idle_jobs(self):
self.print("List of idle jobs:")
self.print([job.id for job in self.cluster_interface.idle_jobs])
def do_list_idle_jobs(self, _):
"""List IDs of all jobs that have been submitted but not yet started."""
self.columnize([str(job.id) for job in self.cluster_interface.idle_jobs])
return True

def do_show_job(self, arg: str):
"""Show information about a specific job.

Usage: show_job <job_id>
"""
if not arg:
self.print("No job ID provided.")
self.print("Usage: show_job <job_id>")
return False

def show_job(self):
try:
self.print("Enter ID")
job_id = int(input())
job_id = int(arg)
job = self.cluster_interface.get_job(job_id)
[self.print(attr, ": ", job.__dict__[attr]) for attr in job.__dict__]
except Exception:
self.print("Error encountered, maybe invalid ID?")
return False

return True

def complete_show_job(self, text, line, begidx, endidx):
"""Tab completion for show_job command."""
return [
str(job.id)
for job in self.cluster_interface.jobs
if str(job.id).startswith(text)
]

def stop_remaining_jobs(self):
def do_stop_remaining_jobs(self, _):
"""Abort all submitted jobs.

Abort all currently running jobs as well as jobs that already have been
submitted but didn't start yet.

Note: This will currently not stop submission of new jobs. If you want to stop
cluster_utils completely, press Ctrl + C instead.
"""
try:
self.print(
make_red("Are you sure you want to stop all remaining jobs? [y/N]")
Expand All @@ -64,7 +105,7 @@ def stop_remaining_jobs(self):
for job in self.cluster_interface.jobs
if job not in self.cluster_interface.successful_jobs
]
self.print(jobs_to_cancel)
self.columnize(list(map(str, jobs_to_cancel)))
answer = input()
if answer.lower() in ["y", "yes"]:
logger = logging.getLogger("cluster_utils")
Expand All @@ -78,8 +119,23 @@ def stop_remaining_jobs(self):
except Exception:
self.print("Error encountered")

return True

def emptyline(self):
# Do not execute a command when pressing enter with empty line.
# Return True, to exit the command loop.
return True

def postcmd(self, stop, line):
# if command returned True (usually the case if there is no error), exit the
# command loop
if stop:
# print a line to separate command output from progress output
print("========================================")
return True

def keyboard_input_available(self):
# checks if theres sth to read from stdin
# checks if there is something to read from stdin
return select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], [])

def check_for_input(self):
Expand All @@ -90,17 +146,12 @@ def check_for_input(self):
c = sys.stdin.read(1)
if c == "\x1b": # x1b is ESC
esc_key_pushed = True

if esc_key_pushed:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings)
self.print("\n\n") # hack to get into right line after tqdms
self.print(
"Enter command, e.g. ", ", ".join(self.input_to_fn_dict.keys())
)
self.print(">>>")

fn_string = input()
if fn_string in self.input_to_fn_dict:
self.input_to_fn_dict[fn_string]()

self.cmdloop()

tty.setcbreak(sys.stdin.fileno())

Expand Down
Loading