Osy#betaRun it on Osyrin: osyrin.com

Reference / Stdlib

Host.Process — run a program on the computer

ProcessResult Host.Process.Run(fileName, arguments, timeout:, workingDirectory:, input:) · HostProcess Host.Process.Start(fileName, arguments, workingDirectory:, input:, logFile:) · stream<ProcessLine> Host.Process.Watch(process) · Host.Process.Kill(process) // under `use Osysharp.Host { program "…"; }`

Run a program on the computer a local app runs on. `Host.Process.Run("git", ["--version"])` waits for it and answers a `ProcessResult` (`ExitCode`, `Output`, `Error`, `IsSuccess`). `Host.Process.Start(…)` answers at once with a `HostProcess` that keeps running; `Host.Process.Watch(process)` is its lines as they arrive, for a page to show, and `Host.Process.Kill(process)` ends it with everything it started. The arguments are a list — never a command line — so no shell is involved, and the program must be declared (`program "git";`).

stable4 examples compiled by CIhostlocalprocess

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 happenedwhat the call does
the program is not declaredrefused, naming the program "…"; line to add
it is declared but this computer has nonethrows, saying so — ask Host.Which(name) first when that is likely
it is still running at the timeoutthe program and everything it started are ended, and a TimeoutException is thrown
it printed more than 8 MBit 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 writtenwhat happens
outlives: true and a lasting line for the programit starts, and is left running when the platform stops
outlives: true and no lasting linerefused, naming the line to add — nothing starts
a lasting line for a program with no program linea 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 Kill
ProcessReplay
Lineswhat it prints to standard output, in order
ErrorLineswhat it prints to standard error, after its output (ProcessLine.IsError)
ExitCodethe code it exits with — 0 unless said
KeepsRunningit 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
LeavesChildRunninga 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#

Related

Host — the computer a local app runs on

`use Osysharp.Host` gives an app the computer it runs on: run a program (`Host.Process`), read and write files in…

Host.File and Host.Directory — files and folders on the computer

Read, write and list real files on the computer a local app runs on, with C#'s `System.IO` names under `Host`:…

yield — a function that produces results over time

A `stream<T>` function produces its results one at a time instead of all at once, and a `live var` bound to one renders…

Http.*

Make an outbound HTTP call to a URL you build at runtime — a webhook, a third-party API, a discovered endpoint…

Stop a task (CancellationToken) and complete one by hand (TaskCompletionSource)

C#'s cancellation, with C#'s meaning. A CancellationTokenSource hands out a Token; cancel the source and every task…