Skip to content

Commit 939256e

Browse files
author
Kamil Trzciński
committed
Add 'pluginhook/' from commit '08d13ab863969ccd6889e71a1963d931df086e82'
git-subtree-dir: pluginhook git-subtree-mainline: f972db7 git-subtree-split: 08d13ab
2 parents f972db7 + 08d13ab commit 939256e

15 files changed

Lines changed: 267 additions & 0 deletions

File tree

‎pluginhook/.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pluginhook
2+
package

‎pluginhook/.travis.yml‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
language: go
3+
go:
4+
- 1.1
5+
script: go get "code.google.com/p/go.crypto/ssh/terminal" && make test
6+
before_install:
7+
- git clone https://github.com/sstephenson/bats.git /tmp/bats && sudo /tmp/bats/install.sh /usr/local
8+
env:
9+
global:
10+
- ARTIFACTS_AWS_REGION=us-east-1
11+
- ARTIFACTS_S3_BUCKET=progrium-pluginhook
12+
- secure: "eGrQigTCcuV/UiiNtAWUyTxgSVinyu7GliN8zVzWBsT2Qcp+YWYIc3vbbMFV\ntBizU6MLUvhvdpbTV7hQN1X4kP7DIBnMuNpuF38vcolFHlDh57rFNj6A6HjA\nlOkpScF0APuaqd50bIxjDOs3cR9Ef2JqmBaFNVR+gJ8poG485Rw="
13+
- secure: "OWHMXQRDXPBU6KUmTiCueteT2TJDlebzl3dU3WRjvYK/hzt6mstpjGyr+7lF\nb+GdKiik8xLf452uC/bhX1iLmeYw7kmHU6y9434EHvp6MjNIsN/Zkb9urwZp\nRIKO0dNeRvNUipPE9QhI+DfqgBoxbGQSWXcpGLpfNsQ7racCzTs="

‎pluginhook/Makefile‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
VERSION = 0.1.1
2+
3+
build: pluginhook
4+
5+
pluginhook:
6+
go build -o pluginhook
7+
GOOS=linux go build -o pluginhook.linux
8+
9+
test: build
10+
PLUGIN_PATH=./tests/plugins BIN=./pluginhook tests/run_tests.sh
11+
12+
package: test build
13+
rm -rf package
14+
mkdir -p package/bin
15+
mv pluginhook.linux package/bin/pluginhook
16+
cd package && fpm -s dir -t deb -n pluginhook -v ${VERSION} --prefix /usr/local bin
17+
18+
release: package
19+
s3cmd -P put package/pluginhook_${VERSION}_amd64.deb s3://progrium-pluginhook

