Skip to content
Kavachकवच

Running Programs

● Intermediate5 minBefore this: Capabilities, catchSpec 14.23

std.process runs other programs: a compiler, a simulator, git, any tool with no library. It needs the capability Process, never goes through a shell, and ends everything a program started when it stops.

use std.process

fn main() ! ProcessError uses Process, Console {
    const done = try process.run("git", ["--version"])
    print(done.code)
    print(done.out.starts_with("git version"))
    print(done.cpu_ms >= 0 and done.peak_bytes > 0)

    // One odd argument, never a second command.
    const odd = try process.run("git", ["--version; echo injected"])
    print(odd.code)
    print(odd.err.lines()[0])
    print(odd.out.is_empty())
}

The program ran and printed:

kavach run run.kav

0truetrue129unknown option: --version; echo injectedtrue
run.kav: Kavach code, then what running it prints
  • process.run runs a program to its end and gives a Finished: its exit code, what it printed (out, err) and what it cost (cpu_ms, peak_bytes).
  • args is a list, passed as it is, never through a shell. "--version; echo injected" is one odd argument, so git refuses it: nothing else runs.
  • A program is a name looked up on the system's PATH, or a path such as ./tools/gen.
  • folder: sets the folder it runs in, and env: adds or changes its environment variables. It reads nothing on standard input.

It needs Process

Running a program reaches outside your own, so the function says so in its uses, or it doesn't compile:

use std.process

fn status() -> Int ! ProcessError uses Console {
    const done = try process.run("git", ["status"])
    done.code
}

fn main() uses Console {
    print(status() catch _ => -1)
}

The compiler refused the program:

kavach run needs.kav

error: `status` uses Process (through `run`), but its `uses` clause doesn't list it: add `Process` to `uses` (spec 8.1)
needs.kav: Kavach code, then what running it prints

Which programs

Process can be granted for one program, as Fs is for one folder. Inside a function with uses Process("git"), every program it runs, and everything it calls runs, must be git. Any other fails with Denied; a program that isn't there fails with NotFound:

use std.process

fn version(program: String) -> String ! ProcessError uses Process("git") {
    const done = try process.run(program, ["--version"])
    "exit code {done.code}"
}

fn anything(program: String) -> String ! ProcessError uses Process {
    const done = try process.run(program, [])
    "exit code {done.code}"
}

fn main() uses Process, Console {
    print(version("git") catch e => "{e}")
    print(version("cargo") catch e => "{e}")
    print(anything("no-such-program-here") catch e => "{e}")
}

The program ran and printed:

kavach run names.kav

exit code 0Denied(program: "cargo")NotFound(program: "no-such-program-here")
names.kav: Kavach code, then what running it prints
WrittenAllows
uses ProcessRunning any program
uses Process("verilator")Only verilator
  • The name is checked when the program runs, since it is a value.
  • A name without a / matches only that name on PATH; a name with one matches only that path.
  • Names nest, as folders do: inside two, a program must match both. So a function that runs two tools has uses Process, or calls one function per tool.
  • A task keeps the names of the function that spawned it.

Output as it comes

process.start gives the program back running, as a Child. Its lines come one by one, each an Output.Out(line) or an Output.Err(line), without its line end. each calls a function with each one:

use std.process

fn main() ! ProcessError uses Process, Console {
    const git = try process.start("git", ["--version"])
    git.each(out => match out {
        Output.Out(line) => {
            print("out: starts with git version? {line.starts_with("git version")}")
            true
        },
        Output.Err(line) => {
            print("err: {line}")
            true
        },
    })
    print(git.wait().code)
}

The program ran and printed:

kavach run lines.kav

out: starts with git version? true0
lines.kav: Kavach code, then what running it prints

next() reads one line, and wait() waits for the end, skipping what is still printed:

use std.process

fn main() ! ProcessError uses Process, Console {
    const git = try process.start("git", ["--no-such-flag"])
    if let first = git.next() {
        print(match first {
            Output.Out(line) => "out: {line}",
            Output.Err(line) => "err: {line}",
        })
    }
    const end = git.wait()
    print(end.code)
}

The program ran and printed:

kavach run next.kav

err: unknown option: --no-such-flag129
next.kav: Kavach code, then what running it prints
  • An exit code of 0 most often means it went well. Git gives 129 for an option it doesn't know.
  • On Linux and macOS, a program ended by a signal gives 128 plus the signal's number, as shells say it.
  • Waiting for a line or for the end lets other tasks take their turns, so a program can run several tools at once, each in its own task.

A longer one, from the spec: a simulator whose errors are picked out of its output.

use std.process

fn simulate(top: String) -> Exit ! ProcessError uses Process("vsim"), Console {
    const sim = try process.start("vsim", ["-c", top], folder: "build")
    sim.each(out => match out {
        Output.Out(line) => {
            if line.contains("Error") {
                print(line)
            }
            true
        },
        Output.Err(line) => {
            print("! {line}")
            true
        },
    })
    sim.wait()
}
from spec 14.23: Kavach code

A program is a tree

What a program starts is part of it. kill(), a Child nothing holds any more, a task cancelled while it waits, and a time limit (.timeout) all end the whole tree: through a job object on Windows, and a process group on Linux and macOS. So a timeout never leaves a simulator running behind it.

What it cost

cpu_ms and peak_bytes say how long a program used the processor and the most memory it held, so a tool that runs too long or grows too big can be seen. process.peak_bytes() says the same of your own program, and needs no capability:

use std.process

fn main() uses Console {
    let numbers: [Int] = []
    for i in 0..1_000_000 {
        numbers.push(i)
    }
    print(numbers.len())
    print(process.peak_bytes() > 8_000_000)
}

The program ran and printed:

kavach run peak.kav

1000000true
peak.kav: Kavach code, then what running it prints

Every function, method and type

Function or methodGives
process.run(program, args, folder: "", env: [:]) -> Finished ! ProcessErrorRuns program to its end and gives its exit code, what it printed and what it cost
process.start(program, args, folder: "", env: [:]) -> Child ! ProcessErrorStarts program and gives it back running, to read its output as it comes
child.next() -> Output?The next line the program printed, waiting for it; none once it has ended and every line was read
child.each(f)Calls f(output) with each line as it comes, until f gives false or the program has ended and every line was read
child.wait() -> ExitWaits for the program's end, skipping what it still prints
child.kill()Ends the program, and every program it started, at once
process.peak_bytes() -> IntThe most memory this program has held so far, in bytes. Needs no capability
TypeHolds
Finishedcode: Int, out: String, err: String, cpu_ms: Int, peak_bytes: Int; out and err are the lines printed, each ending in a line break
OutputOut(line: String) or Err(line: String): a line of standard output or of standard error, without its line end
Exitcode: Int, cpu_ms: Int, peak_bytes: Int
ProcessErrorNotFound(program: String), Denied(program: String) or Failed(reason: String)

Not yet: text sent to a program's standard input, output read as Bytes, and a fake Process for kavach test. Until there is one, generated tests skip functions that use Process.