As promised, here is part seven of the series about Rash, the Raku Shell.
See also: The Introduction | Part 1: The Path | Part 2: The Loop | Part 3: The Execution | Part 4: The Interrupt | Part 5: The Dynamic | Part 6: The Process.
No one noticed and reported the error I had placed in Rash (or missed myself; take your pick).
Pressing return at the Raku prompt gives a nasty surprise (that fortunately doesn't terminate the program):
rash: Enter «exit» to exit
>
Use of uninitialized value $cmd of type Any in string context.
Methods .^name, .raku, .gist, or .say can be used to stringify it to something meaningful.
in block at rash line 28
Use of uninitialized value $cmd of type Any in string context.
Methods .^name, .raku, .gist, or .say can be used to stringify it to something meaningful.
in block at rash line 28
Use of uninitialized value $cmd of type Any in string context.
Methods .^name, .raku, .gist, or .say can be used to stringify it to something meaningful.
in block at rash line 45
Use of uninitialized value $cmd of type Any in string context.
Methods .^name, .raku, .gist, or .say can be used to stringify it to something meaningful.
in block at rash line 45
Unknown command: "" (use "help" for a list of commands)
Fix this.
See The solution.
In natural language, a promise is a pledge to do something. Breaking a promise is generally frowned upon, and can get you into legal an/or moral trouble.
In Raku a promise is much the same thing; a pledge to do something, and it is possible to break the promise.
In Part 6: The Process we looked at the run
and shell
functions for executing external programs. They are quite
powerful, but are limited to executing «one program at a time». So when the
external program is running, the Raku programs is waiting.
Proc::Async
is much more flexible, as it allows parallel execution.
We'll get back to that. First the basics:
> my $proc = Proc::Async.new('emacs', 'foo.txt', 'bar.txt');
It is dormant, until we turn the key (so to speak).
We can take a look at the Proc::Async
object, before fiddling
with the key:
> say $proc;
Proc::Async.new(
path => "emacs",
args => ["foo.txt", "bar.txt"],
command => ("emacs", "foo,txt", "bar.txt"),
w => Any,
enc => "utf8",
translate-nl => Bool::True,
started => Bool::False
)
The last value tells us that we haven't started the process
yet. It is shown with the Bool::
prefix as Boolean values are set up as an
Enum(eration), and not as their own type.
We can use the enums
method to get the values for an Enum(erated) type. E.g.:
> say Bool.enums;
Map.new((False => 0, True => 1))
See
docs.raku.org/language/typesystem#enum
for more information about the enum
type.
See
docs.raku.org/routine/enums
for more information about the enums
method.
Map
is essentially a
read only version of a hash. See
docs.raku.org/type/Map for more information
about the Map
class.
> say $proc.started; # -> False
That is pretty much al lwe can do, until we start the process (turn the key). We do
that by invoking .start
on it:
> my $promise = $proc.start;
This fires off the extrenal program, and returns a Promise
object
without waiting for the program to finish.
You should now have an emacs window on your screen.
We can take a look at the Promise
object:
> say $promise;
Promise.new(
scheduler => ThreadPoolScheduler.new(
initial_threads => 0,
max_threads => 64,
uncaught_handler => -> $exception { #`(Block|94540249746184) ... }
),
status => PromiseStatus::Planned
)
The last line is the interesting one.
Now, close the emacs window, and inspect the Promise
object again:
> say $promise;
Promise.new(
scheduler => ThreadPoolScheduler.new(
initial_threads => 0,
max_threads => 64,
uncaught_handler => -> $exception { #`(Block|94540249746184) ... }
),
status => PromiseStatus::Kept
)
A Promise is not Kept until the program has finished. This is like sending a registered letter. The job hasn't finished successfully until the recipient has receieved and signed for it.
You may have guessed that PromiseStatus is an enumeration. Let us have a look at the values:
> say PromiseStatus.enums;
Map.new((Broken => 2, Kept => 1, Planned => 0))
A broken promise is something that happens if we try to execute a non-existing program:
> my $proc = Proc::Async.new('emacs-foo', 'foo.txt', 'bar.txt');
> say $proc.started; # -> False
> my $promise = $proc.start; say $promise.status; # -> Planned
> say $promise.status; # -> Broken
You get «Planned« the first time, as Raku hasn't figured out that the program doesn't exist yet.
You may want to get the exit code from the program, and we can use the
result
method on the Promise
object. The exit code is not
known until the program has finished, so a side effect of calling result
is that Raku will wait for the program to finish before doing anything else.
Doing this on a broken promise causes an exception:
> my $result = $promise.result;
Tried to get the result of a broken Promise
Original exception:
Failed to spawn process emacs-foo: no such file or directory
(error code -2)
Doing it on a running program gives a Proc
object, which is what we get
if we use run
or shell
:
> my $proc = Proc::Async.new('emacs', 'foo.txt', 'bar.txt');
> my $promise = $proc.start;
> say $promise.result; # [1]
Proc.new(in => IO::Pipe,
out => IO::Pipe,
err => IO::Pipe,
exitcode => 0,
signal => 0,
pid => Nil,
command => ("emacs", "foo.txt", "bar.txt"))
[1] This one does nothing (i.e. hangs), until we close the emacs window.
Note that the arguments to
Proc::Async.new
must be specified as scalar values, and not
as a list. This is identical to run
and shell
.
We can wrap up what we have seen so far in a program:
File: proc-run-err
sub MAIN(*@cmd) # [1]
{
my $process = Proc::Async.new(|@cmd); # [2]
my $promise = $process.start; # [3]
my $result = $promise.result; # [4]
say "Command exit code { $result.exitcode }" if $result.exitcode; # [5]
}
[1] A Slurpy argument, so that we get all the arguments in the array.
[2] We flatten the arguments, as Proc::Async
accepts single arguments
and not a list.
[3] $proc.start
returns a Promise.
[4] and we get the result.
[5] Display the status code, if non zero (as that is an error message).
We can test it, as we did in Part 3: The Execution:
$ raku proc-run-err pwd
/home/arne/raku/kurs/text/code
$ raku proc-run-err true
$ raku proc-run-err false
Command exit code:1
$ raku proc-run-err falseX
Tried to get the result of a broken Promise
Original exception:
no such file or directory
And it works, except for non-existing programs where the Exception terminates the program.
The exception happen when we access $promise.result
, as the Promise
was Broken.
We can add a CATCH
block (or handler) to intercept
(or catch) the exception:
sub MAIN(*@cmd)
{
my $process = Proc::Async.new(|@cmd);
my $promise = $process.start;
my $result = $promise.result; # [1]
CATCH # [3]
{
say 'Unknown command: "{ @cmd[0] }".'; # [4]
exit 1; # [5]
}
say "Command exit code { $result.exitcode }" if $result.exitcode; # [2]
}
[1] Accessing the result of the Promise fails if the program couldn't be executed. But it is a Soft Failure.
[2] So the actual Failure happens when we access the .exitcode
field.
[3] and CATCH
prevents the program from crashing.
[4] Display an error message.
[5] Exit with error code 1.
See
docs.raku.org/language/exceptions#index-entry-CATCH-CATCH
for more information about CATCH
.
Note that we don't need the
pid
to decide if the program was executed, as we did with
run
.
Besides that, if this is all you'd want to do, use run
instead
and let Raku handle the complexity.
Let us take a step back, and use $promise.status
so that we can avoid
the Exception:
sub MAIN(*@cmd)
{
my $proc = Proc::Async.new(|@cmd);
my $promise = $proc.start;
my $status = $promise.status;
say $status;
if $status ~~ Broken
{
say "Unknown command: \" @cmd[0] \".";
exit 1;
}
my $result = $promise.result;
say "Command exit code:{ $result.exitcode }" if $result.exitcode;
}
This fails for non-existing programs:
$ raku proc-run-status trueX
Planned
Tried to get the result of a broken Promise
Original exception:
no such file or directory
The problem is that we don't wait for the program to finish, as the
$promise.result
call did. So the program runs along, and checks
the status of the process before it even started (and the status is
Planned). When we did this in REPL mode, typing the command gave Raku time
enough to figure it out. But running a script is too fast.
We can give Raku time with the sleep
call, which does absolutely
nothing for the specified number of seconds. E.g.
> say "Now"; sleep 10; say "Ten seconds later"; sleep 0.5; say "Half a second later";
Now
Ten seconds later
Half a second later
See
docs.raku.org/routine/sleep
for more information about sleep
.
Applied to the program:
File: proc-run-status2
sub MAIN(*@cmd)
{
my $process = Proc::Async.new(|@cmd);
my $promise = $process.start;
say $promise.status;
sleep 1;
say $promise.status;
if $promise.status ~~ Broken
{
say "Unknown command: \" @cmd[0] \".";
exit 1;
}
my $result = $promise.result;
say "Command exit code:{ $result.exitcode }" if $result.exitcode;
}
This works:
$ raku proc-run-status2 false
Planned
Kept
Command exit code:1
$ raku proc-run-status2 falseX
Planned
Broken
Unknown command: " falseX ".
$ raku proc-run-status2 true
Planned
Kept
We have added a 1 second delay, that is unneccesary (and irritating) in most situations. And the delay may even be too short if the load on the computer is extremely high.
Inspecting $promise.status
can be likened to open the front door to
see if there are anybody there. It is better to install a bell, so that a visitor
can inform you of their presence.
We did this with $promise.result
above, but we can also use the
more general await
on the Promise:
sub MAIN(*@cmd)
{
my $process = Proc::Async.new(|@cmd);
my $promise = $process.start;
say $promise.status;
await $promise; # [1]
say $promise.status;
if $promise.status ~~ Broken
{
say "Unknown command: \" @cmd[0] \".";
exit 1;
}
my $result = $promise.result;
say "Command exit code:{ $result.exitcode }" if $result.exitcode;
}
[1] It is important to place it before accessing the result.
$ raku proc-run-await true
Planned
Kept
And that works out beautifully. As long as the program exists:
$ raku proc-run-await trueX
Planned
An operation first awaited:
in sub MAIN at proc-run-await line 10
Died with the exception:
Failed to spawn process trueX: no such file or directory (error code -2)
And the program must return «0» (to indicate that all went well):
$ raku proc-run-await false
Planned
The spawned command 'false' exited unsuccessfully (exit code: 1, signal: 0)
in sub MAIN at proc-run-await line 10
The problem is that await
on a Broken promise will get the
exception from the promise.
And it regards a non-zero exit code from the program as an error, so running «proc-run-await false» gives an exception as well.
We can wrap it in try
, like this:
try await $promise;
But that doesn't help, as we disregard the return value of await
,
and doing so (called sunk value in Raku) causes an Exception.
We can avoid that by getting rid of the value ourself (sinking it):
try sink await $promise;
Some examples:
> "12".say
12
> "12".sink.say
Nil
The example above is pretty useless, as we could (and should) have dropped the
say
. But we can use sink
to get rid of objects that
cause Exeptions if we ignore them.
See
docs.raku.org/routine/await
for more information about await
.
See
docs.raku.org/routine/sink
for more information about sink
.
The problem was that we disregarded the value. These lines are equivalent:
try sink await $promise;
my $dummy = try await $promise;
$ = try await $promise;
The modified program (without printing the status of the promise):
File: proc-run-await2
sub MAIN(*@cmd)
{
my $process = Proc::Async.new(|@cmd);
my $promise = $process.start;
try sink await $promise;
if $promise.status ~~ Broken
{
say "Unknown command: \" @cmd[0] \".";
exit 1;
}
my $result = $promise.result;
say "Command exit code:{ $result.exitcode }" if $result.exitcode;
}
Try it with the four use cases; «true», «false», «trueX», «emacs»
We can add support for timeout of running programs:
File: proc-run-timeout (first part)
sub MAIN(*@cmd)
{
my $timeout = 10; # [1]
my $process = Proc::Async.new(|@cmd);
my $promise = $process.start;
We skip the CATCH
, as we'll handle it another way.
my $waitfor = Promise.anyof(Promise.in($timeout), $promise); # [1] [2]
await $waitfor; # [3]
[1] We set up a promise 10 seconds into the future (with Promise.in
).
[2] and set up a third promise that waits for any one of the first and second
promises to succeed (with Promise.anyof
).
[3] Then we have to wait for this third promise, and this is done with
await
.
Note that the Exception problem in the previous section does not apply here, as the outer promise (the third one) doesn't allow the Exception from the inner promise to be thrown.
The next part is handling this third Promise:
File: proc-run-timeout (third part)
if $promise.status ~~ Kept # [4]
{
my $exit = $promise.result.exitcode;
say "Command exit code: $exit" if $exit;
exit $exit;
}
[4] We check if the first promise («I promise to run this program») was kept. If it has, we get the exit code and print it if non zero.
Then we check for failure:
File: proc-run-timeout (fourth part)
elsif $promise.status ~~ Broken
{
say "Unknown command: @cmd[0].";
exit 1;
}
Note that we don't access promise.result
as we did in «proc-run»
so we don't trigger an Exception. (And we don't check for this Exception
to tell us of the failure to run the program.)
If the promise isn't Kept or Broken, it is Planned - and that means that the program is still running:
File: proc-run-timeout (last part)
else
{
$process.kill; # [5]
say "Program @cmd[] did not finish after $timeout seconds"; # [6]
exit 2; # [7]
}
}
[6] Display an error message.
[7] Exit with a suitable error code.
See
docs.raku.org/routine/kill
for more information about the kill
method.
Testing it:
$ raku proc-run-timeout true
$ raku proc-run-timeout false
Command exit code: 1
$ raku proc-run-timeout falseX
Unknown command: "falseX".
$ raku proc-run-timeout emacs 1 2 3
Program emacs 1 2 3 did not finish after 10 seconds
It is easy to modify the program so that we can specify the timeout on the command line:
File: proc-run-timeout (top)
sub MAIN(*@cmd, :$timeout = 10)
{
# my $timeout = 10;
...
Then some testing:
$ raku proc-run-timeout emacs 1 2 3
Program emacs 1 2 3 did not finish after 10 seconds
$ raku proc-run-timeout --timeout=2 emacs 1 2 3
Program emacs 1 2 3 did not finish after 2 seconds
Note that we have to use a named argument for the timeout parameter, so that the slurpy array doesn't eat it.
The whole program:
File: proc-run-timeout
sub MAIN(*@cmd, :$timeout = 10)
{
my $process = Proc::Async.new(|@cmd);
my $promise = $process.start;
my $waitfor = Promise.anyof(Promise.in($timeout), $promise);
await $waitfor;
if $promise.status ~~ Kept
{
my $exit = $promise.result.exitcode;
say "Command exit code: $exit" if $exit;
exit $exit;
}
elsif $promise.status ~~ Broken
{
say "Unknown command: @cmd[0].";
exit 1;
}
else
{
$process.kill;
say "Program @cmd[] did not finish after $timeout seconds";
exit 2;
}
}
We have managed to do this without a single Exception handler (try
or CATCH
). But one can argue that the code is complicated.
And now we can add timeout support to Rash (the Raku Shell).
We could call our command «timeout6» (so as not to collide with the external «timeout» program, which does the same) and use it like this:
> timeout6 10 top
Where the number is the time limit in seconds.
But Rash has command completion for the first argument only, so this approach would annoy the users (as the completion wouldn't work when we come to the actual command).
So we'll add the time limit at the end of the command line instead, with a prefixed colon to show that it isn't part of the argument list to the program:
> top :10
Adding the procedure handling this is easy, given the «proc-run-timeout» program:
File: rash-timeout (partial)
sub do-run-timeout ($cmd, @args, :$timeout)
{
my $process = Proc::Async.new($cmd, |@args);
my $promise = $process.start;
my $waitfor = Promise.anyof(Promise.in($timeout), $promise);
await $waitfor;
if $promise.status ~~ Kept
{
my $exit = $promise.result.exitcode;
say "Command exit code: $exit" if $exit;
return;
}
elsif $promise.status ~~ Broken
{
say "Unknown command: $cmd.";
return;
}
else
{
$process.kill;
say "Program $cmd did not finish after $timeout seconds";
return;
}
}
This should look very familiar by now.
Note that we'll keep the old way to run programs without timeouts, so we don't have to handle a no timeout situation here.
I could have used multi subs
, but it has no benefits (and is slightly
slower due to the lookup).
The default
blocks must be updated:
default
{
my ($cmd, @args) = $line.words;
if %programs{$cmd}
{
my $timeout = False;
if @args.elems
{
if @args[*-1] ~~ /^\:\d+$/
{
my $last = @args.pop;
$last ~~ /^\:(\d+)$/;
$timeout = $0.Int if $0;
}
}
$timeout
?? do-run-timeout %programs{$cmd}, @args, :$timeout
!! do-run %programs{$cmd}, @args;
}
else
{
say "Unknown command: \"$_\" (use \"help\" for a list of commands)";
}
}
Running it:
$ rash-timeout
> top :10
...
Program /usr/bin/top did not finish after 10 seconds
The number of ways this can be used to piss off Rash users is almost limtless...
Add support for the «timeout6» command (and support «timeout» as well, if you feel like it) in the configuration file.
When the user runs a program, use the value on the command line, if given. If not, use the one set up with «timeout6» (on the command line or in the configuration file). If the user specifies «:0», that means no limit (ans thus ignore any value already set).
See The solution.
Probably. I have some ideas:
And that's it for now.