ppow

package module
v0.8.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 12, 2024 License: MIT Imports: 17 Imported by: 0

README

ppow is a developer tool that triggers commands and manages daemons in response to filesystem changes. It is a fork of excellent but unmaintained modd.

If you use ppow, you should also look at devd, a compact HTTP daemon for developers. Devd integrates with ppow, allowing you to trigger in-browser livereload with ppow.

The repo contains a set of example ppow.conf files that you can look at for a quick idea of what ppow can do:

Example Description
frontend.conf A front-end project with React + Browserify + Babel. ppow and devd replace many functions of Gulp/Grunt.
go.conf Live unit tests for Go.
python.conf Python + Redis, with devd managing livereload.

Install

ppow is a single binary with no external dependencies, released for OSX, Windows, Linux, FreeBSD, NetBSD and OpenBSD. Go to the releases page, download the package for your OS, and copy the binary to somewhere on your PATH.

Alternatively, with Go 1.17+ installed, you can install ppow directly using go install. Please note that CGO is required, so if you happen to have it disabled you will need to prepend the CGO_ENABLED=1 environment variable.

$ go install github.com/dottedmag/ppow/cmd/ppow@latest

Quick start

Put this in a file called ppow.conf:

**/*.go {
    prep: go test @dirmods
}

Now run ppow like so:

$ ppow

The first time ppow is run, it will run the tests of all Go modules. Whenever any file with the .go extension is modified, the "go test" command will be run only on the enclosing module.

Details

On startup, ppow looks for a file called ppow.conf in the current directory. This file has a simple but powerful syntax - one or more blocks of commands, each of which can be triggered on changes to files matching a set of file patterns. The ppow.conf file is meant to be portable, and can safely be checked into source repositories. Functionality that users will want to customize (like desktop notifications) is controlled through command-line flags.

Commands have two flavors: prep commands that run and terminate (e.g. compiling, running test suites or running linters), and daemon commands that run and keep running (e.g databases or webservers). Daemons are sent a SIGHUP (by default) when their block is triggered, and are restarted if they ever exit.

Prep commands are run in order of occurrence. If any prep command exits with an error, execution of the current block is stopped immediately. If all prep commands succeed, any daemons in the block are restarted, also in order of occurrence. If multiple blocks are triggered by the same set of changes, they too run in order, from top to bottom.

Here's a modified version of the ppow.conf file I use when hacking on devd. It runs the test suite whenever a .go file changes, builds devd whenever a non-test file is changed, and keeps a test instance running throughout.

**/*.go {
    prep: go test @dirmods
}

# Exclude all test files of the form *_test.go
**/*.go !**/*_test.go {
    prep: go install ./cmd/devd
    daemon +sigterm: devd -m ./tmp
}

The @dirmods variable expands to a properly escaped list of all directories containing changed files. When ppow is first run, this includes all directories containing matching files. So, this means that ppow will run all tests on startup, and then subsequently run the tests only for the affected module whenever there's a change. There's a corresponding @mods variable that contains all changed files.

Note the +sigterm flag to the daemon command. When devd receives a SIGHUP (the default signal sent by ppow), it triggers a browser livereload, rather than exiting. This is what you want when devd is being used to serve a web project you're hacking on, but when developing devd itself, we actually want it to exit and restart to pick up changes. We therefore tell ppow to send a SIGTERM to the daemon instead, which causes devd to exit and be restarted by ppow.

By default ppow interprets commands using a built-in POSIX-like shell. Some external shells are also supported, and can be used by setting @shell variable in your "ppow.conf" file.

File watch patterns

ppow batches up changes until there is a lull in filesystem activity - this means that coherent processes like compilation and rendering that touch many files are likely to trigger commands only once. Patterns therefore match on a batch of changed files - when the first match in a batch is seen, the block is triggered.

Patterns and the paths they match against are always in slash-delimited form, even on Windows. Paths are cleaned and normalised being matched, with redundant components removed. If the path is within the current working directory, the normalised path is relative to the current working directory, otherwise it is absolute. One subtlety is that this means that a pattern like ./*.js will never match, because inbound paths will not have a leading ./ component - just use *.js instead.

Quotes

File patterns can be naked or quoted strings. Quotes can be either single or double quotes, and the corresponding quote mark can be escaped with a backslash within the string:

"**/foo\"bar"

Negation

Patterns can be negated with a leading !. For quoted patterns, the exclamation mark goes outside of the quotes. So, this matches all files recursively, bar those with a .html extension and those in the docs directory.

