Async Commands
In a REPL environment, there are often many commands the user may be able to run. Some commands may take a long time to complete and maybe there are other commands that even give status about what the first command is doing. Here’s an example program that we will highlight below:
"""
This application has some commands that can run in the background and some in
the foreground only.
"""
import asyncio
import recline
from recline.formatters.table_formatter import TableFormat
PERCENT_COMPLETE = None
@recline.command
async def deploy(duration: int = 30) -> TableFormat:
"""Runs a deployment operation over a period of time
Args:
duration: The amount of time to run for
"""
global PERCENT_COMPLETE
try:
seconds_slept = 0
while seconds_slept < duration:
await asyncio.sleep(1)
seconds_slept += 1
PERCENT_COMPLETE = (seconds_slept / duration) * 100
return [{'duration': duration}]
except asyncio.CancelledError:
print('I only managed to get %s out of %s seconds of sleep before you interrupted me' % (seconds_slept, duration))
@recline.command(name="deploy status")
def deploy_status() -> None:
"""Get the current deployment status percentage of an ongoing operation"""
if PERCENT_COMPLETE is None:
print("No deployment has been started yet")
return
print("The current deployment is %s%% complete" % PERCENT_COMPLETE)
recline.relax()
Writing an Async Command
Writing a command that can be put in the background is easy. Simply add the async
keyword while defining the function. This tells recline that the command may be long
running and it allows the user of the application to execute the command and put it
in the background or foreground as desired.
In the body of your async command, it is recommended to use await
with coroutines
where available to help make your command more responsive. Backgrounding and foregrounding
don’t rely on this, but cancelling an async command can only be done on the next
switch of the event loop.
In our deploy
command above, we’ve implemented a simple async sleep loop to
simulate something that takes a long time. We’ve declared the function with async def
and inside the function body we’ve used await asyncio.sleep(1)
inside the loop
to make long running behavior.
In this example, there is a try/except
block around the async operation. This
is optional, but it allows our command to have special behavior in the case the user
cancels the command. We get one more opportunity to run and clean up if needed. In
this case, we elected to show the user a short message saying that we didn’t get to
complete.
Executing an Async Command
From the user’s perspective, there isn’t too much that is different about an async command. Commands are executed the same way as non-async commands:
$ python examples/async.py
> deploy -duration 5
+----------+
| Duration |
+----------+
| 5 |
+----------+
>
If the user knows that the command will take a long time, they may wish to run
it from the background to begin with. In this case, they can pass -background
when starting the command:
$ python examples/async.py
> deploy -duration 5 -background
^Z
Job 2 is running in the background
>
Send an Async Command to the Background
Warning
This section only works on Unix-like systems. It does not apply to Windows as the handling of SIGTSTP is not possible there.
If the command was started in the foreground and the users wishes to run other commands
while the first is executing, they can press ctrl+z
to send it to the background.
When doing this, the command continues to execute, but the user is free to run other
commands as well:
$ python examples/async.py
> deploy -duration 30
^Z
Job 1 is running in the background
> deploy status
The current deployment is 20.0% complete
>
Bring a Backgrounded Command Back to the Foreground
After a command was sent to the background, the shell prints a message indicating
the job number associated with the command. The user may re-attach to the command
by using the fg
builtin. This will bring the command back to the foreground
and once again block user input until the command has completed:
> deploy -duration 30
^Z
Job 2 is running in the background
> deploy status
The current deployment is 26.666666666666668% complete
> fg -job 2
+----------+
| Duration |
+----------+
| 30 |
+----------+
>
If the command completed executing while in the background, it remains available
for the user to foreground it in order to retrieve its result. In this case, the
user can expect to execute the fg
command and the result would be printed immediately.
Once a command execution has finished and the result has been returned, whether it finished in the foreground or it was foregrounded after completion, the entry is cleaned up and it is no longer available to be foregrounded:
> deploy -duration 30
^Z
Job 3 is running in the background
> fg -job 3
+----------+
| Duration |
+----------+
| 30 |
+----------+
> fg -job 3
Could not find a running job for 3
>