‎pluginhook/README.md‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# pluginhook
2+
3+
A simple plugin system for Bash programs that's better than just hook scripts.
4+
5+
## Plugins as a better way
6+
7+
Let's take the core benefits of hook scripts and re-structure it slightly:
8+
9+
1. Instead of focusing on hook scripts, we focus on plugins -- a directory of hook scripts
10+
1. Like hook scripts, plugins are active by being in a certain place. But they can be named anything
11+
1. Multiple plugins can handle a hook. Either for fanout event triggering, or for pipeline filtering
12+
13+
So what is a plugin? A directory full of hook scripts. When a hook is triggered, all arguments provided
14+
in the trigger are given to each hook script. They also receive STDIN in a pipelined fashion.
15+
16+
## Triggering plugin hooks
17+
18+
You use the `pluginhook` command to trigger hooks as if you might call a traditional hook script directly.
19+
Where before you might have triggered by calling something like:
20+
21+
hooks/post-commit $REV $USER
22+
23+
You'd instead trigger like this:
24+
25+
pluginhook post-commit $REV $USER
26+
27+
The `pluginhook` command simply loops through all plugin directories found in the path defined by the environment variable `PLUGIN_PATH` and passes the same arguments to any hook scripts by that name. This means installing a plugin is as simple as putting it in your `PLUGIN_PATH`. Then any plugin that has the `post-commit` hook script will be run.
28+
29+
## Pipeline filtering with plugins
30+
31+
You don't just get a "broadcast" mechanism for arguments. You also get stream pipelining. If you pipe a stream
32+
into pluginhook, it will be passed *through* each plugin hook, letting each plugin act as a filtering process. By clearly
33+
defining how a hook should be used and how it can play well with others, this becomes very powerful infrastructure.
34+
35+
Here is a plugin we'll call `upper` implementing a `text` hook (which would be in `$PLUGIN_PATH/upper/text`):
36+
37+
#!/usr/bin/env python
38+
import sys
39+
sys.stdout.write(sys.stdin.read().upper())
40+
41+
Here is a plugin we'll call `reverse` that also implements a `text` hook (`$PLUGIN_PATH/reverse/text`):
42+
43+
#!/usr/bin/env ruby
44+
puts STDIN.read.strip.reverse
45+
46+
One plugin uses Python to implement the hook, the other uses Ruby. But it doesn't matter, they work together when you trigger the hook:
47+
48+
$ echo "hello world" | pluginhook text
49+
DLROW OLLEH
50+
51+
Only plugins that implement a hook are used as filters for that hook, so there's no need to implement pass-through hooks if a
52+
plugin doesn't care about a hook.
53+
54+
If ordering is important, you can always rename your plugin directory to start with a number, which will define an order of
55+
execution. A plugin author might care about when it is run, but it's up to the user to take their advice or decide
56+
to run it in a different position in the order, by simply renaming the plugin script.
57+
58+
## What's wrong with just hook scripts?
59+
60+
Lots of shell-based systems use hook scripts as a means to allow users to extend or customize behavior. Popular examples
61+
are [Git](https://www.kernel.org/pub/software/scm/git/docs/githooks.html) and [SVN](http://svnbook.red-bean.com/nightly/en/svn.ref.reposhooks.html), but many systems from [libvirt](http://www.libvirt.org/hooks.html) to [NPM](https://npmjs.org/doc/scripts.html) to [OS X](http://superuser.com/questions/295924/how-to-run-a-script-at-login-logout-in-os-x) use this pattern. Shell scripts make
62+
for a great way to expose hooks because the shell environment is ubiquitous and lets you easily call into
63+
scripts or programs written in your language of choice.
64+
65+
The standard implementation of hook scripts is to have a shell script with an execute bit in a particular location
66+
that's named after the hook. The most famous example is the "post-commit" hook of SVN, which if `hooks/post-commit`
67+
exists in your repository directory with an execute bit, it will trigger this script after each commit. Some systems
68+
let you register hooks by providing a location instead of using convention.
69+
70+
However, in either case, each hook points to **one** script. The only way for a third-party piece of software to
71+
"hook in" is to install itself as the one hook script, or have you manually install it by calling it from your
72+
existing hook script. What's more, if a third-party piece of software wants to use multiple hooks, you have to
73+
deal with this several times over. Not only is this a hassle, but leads to complex and non-obvious configurations.
74+
75+
## Author
76+
77+
Jeff Lindsay <progrium@gmail.com>
78+
79+
## License
80+
81+
MIT

‎pluginhook/pluginhook.go‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"fmt"
6+
"os/exec"
7+
"syscall"
8+
"path/filepath"
9+
"log"
10+
"flag"
11+
"bytes"
12+
"strings"
13+
"code.google.com/p/go.crypto/ssh/terminal"
14+
)
15+
16+
func main() {
17+
var parallel = flag.Bool("p", false, "Run hooks in parallel")
18+
var trace = flag.Bool("x", false, "Trace mode")
19+
flag.Parse()
20+
21+
if len(os.Getenv("PLUGINHOOK_TRACE")) > 0 {
22+
*trace = true
23+
}
24+
25+
pluginPath := os.Getenv("PLUGIN_PATH")
26+
if pluginPath == "" {
27+
log.Fatal("[ERROR] Unable to locate plugins: set $PLUGIN_PATH\n")
28+
os.Exit(1)
29+
}
30+
if flag.NArg() < 1 {
31+
log.Fatal("[ERROR] Hook name argument is required\n")
32+
os.Exit(1)
33+
}
34+
cmds := make([]exec.Cmd, 0)
35+
var matches, _ = filepath.Glob(fmt.Sprintf("%s/*/%s", pluginPath, flag.Arg(0)))
36+
for _, hook := range matches {
37+
cmd := exec.Command(hook, flag.Args()[1:]...)
38+
cmds = append(cmds, *cmd)
39+
}
40+
for i := len(cmds)-1; i >= 0; i-- {
41+
cmds[i].Stderr = os.Stderr
42+
43+
if i == len(cmds)-1 {
44+
cmds[i].Stdout = os.Stdout
45+
}
46+
if i > 0 {
47+
if *parallel {
48+
stdout, err := cmds[i-1].StdoutPipe()
49+
if err != nil {
50+
log.Fatal(err)
51+
}
52+
cmds[i].Stdin = stdout
53+
} else {
54+
var buf bytes.Buffer
55+
cmds[i-1].Stdout = &buf
56+
cmds[i].Stdin = &buf
57+
}
58+
}
59+
if i == 0 && !terminal.IsTerminal(syscall.Stdin) {
60+
cmds[i].Stdin = os.Stdin
61+
}
62+
}
63+
64+
if *parallel {
65+
done := make(chan bool, len(cmds))
66+
67+
for i := 0; i < len(cmds); i++ {
68+
go func(cmd exec.Cmd, i int) {
69+
if *trace {
70+
fmt.Fprintln(os.Stderr, "+", strings.Join(cmds[i].Args, " "))
71+
}
72+
err := cmd.Run()
73+
if msg, ok := err.(*exec.ExitError); ok { // there is error code
74+
os.Exit(msg.Sys().(syscall.WaitStatus).ExitStatus())
75+
}
76+
done <- true
77+
}(cmds[i], i)
78+
}
79+
for i := 0; i < len(cmds); i++ {
80+
<-done
81+
}
82+
} else {
83+
for i := 0; i < len(cmds); i++ {
84+
if *trace {
85+
fmt.Fprintln(os.Stderr, "+", strings.Join(cmds[i].Args, " "))
86+
}
87+
err := cmds[i].Run()
88+
if msg, ok := err.(*exec.ExitError); ok { // there is error code
89+
os.Exit(msg.Sys().(syscall.WaitStatus).ExitStatus())
90+
}
91+
}
92+
}
93+
}

‎pluginhook/tests/plugins/bar/cat‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
cat
3+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
cat
3+
echo "bar pre-build $2 $3 $4 $5"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/bin/bash
2+
echo "bar pre-deploy"

‎pluginhook/tests/plugins/bar/text‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env ruby
2+
puts STDIN.read.strip.reverse

‎pluginhook/tests/plugins/bar/time‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/bash
2+
date +%s
3+
sleep 2
4+

0 commit comments

Comments
 (0)