** !**/*.html !"docs/**"

Negations are applied after all positive patterns - that is, ppow collects all files matching the positive patterns, then removes files matching the negation patterns.

Default ignore list

Common nuisance files like VCS directories, swap files, and so forth are ignored by default. You can list the set of ignored patterns using the -i flag to the ppow command. The default ignore patterns can be disabled using the special +noignore flag, like so:

.git/config +noignore {
    prep: echo "git config changed"
}

Empty match pattern

If no match pattern is specified, prep commands run once only at startup, and daemons are restarted if they exit, but won't ever be explicitly signalled to restart by ppow.

{
    prep: echo hello
}

ppow does not implicitly traverse symlinks. To monitor a symlink, split the path specification and the matching pattern, like this:

mydir/symlinkdir foo.* {
    prep: echo changed
}

Behind the scenes, we resolve the symlinked directory as if it was specified directly by the user. This means that if the symlink destination lies outside of the current working directory, the resulting paths for matches, exclusions and commands will be absolute.

Syntax

File patterns support the following syntax:

Term Meaning
* any sequence of non-path-separators
** any sequence of characters, including path separators
? any single non-path-separator character
[class] any single non-path-separator character against a class of characters
{alt1,...} any of the comma-separated alternatives - to avoid conflict with the block specification, patterns with curly-braces should be enclosed in quotes

Any character with a special meaning can be escaped with a backslash (\). Character classes support the following:

Class Meaning
[abc] any character within the set
[a-z] any character in the range
[^class] any character which does not match the class

Blocks

Each file match pattern specification has an associated block, which is enclosed in curly brackets. Blocks contain commands and block-scoped options.

Single-line commands don't need to be quoted:

prep: echo "I'm now rebuilding" | tee /tmp/output

Newlines can be escaped with a backslash for multi-line commands:

prep: ls \
        -l \
        -a

You can also enclose commands in single or double quotes, letting easily specify compound, multi-statement commands. These can contain anything you'd normally put in a shell script, and the same quoting and escaping conventions apply.

prep: "
    ls \
        -l \
        -a
    echo \"hello again\"
    echo \"hello yet again\"
"

Within commands, the @ character is treated specially, since it is the marker for variable replacement. You can include a verbatim @ symbol b escaping it with a backslash, and backslashes preceding the @ symbol can themselves be escaped recursively.

@foo = bar
{
    prep: echo "@foo"   # bar
    prep: echo "\@foo"  # @foo
    prep: echo "\\@foo" # \bar
}

Prep commands

All prep commands in a block are run in order before any daemons are restarted. If any prep command exits with an error, execution stops.

The following variables are automatically generated for prep commands

Variable Meaning
@mods On first run, all files matching the block patterns. On subsequent change, a list of all modified files.
@confdir The absolute path of the directory that contains the current ppow config file.
@dirmods On first run, all directories containing files matching the block patterns. On subsequent change, a list of all directories containing modified files.

All file names in variables are relative to the current directory, and shell-escaped for safety. All paths are in slash-delimited form on all platforms.

Given a config file like this, ppow will run eslint on all .js files when started, and then after that only run eslint on files if they change:

**/*.js {
    prep: eslint @mods
}

By default, prep commands are executed on the initial run of ppow. The +onchange option can be used to skip the initial run, and only execute when there is a detected change.

*.go {
	# only trigger on file changes
	prep +onchange: go test
}

Daemon commands

Daemons are executed on startup, and are restarted by ppow whenever they exit. When a block containing a daemon command is triggered, ppow sends a signal to the daemon process group. If the signal causes the daemon to exit, it is immediately restarted by ppow - however, it's also common for daemons to do other useful things like reloading configuration in response to signals.

The default signal used is SIGHUP, but the signal can be controlled using modifier flags, like so:

daemon +sigterm: mydaemon --config ./foo.conf

The following signals are supported: sighup, sigterm, sigint, sigkill, sigquit, sigusr1, sigusr2, sigwinch.

Support for signals on Windows is limited. The signal type is ignored, and all daemons are stopped and restarted when a signal would normally be sent.

The following variables are automatically generated for prep commands

Variable Meaning
@confdir The absolute path of the directory that contains the current ppow config file.

Controlling log headers

