Summary#
Host.Process.Run runs a program to its end and answers what it left behind. It is the local-app way to use a
tool the user already has — git, a compiler, a converter — from a function.
app Toolbox {
model "**/*.osy";
use Osysharp.Ui;
use Osysharp.Host {
program "git";
folder "~/Projects";
}
}
using Osysharp.Host;
string GitVersion() {
ProcessResult run = Host.Process.Run("git", ["--version"]);
return run.IsSuccess ? run.Output.Trim() : "git answered " + run.ExitCode + ": " + run.Error.Trim();
}A program the app did not declare does not start; see Host — the computer a local app runs on for the declaration and the rules every Host
call follows. The types on this page — ProcessResult, HostProcess, ProcessLine — are named in a file that says
using Osysharp.Host;.
Signature#
ProcessResult Host.Process.Run(
string fileName, // a bare name ("git") or a full path
List<string> arguments, // one string per argument; [] or omitted for none
TimeSpan timeout = 30 seconds, // may be named: timeout: TimeSpan.FromMinutes(2)
string workingDirectory = home, // a folder the app declared
string input = none) // text for the program's standard input
class ProcessResult {
int ExitCode; // 0 is success by convention
string Output; // everything written to standard output
string Error; // everything written to standard error
bool IsSuccess; // ExitCode == 0
}
// a program that keeps running
HostProcess Host.Process.Start(
string fileName,
List<string> arguments,
string workingDirectory = home,
string input = none, // written to its standard input, which then stays open
string logFile = none, // every line it prints, kept in a file the app declared a folder for
bool outlives = false) // true: it is NOT ended when the platform stops — needs `lasting "…";`
stream<ProcessLine> Host.Process.Watch(process, timeout:) // its lines so far, then each new one, until it exits
HostProcess? Host.Process.Find(Guid id) // the program as it is now, or null when there is none
void Host.Process.Kill(process) // end it and everything it started
void Host.Process.Write(process, string text) // to its standard input
void Host.Process.CloseInput(process)
// `process` is the HostProcess that Start answered, or its Id.
class HostProcess {
Guid Id; // how it is found again
int ProcessId; // the operating system's process id
string FileName;
DateTime StartedAt;
bool HasExited; // when this was read
int? ExitCode; // null while it runs
bool ChildrenRunning; // a program it started still runs, after it exited too
}
class ProcessLine {
long Sequence; // 1, 2, 3 … in the order the lines arrived
string Text;
bool IsError; // written to standard error
DateTime At;
}Description#
The arguments are a list, and that is the safety of it. Each string in the list reaches the program as one argument, exactly as written. Nothing splits it, and nothing reads it as a command — so a value a person typed can be an argument without being able to run anything:
int Commit(string folder, string message) =>
Host.Process.Run("git", ["commit", "-m", message], workingDirectory: folder).ExitCode;
string Sorted(string lines) =>
Host.Process.Run("git", ["stripspace"], input: lines).Output;There is no form that takes a whole command line, and passing one string where the list goes is a compile error that
says so. To use a shell feature — a pipe, a redirect — run the two programs and pass one's Output as the other's
input.
A non-zero exit code is an answer, not an exception. Most programs say "no" that way, so branch on ExitCode or
IsSuccess. Text on Error does not mean failure either: many programs write progress there on a run that worked.
What does throw:
| what happened | what the call does |
|---|---|
| the program is not declared | refused, naming the program "…"; line to add |
| it is declared but this computer has none | throws, saying so — ask Host.Which(name) first when that is likely |
| it is still running at the timeout | the program and everything it started are ended, and a TimeoutException is thrown |
| it printed more than 8 MB | it is ended and the call throws — this call holds everything a program prints |
Naming the program. A bare name is looked up on the user's search path, as a terminal would. A full path is
accepted when it is the very file a declaration means — so Host.Process.Run(Host.Which("git"), …) works under
program "git";, and a file somewhere else that is merely called git does not.
Where it runs. In the user's home folder unless workingDirectory names another, which must be inside a folder
the app declared. The program gets the user's own environment — the variables a terminal would give it, with the
user's current search path — without the platform's own configuration.
On Windows, a .cmd or .bat file is the one exception to "nothing reads the arguments". Windows runs a batch
file through its command interpreter, which would act on characters such as &, |, % and " inside an argument.
So an argument holding one of those is refused for a batch file, with a message that says to send the text on
input: instead. Programs installed through npm are batch files on Windows.
A program that is left running when the platform stops#
Everything an app starts is ended when the local platform stops — when it is stopped, and when it goes quiet and exits by itself. That is the promise that an app leaves nothing running on the computer. One kind of program must break it: a server the person is meant to keep using after the app that started it is closed. For that, say so twice — in the manifest, where a reader can see it, and at the call:
use Osysharp.Host {
program "osy";
lasting "osy"; // this app may leave `osy` running after it stops
}
HostProcess Open(string folder) =>
Host.Process.Start("osy", ["launch"], workingDirectory: folder, outlives: true);While the platform runs, such a program is a started program like any other: Find finds it, Watch reads its
lines, Kill ends it. When the platform stops it is let go of instead of ended, with everything it started. It is
also not a reason for the platform to stay up.
| what is written | what happens |
|---|---|
outlives: true and a lasting line for the program | it starts, and is left running when the platform stops |
outlives: true and no lasting line | refused, naming the line to add — nothing starts |
a lasting line for a program with no program line | a compile error: a program that may be left running is first one that may be started |
no outlives: | it is ended with the platform, whatever the manifest says |
⚠ After the platform is gone nothing reads what the program prints, and logFile: stops being written. A program
meant to be left should keep its own log.
osy explain lists the programs an app may leave running, under their own heading.
A program that keeps running, and its output on a page#
Host.Process.Start answers at once. The program is the platform's, not the request's: it keeps running when the
page that started it is closed. A yield — a function that produces results over time function hands its lines to a page as they are printed:
using Osysharp.Host;
HostProcess StartLog(string folder) =>
Host.Process.Start("git", ["log", "--oneline"], workingDirectory: folder);
stream<ProcessLine> Lines(Guid id) {
if (Host.Process.Find(id) == null) { return; } // not started yet, or long gone
foreach (var line in Host.Process.Watch(id)) {
yield return line;
}
}
void Stop(Guid id) { Host.Process.Kill(id); }
component LogView(string folder) {
Guid running = Guid.Empty;
live var lines = Lines(running); // follows `running`: a new program, a new list
action Go() { running = StartLog(folder).Id; }
action Halt() { Stop(running); }
render {
Stack(gap: 2) {
Row(gap: 2) {
Button("Start", onPress: Go);
Button("Stop", onPress: Halt, disabled: running == Guid.Empty);
}
foreach (var line in lines) { Text(line.Text); }
}
}
}The lines are kept by the platform. Watch gives every line the program has printed so far and then each new one,
and ends when the program has exited. So a page that opens late, is closed and opened again, or is open in two tabs
sees the same lines from the first. The newest 20,000 are held; logFile: keeps all of them in a file — standard
output in the file named, standard error in the same name with .err added.
The file can be read with Host.File while the program is still writing it — on every system, Windows included,
where a file open for writing is otherwise refused to a reader ("being used by another process"). A read answers what
has been written so far.
Reading with a deadline. In a function — not a page following a stream — a loop over Watch runs until the
program exits, and a program that has gone silent never gives the loop another turn to look at the clock. Give the
read its own deadline:
var launch = Host.Process.Start("osy", ["launch"], workingDirectory: folder);
var printed = "";
foreach (var line in Host.Process.Watch(launch.Id, timeout: TimeSpan.FromMinutes(5))) {
printed = printed + line.Text + "\n";
}
var now = Host.Process.Find(launch.Id);
if (now != null && !now.HasExited) {
Host.Process.Kill(launch.Id); // still running: reading stopped, the program did not
return "It took more than five minutes to start. It last said: " + printed;
}timeout: is how long the reading goes on, by the clock, counted from the moment it begins. When it passes, the
stream ends — whether or not the program has. The lines read until then are kept, the program is left running,
and Host.Process.Find says which happened: HasExited is false for a program the deadline cut short. Stopping it
is Host.Process.Kill; a later Watch reads every line again from the first. (Host.Process.Run's timeout: is
different on purpose: a run owns its program, so it ends it and throws.)
Stop ends the whole tree. Kill ends the program and everything it started, including a helper whose own parent
has already exited, and answers once the program has ended. It works after the program has exited by itself too: what it
left running is still its tree, and ChildrenRunning says whether anything of it is still going. Stopping the local platform does the same for every
started program, so nothing an app started is left running behind it. And while a started program runs, the local
platform does not shut itself down for being idle.
HostProcess is the program as it was when read. HasExited and ExitCode on a value already held do not
change; Host.Process.Find(id) reads the present. Keep the Id — in page state or in a row — to reach the program
from a later request. A program is known from its start until an hour after it exits, to the app that started it,
and not across a restart of the local platform; Find answers null otherwise.
Talking to it. input: and Host.Process.Write write to its standard input; Host.Process.CloseInput closes
it, which is how most programs learn there is no more to read.
Standing in for a program in a test#
A test should not need the program to be installed, signed in or on a network. Both ways of starting one can be stubbed for the rest of a test:
Host.Process.Run.Stub((fileName, arguments) =>
new ProcessResult { ExitCode = 0, Output = "git version 9.9.9", Error = "" });
Host.Process.Start.Stub((fileName, arguments) => ["first line", "second line"]);Run's stub answers the ProcessResult (IsSuccess follows from the exit code). Start's stub answers the lines
the program prints; what the stubbed Start hands back is a HostProcess like any other, already exited with code
0, so the Watch, Find and Kill the app wrote run against it unchanged — a recorded transcript of a long tool,
replayed.
When the lines alone do not say it, Start's stub answers a ProcessReplay instead: a program that fails, one
that complains on standard error, or one that prints and then keeps running until it is stopped — which is how a
test sees what the app does with a program that has gone quiet, and that its Stop ends one.
Host.Process.Start.Stub((fileName, arguments) =>
new ProcessReplay { Lines = ["working…"], ErrorLines = ["out of disk"], ExitCode = 3 });
Host.Process.Start.Stub((fileName, arguments) =>
new ProcessReplay { Lines = ["started"], KeepsRunning = true }); // Watch does not end until KillProcessReplay | |
|---|---|
Lines | what it prints to standard output, in order |
ErrorLines | what it prints to standard error, after its output (ProcessLine.IsError) |
ExitCode | the code it exits with — 0 unless said |
KeepsRunning | it prints its lines and stays running: HasExited is false and Watch does not end until Host.Process.Kill, after which its exit code is 137, as a stopped program's is |
LeavesChildRunning | a program it started is still running, after it exits too: ChildrenRunning is true until Host.Process.Kill ends the tree |
A replayed program also keeps what the app writes to it — the input: it was started with, then each
Host.Process.Write while it "runs" — in the log file's name with .in added. That is how a test says what an app
told a program it is driving: Assert.Contains("stop after two", Host.File.ReadAllText(log + ".in")).
A replayed program keeps its logFile: too — its output in the file named and its errors in the same name with
.err added — so code that reads a program's log back runs in a test as it does for real.
A stub stands in for the program, not for the app's declaration: the program must still be one the app declared,
and a workingDirectory: or logFile: outside the declared folders is refused as it is for a real start. A test
does not pass on a call the app itself would be refused.
Examples#
[Test]
void an_undeclared_program_is_refused_and_named() {
var refused = Assert.Throws<Exception>(() => Host.Process.Run("curl", ["--version"]));
Assert.Contains("this app has not declared it", refused.Message);
Assert.Contains("`program \"git\";`", refused.Message);
Assert.Contains("add `program \"curl\";`", refused.Message);
}See also#
- Host — the computer a local app runs on — the capability, what an app declares, and where it runs
- Host.File and Host.Directory — files and folders on the computer — the files a program reads and writes, and watching a folder for the ones it makes
- yield — a function that produces results over time — a function whose answer arrives over time, and the
live varthat shows it - Http.* — the same shape for a call over the network: the answer is data, a failure to reach is the exception
- Stop a task (CancellationToken) and complete one by hand (TaskCompletionSource) — stopping work a function has started