...

Package run

import "github.com/amonks/run/pkg/run"
Overview
Index
Examples

Overview ▾

Package run runs a collection of programs specified in tasks.toml files, and provides a UI for inspecting their execution. Run displays long-lived processes in an interactive TUI. Run also works well for short-lived processes, and its interleaved output can be sent to a file.

Run can be used and extended programatically through its Go API, which is documented here. Run's primary documentation is on Github.

Conceptual Overview

1. You call Load to parse task files and get a Task set.

  • You can also generate your own task set, or append your own tasks onto it.
  • Tasks just implement an interface.

2. You combine a task list with an ID to get a Run.

  • You can also generate your own Run.
  • Runs just implement an interface.

3. You pass a UI into a Run and Start it.

  • You can also make your own UI.
  • You can also use a Run UI with any other collection of processes that expect io.Writers.

Example

In this example, we use components from Run to build our own version of the run CLI tool. See cmd/run for the source of the -real- run CLI, which isn't too much more complex.

Code:

tasks, _ := run.Load(".")
r, _ := run.RunTask(".", tasks, "dev")
ui := run.NewTUI(r)

ctx := context.Background()
uiReady := make(chan struct{})

go ui.Start(ctx, uiReady, os.Stdin, os.Stdout)
<-uiReady

r.Start(ctx, ui) // blocks until done

Example (BringYourOwnTasks)

In this example, we generate our own Task and run it.

Code:

tasks := run.NewTasks([]run.Task{
    run.FuncTask(func(ctx context.Context, w io.Writer) error {
        w.Write([]byte("sleep"))
        time.Sleep(1 * time.Second)
        w.Write([]byte("done"))
        return nil
    }, run.TaskMetadata{ID: "custom", Type: "short"}),
})

run, err := run.RunTask(".", tasks, "custom")
if err != nil {
    log.Fatal(err)
}

fmt.Println(strings.Join(run.IDs(), ", "))

Output:

custom

Example (BringYourOwnUI)

In this example, we build a version of the run CLI tool that uses a UI we provide ourselves.

Code:

package run_test

import (
    "context"
    "io"
    "log"
    "os"

    "github.com/amonks/run/pkg/run"
)

// ui implements MultiWriter
var _ run.MultiWriter = ui{}

type ui struct{}

func (w ui) Writer(string) io.Writer {
    return os.Stdout
}

// In this example, we build a version of the run CLI tool that uses a UI we
// provide ourselves.
func Example_bringYourOwnUI() {
    tasks, err := run.Load(".")
    if err != nil {
        log.Fatal(err)
    }

    run, err := run.RunTask(".", tasks, "dev")
    if err != nil {
        log.Fatal(err)
    }

    ui := ui{}

    if err := run.Start(context.Background(), ui); err != nil {
        log.Fatal(err)
    }
}

type MultiWriter

MultiWriter is the interface Runs use to display UI. To start a Run, you must pass a MultiWriter into Run.Start.

MultiWriter is a subset of UI, so the UIs produced by NewTUI and NewPrinter implement MultiWriter.

type MultiWriter interface {
    Writer(id string) io.Writer
}

type Run

A Run represents an execution of a task, including,

A Run is safe to access concurrently from multiple goroutines.

type Run struct {
    // contains filtered or unexported fields
}

func RunTask

func RunTask(dir string, allTasks Tasks, taskID string) (*Run, error)

RunTask creates an executable Run from a taskList and a taskID.

The run will handle task dependencies, watches, and triggers as documented in the README.

func (*Run) IDs

func (r *Run) IDs() []string

IDs returns the list of output stream names that a Run would write to. This includes the IDs of each Task that will be used in the run, plus (if applicable) the id "@watch", which the Run uses for messaging about file watchers.

func (*Run) Invalidate

func (r *Run) Invalidate(id string)

Invalidate asks a task to rerun. It will block until the Run gets the message (which is BEFORE the task is restarted).

func (*Run) Start

func (r *Run) Start(ctx context.Context, out MultiWriter) error

Start starts the Run, waits for it to complete, and returns an error. Remember that "long" runs will never complete until canceled.

func (*Run) TaskStatus

func (r *Run) TaskStatus(id string) TaskStatus

TaskStatus, given a task ID, returns that task's TaskStatus.

func (*Run) Tasks

func (r *Run) Tasks() Tasks

Tasks returns the Tasks that a Run would execute.

func (*Run) Type

func (r *Run) Type() RunType

Type returns the RunType of a run. It is RunTypeLong if any task is "long", otherwise it is RunTypeShort.

If a run is RunTypeShort, it will exit once all of its tasks have succeeded. If a run is RunTypeLong, it will continue running until it is interrupted. File watches are only used if a run is RunTypeLong.

type RunType

A Run's RunType is RunTypeLong if any task is "long", otherwise it is RunTypeShort.

If a run is RunTypeShort, it will exit once all of its tasks have succeeded. If a run is RunTypeLong, it will continue running until it is interrupted. File watches are only used if a run is RunTypeLong.

type RunType int
const (
    RunTypeShort RunType
    RunTypeLong
)

func (RunType) String

func (i RunType) String() string

type Task

Anything implementing Task can be run by bundling it into a Tasks and then passing it into RunTask.

ScriptTask and FuncTask can be used to create Tasks.

A Task must be safe to access concurrently from multiple goroutines.

type Task interface {
    Start(ctx context.Context, stdout io.Writer) error
    Metadata() TaskMetadata
}

func FuncTask

func FuncTask(fn func(ctx context.Context, w io.Writer) error, metadata TaskMetadata) Task

FuncTask produces a runnable Task from a go function. metadata.Dir is ignored.

func ScriptTask

func ScriptTask(script string, dir string, env []string, metadata TaskMetadata) Task

ScriptTask produces a runnable Task from a bash script and working directory. The script will execute in metadata.Dir. The script's Stdout and Stderr will be provided by the Run, and will be forwarded to the UI. The script will not get a Stdin.

Script runs in a new bash process, and can have multiple lines. It is run basically like this:

$ cd $DIR
$ bash -c "$CMD" 2&>1 /some/ui

type TaskMetadata

TaskMetadata contains the data which, regardless of the type of Task, a Run uses for task execution.

type TaskMetadata struct {
    // ID identifies a task, for example,
    //   - for command line invocation, as in `$ run <id>`
    //   - in the TUI's task list.
    ID string

    // Description optionally provides additional information about a task,
    // which can be displayed, for example, by running `run -list`. It can
    // be one line or many lines.
    Description string

    // Type specifies how we manage a task.
    //
    // If the Type is "long",
    //   - We will keep the task alive by restarting it if it exits.
    //   - If the long task A is a dependency of task B, we will begin B as
    //     soon as A starts.
    //   - It is invalid to use a long task as a trigger, since long tasks
    //     aren't expected to end.
    //
    // If the Type is "short",
    //   - If the Start returns nil, we will consider it done.
    //   - If the Start returns an error, we will wait 1 second and rerun it.
    //   - If the short task A is a dependency or trigger of task B, we will
    //     wait for A to complete before starting B.
    //
    // Any Type besides "long" or "short" is invalid. There is no default
    // type: every task must specify its type.
    Type string

    // Dependencies are other tasks IDs which should always run alongside
    // this task. If a task A lists B as a dependency, running A will first
    // run B.
    //
    // Dependencies do not set up an invalidation relationship: if long
    // task A lists short task B as a dependency, and B reruns because a
    // watched file is changed, we will not restart A, assuming that A has
    // its own mechanism for detecting file changes. If A does not have
    // such a mechanhism, use a trigger rather than a dependency.
    //
    // Dependencies can be task IDs from child directories. For example,
    // the dependency "css/build" specifies the task with ID "build" in the
    // tasks file "./css/tasks.toml".
    //
    // If a task depends on a "long" task, Run doesn't really know when the
    // long task has produced whatever output is depended on, so the
    // dependent is run 500ms after the long task starts.
    Dependencies []string

    // Triggers are other task IDs which should always be run alongside
    // this task, and whose success should cause this task to re-execute.
    // If a task A lists B as a dependency, and both A and B are running,
    // successful execution of B will always trigger an execution of A.
    //
    // Triggers can be task IDs from child directories. For example, the
    // trigger "css/build" specifies the task with ID "build" in the tasks
    // file "./css/tasks.toml".
    //
    // It is invalid to use a "long" task as a trigger.
    Triggers []string

    // Watch specifies file paths where, if a change to
    // the file path is detected, we should restart the
    // task. Watch supports globs, and does **not**
    // support the "./..." style used typical of Go
    // command line tools.
    //
    // For example,
    //  - `"."` watches for changes to the working
    //    directory only, but not changes within
    //    subdirectories.
    //  - `"**" watches for changes at any level within
    //    the working directory.
    //  - `"./some/path/file.txt"` watches for changes
    //    to the file, which must already exist.
    //  - `"./src/website/**/*.js"` watches for changes
    //    to javascript files within src/website.
    Watch []string
}

type TaskStatus

type TaskStatus int
const (
    TaskStatusNotStarted TaskStatus
    TaskStatusRunning
    TaskStatusRestarting
    TaskStatusFailed
    TaskStatusDone
)

func (TaskStatus) String

func (i TaskStatus) String() string

type Tasks

Tasks is an opaque data structure representing an immutable, ordered collection of [Task]s. You can create a Run by passing a Tasks into RunTask.

type Tasks struct {
    // contains filtered or unexported fields
}

func Load

func Load(cwd string) (Tasks, error)

Load loads a task file from the specified directory, producing a set of Tasks.

func NewTasks

func NewTasks(tasks []Task) Tasks

NewTasks creates a Tasks from the given slice of tasks.

func (Tasks) Get

func (ts Tasks) Get(id string) Task

Get looks up a specific task by its ID. If no task bearing that ID is present, the task will be nil.

func (Tasks) Has

func (ts Tasks) Has(id string) bool

Has returns true if the given ID is present among the Tasks.

func (Tasks) IDs

func (ts Tasks) IDs() []string

IDs returns the task IDs in their canonical order.

func (Tasks) Validate

func (ts Tasks) Validate() error

Validate inspects a set of Tasks and returns an error if the set is invalid. If the error is not nill, its [error.Error] will return a formatted multiline string describing the problems with the task set.

type UI

A UI is essentially a multiplexed io.Writer that can be started and stopped. Since UIs implement MultiWriter, they can be passed into Run.Start to display run execution.

The functions NewTUI and NewPrinter produce implementors of UI.

type UI interface {
    Start(ctx context.Context, ready chan<- struct{}, stdin io.Reader, stdout io.Writer) error
    Writer(id string) io.Writer
}

func NewPrinter

func NewPrinter(run *Run) UI

NewPrinter produces a non-interactive UI for displaying interleaved multiplexed streams. The UI prints interleaved output from all of the streams to its Stdout. The output is suitable for piping to a file.

The UI can be passed into Run.Start to display a run's execution.

The UI is safe to access concurrently from multiple goroutines.

func NewTUI

func NewTUI(run *Run) UI

NewTUI produces an interactive terminal UI for displaying mulitplexed streams. The UI shows a list of the streams, and allows keyboard and mouse navigation for selecting a particular stream to inspect.

The UI can be passed into Run.Start to display a run's execution.

The UI is safe to access concurrently from multiple goroutines.