ppow outputs a short header on the terminal to show which command is responsible for output. This header is calculated from the first non-whitespace line of the command - backslash escapes are removed from the end of the line, comment characters are removed from the beginning, and whitespace is stripped. Using the fact that the shell itself permits comments, you can completely control the log display name.

{
    # This will show as "prep: mycommand"
    prep: "
        mycommand \
            --longoption 1 \
            --longoption 2
    "
    # This will show as "prep: daemon 1"
    prep: "
        # daemon 1
        mycommand \
            --longoption 1 \
            --longoption 2
    "
}

Options

The only block option at the moment is indir, which controls the execution directory of a block. ppow will change to this directory before executing commands and daemons, and change back to the previous directory afterwards.

The directory specification follows the same conventions as commands, and can be enclosed in quotes to span multiple lines.

{
    indir: ./my/directory
    prep: ls
}

Variables

Variables are declared as follows:

@variable = value

Variables can only be declared in the global scope (i.e. not inside blocks). All values are strings and follow the same semantics as commands - that is, they can have escaped line endings, or be quoted strings. Variables are read once at startup, and it is an error to re-declare a variable that already exists.

You can use variables in commands like so:

@dst = ./build/dst
** {
    prep: ls @dst
}

There is a special "@shell" variable that determines which shell is used to execute commands. Valid values are bash, sh (the default) and powershell. This variable is set as follows:

@shell = bash

Desktop Notifications

When the -n flag is specified, ppow sends anything sent to stderr from any prep command that exits abnormally to a desktop notifier. Since ppow commands are shell scripts, you can redirect or manipulate output to entirely customise what gets sent to notifiers as needed.

At the moment, we support Growl on OSX, and libnotify on Linux and other Unix systems.

Growl

For Growl to work, you will need Growl itself to be running, and have the growlnotify command installed. Growlnotify is an additional tool that you can download from the official Growl website.

Libnotify

Libnotify is a general notification framework available on most Unix-like systems. ppow uses the notify-send command to send notifications using libnotify. You'll need to use your system package manager to install libnotify.

Colour output in process logs

Some programs that have colourised output when run on the command-line don't emit colour when run under ppow. Users might assume that ppow is stripping the colour from the command output, but that is not the case. Well-behaved terminal programs check whether they are connected to a terminal, and if not, disable colour codes in their own output. It is possible to trick a program into believing that a terminal is present through pseudo-terminal emulation, but this is complex and platform dependent and is not a good fit for a simple, reliable tool like ppow.

This leaves users with two options:

  • Many tools that produce colour output also have a flag to force colour when no terminal is detected, and many logging libraries with human-friendly output do the same. The simplest solution is to work out how to force output and explicitly specify this in your ppow configuration.
  • There are platform-specific tools you can interpose between ppow and the subprocess to emulate a terminal. One example is unbuffer on Linux.

Development

The scripts used to build this package for distribution can be found here.

Documentation

Index

Constants

View Source
const (
	// MinRestart is the minimum amount of time between daemon restarts
	MinRestart = 10 * time.Millisecond
	// MulRestart is the exponential backoff multiplier applied when the daemon exits uncleanly
	MulRestart = 1.5
	// MaxRestart is the maximum amount of time between daemon restarts
	MaxRestart = 500 * time.Millisecond
)
View Source
const Version = "0.9-pre"

Version is the ppow release version

Variables

View Source
var CommonExcludes = []string{

	"**/.git/**",
	"**/.hg/**",
	"**/.svn/**",
	"**/.bzr/**",

	"**/.DS_Store/**",

	"**.tmp",
	"**~",
	"**#",
	"**.bak",
	"**.swp",
	"**.___jb_old___",
	"**.___jb_bak___",
	"**mage_output_file.go",

	"**.py[cod]",

	"**/node_modules/**",
}

CommonExcludes is a list of commonly excluded files suitable for passing in the excludes parameter to Watch - includes repo directories, temporary files, and so forth.

View Source
var Default = "sh"
View Source
var ValidShells = map[string]bool{
	"bash":       true,
	"powershell": true,
	"sh":         true,
}

Functions

func CheckShell

func CheckShell(shell string) (string, error)

CheckShell checks that a shell is supported, and returns the correct command name

func GetShellName

func GetShellName(v string) (string, error)

func RunPreps

func RunPreps(
	b conf.Block,
	vars map[string]string,
	mod *moddwatch.Mod,
	log termlog.TermLog,
	notifiers []Notifier,
	initial bool,
) error

RunPreps runs all commands in sequence. Stops if any command returns an error.

func RunProc

func RunProc(cmd string, shellMethod string, dir string, log termlog.Stream) error

RunProc runs a process to completion, sending output to log

Types

type BeepNotifier

type BeepNotifier struct{}

BeepNotifier just emits a beep on the terminal

func (*BeepNotifier) Push

func (*BeepNotifier) Push(string, string, string)

Push implements Notifier

type DaemonPen

type DaemonPen struct {
	sync.Mutex
	// contains filtered or unexported fields
}

DaemonPen is a group of daemons in a single block, managed as a unit.

func NewDaemonPen

func NewDaemonPen(block conf.Block, vars map[string]string, log termlog.TermLog) (*DaemonPen, error)

NewDaemonPen creates a new DaemonPen

func (*DaemonPen) Restart

func (dp *DaemonPen) Restart()

Restart all daemons in the pen, or start them if they're not running yet.

func (*DaemonPen) Shutdown

func (dp *DaemonPen) Shutdown(sig os.Signal)

Shutdown all daemons in the pen

func (*DaemonPen) Signal

func (dp *DaemonPen) Signal(sig os.Signal)

type DaemonWorld

type DaemonWorld struct {
	DaemonPens []*DaemonPen
}

DaemonWorld represents the entire world of daemons

func NewDaemonWorld

func NewDaemonWorld(cnf *conf.Config, log termlog.TermLog) (*DaemonWorld, error)

NewDaemonWorld creates a DaemonWorld

func (*DaemonWorld) Shutdown

func (dw *DaemonWorld) Shutdown(s os.Signal)

Shutdown all daemons with signal s

func (*DaemonWorld) Signal

func (dw *DaemonWorld) Signal(s os.Signal)

type ExecState

type ExecState struct {
	Error     error
	ErrOutput string
	ProcState string
}

type Executor

type Executor struct {
	Shell   string
	Command string
	Dir     string

	sync.Mutex
	// contains filtered or unexported fields
}

func NewExecutor

func NewExecutor(shell string, command string, dir string) (*Executor, error)

func (*Executor) Run

func (e *Executor) Run(log termlog.Stream, bufferr bool) (error, *ExecState)

func (*Executor) Running

func (e *Executor) Running() bool

func (*Executor) Signal

func (e *Executor) Signal(sig os.Signal) error

type GrowlNotifier

type GrowlNotifier struct {
}

GrowlNotifier is a notifier for Growl

func (GrowlNotifier) Push

func (GrowlNotifier) Push(title string, text string, iconPath string)

Push implements Notifier

type LibnotifyNotifier

type LibnotifyNotifier struct {
}

LibnotifyNotifier is a notifier for lib-notify

func (LibnotifyNotifier) Push

func (LibnotifyNotifier) Push(title string, text string, iconPath string)

Push implements Notifier

type ModRunner

type ModRunner struct {
	Log        termlog.TermLog
	Config     *conf.Config
	ConfPath   string
	ConfReload bool
	Notifiers  []Notifier
	// contains filtered or unexported fields
}

ModRunner coordinates running the ppow command

func NewModRunner

func NewModRunner(confPath string, log termlog.TermLog, notifiers []Notifier, confreload bool) (*ModRunner, error)

NewModRunner constructs a new ModRunner

func (*ModRunner) PrepOnly

func (mr *ModRunner) PrepOnly(initial bool) error

PrepOnly runs all prep functions and exits

func (*ModRunner) ReadConfig

func (mr *ModRunner) ReadConfig() error

ReadConfig parses the configuration file in ConfPath

func (*ModRunner) Run

func (mr *ModRunner) Run() error

Run is the top-level runner for ppow

type Notifier

type Notifier interface {
	Push(title string, content string, icon string)
}

A Notifier notifies

func PlatformNotifier

func PlatformNotifier() Notifier

PlatformNotifier finds a notifier for this platform

type ProcError

type ProcError struct {
	Output string
	// contains filtered or unexported fields
}

ProcError is a process error, possibly containing command output

func (ProcError) Error

func (p ProcError) Error() string

type VarCmd

type VarCmd struct {
	Block    *conf.Block
	Modified []string
	Vars     map[string]string
}

VarCmd represents a set of variables for a specific block and mod set. It should be re-created anew each time the block is executed.

func (*VarCmd) Render

func (v *VarCmd) Render(cmd string) (string, error)

Render renders the command with a map of variables

Directories

Path Synopsis
cmd
ppow command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL