Skip to content

Commit 18fb000

Browse files
authored
Full duplex (#30)
* duplex rewrite * fix CLI usage bug * fix race * redesign api * update README.md
1 parent 10a22ee commit 18fb000

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+1636
-1110
lines changed

Gopkg.lock

Lines changed: 8 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,39 @@ go build github.com/wybiral/hookah/cmd/hookah
1414
```
1515

1616
# Usage instructions (CLI)
17-
The hookah command allows you to specify an input source -i and an output destination -o.
18-
Any data that's fed into the input will be piped to the output.
17+
The hookah command allows you to pipe data betweem various sources/destinations.
18+
By default pipes are full duplex but can be limited to input/output-only mode.
19+
20+
For details run `hookah -h`
1921

2022
## Examples
2123

22-
Pipe from stdin to a new TCP listener on port 8080:
24+
Pipe from stdin/stdout to a new TCP listener on port 8080:
25+
```
26+
hookah stdio tcp-listen://localhost:8080
27+
```
28+
Note: this is the same even if you ommit the `stdio` part because hookah will
29+
assume stdio is indended when only one node (tcp-listen in this case) is used.
30+
31+
Pipe from an existing TCP listener on port 8080 to a new WebSocket listener on
32+
port 8081:
2333
```
24-
hookah -o tcp-listen://localhost:8080
34+
hookah tcp-listen://localhost:8080 ws-listen://localhost:8081
2535
```
2636

27-
Pipe from an existing TCP listener on port 8080 to a new HTTP listener on port 8081:
37+
Pipe from a new Unix domain socket listener to a TCP client on port 8080:
2838
```
29-
hookah -i tcp://localhost:8080 -o http-listen://localhost:8081
39+
hookah unix-listen://path/to/sock tcp://localhost:8080
3040
```
3141

32-
Pipe from a new Unix domain socket listener to stdout:
42+
Pipe only the input from a TCP client to the output of another TCP client:
3343
```
34-
hookah -i unix-listen://path/to/sock
44+
hookah -i tcp://:8080 -o tcp://:8081
3545
```
3646

37-
Pipe from a new HTTP listener on port 8080 to an existing Unix domain socket:
47+
Fan-out the input from a TCP listener to the output of multiple TCP clients:
3848
```
39-
hookah -i http-listen://localhost:8080 -o unix://path/to/sock
49+
hookah -i tcp-listen://:8080 -o tcp://:8081 -o tcp://:8082 -o tcp://:8083
4050
```
4151

4252
# Usage instructions (Go package)

cmd/hookah/main.go

Lines changed: 90 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,65 +2,116 @@
22
package main
33

44
import (
5+
"errors"
56
"flag"
67
"fmt"
7-
"io"
8-
"log"
98
"os"
109
"os/signal"
1110

1211
"github.com/wybiral/hookah"
12+
"github.com/wybiral/hookah/internal/app"
13+
"github.com/wybiral/hookah/pkg/flagslice"
1314
)
1415

1516
func main() {
1617
// Create hookah API instance
1718
h := hookah.New()
1819
flag.Usage = func() {
19-
fmt.Print("NAME:\n")
20-
fmt.Print(" hookah\n\n")
21-
fmt.Print("USAGE:\n")
22-
fmt.Print(" hookah -i input -o output\n\n")
23-
fmt.Print("VERSION:\n")
24-
fmt.Printf(" %s\n\n", hookah.Version)
25-
fmt.Print("INPUTS:\n")
26-
for _, reg := range h.ListInputs() {
27-
fmt.Printf(" %s\n", reg.Usage)
28-
}
29-
fmt.Print("\n")
30-
fmt.Print("OUTPUTS:\n")
31-
for _, reg := range h.ListOutputs() {
32-
fmt.Printf(" %s\n", reg.Usage)
33-
}
34-
fmt.Print("\n")
20+
usage(h)
3521
os.Exit(0)
3622
}
3723
// Parse flags
38-
var inOpts string
39-
flag.StringVar(&inOpts, "i", "stdin", "Stream input")
40-
var outOpts string
41-
flag.StringVar(&outOpts, "o", "stdout", "Stream output")
24+
var opts, rOpts, wOpts flagslice.FlagSlice
25+
flag.Var(&rOpts, "i", "input node (readonly)")
26+
flag.Var(&wOpts, "o", "output node (writeonly)")
4227
flag.Parse()
43-
// Setup input stream
44-
r, err := h.NewInput(inOpts)
28+
opts = flag.Args()
29+
// Run and report errors
30+
err := run(h, opts, rOpts, wOpts)
31+
if err != nil {
32+
fmt.Fprintln(os.Stderr, err)
33+
os.Exit(1)
34+
}
35+
os.Exit(0)
36+
}
37+
38+
func run(h *hookah.API, opts, rOpts, wOpts []string) error {
39+
a := app.New(nil)
40+
// Closing this App instance will close all the nodes being created
41+
defer a.Close()
42+
// Add bidirectional nodes
43+
err := addNodes(h, a, opts, true, true)
44+
if err != nil {
45+
return err
46+
}
47+
// Add reader (input) nodes
48+
err = addNodes(h, a, rOpts, true, false)
4549
if err != nil {
46-
log.Fatal(err)
50+
return err
4751
}
48-
defer r.Close()
49-
// Setup output stream
50-
w, err := h.NewOutput(outOpts)
52+
// Add writer (output) nodes
53+
err = addNodes(h, a, wOpts, false, true)
5154
if err != nil {
52-
log.Fatal(err)
55+
return err
56+
}
57+
// No nodes, show usage
58+
if len(a.Nodes) == 0 {
59+
flag.Usage()
60+
return nil
61+
}
62+
// Only one node, link to stdio
63+
if len(a.Nodes) == 1 {
64+
n, err := h.NewNode("stdio")
65+
if err != nil {
66+
return err
67+
}
68+
a.AddNode(n)
5369
}
54-
defer w.Close()
55-
// Listen for interrupt to close gracefully
56-
ch := make(chan os.Signal, 1)
57-
signal.Notify(ch, os.Interrupt)
70+
if len(a.Readers) == 0 {
71+
return errors.New("no input nodes")
72+
}
73+
if len(a.Writers) == 0 {
74+
return errors.New("no output nodes")
75+
}
76+
// Handle CTRL+C by sending a Quit signal
77+
interrupt := make(chan os.Signal, 1)
78+
signal.Notify(interrupt, os.Interrupt)
5879
go func() {
59-
<-ch
60-
r.Close()
61-
w.Close()
62-
os.Exit(1)
80+
<-interrupt
81+
a.Quit <- nil
6382
}()
64-
// Copy all of in to out
65-
io.Copy(w, r)
83+
return a.Run()
84+
}
85+
86+
// Add nodes to the app from opt strings and r/w status
87+
func addNodes(h *hookah.API, a *app.App, opts []string, r, w bool) error {
88+
for _, opt := range opts {
89+
n, err := h.NewNode(opt)
90+
if err != nil {
91+
return err
92+
}
93+
if !r {
94+
n.R = nil
95+
}
96+
if !w {
97+
n.W = nil
98+
}
99+
a.AddNode(n)
100+
}
101+
return nil
102+
}
103+
104+
// Print CLI usage info
105+
func usage(h *hookah.API) {
106+
fmt.Print("NAME:\n")
107+
fmt.Print(" hookah\n\n")
108+
fmt.Print("USAGE:\n")
109+
fmt.Print(" hookah node [node] -i in_node -o out_node\n\n")
110+
fmt.Print("VERSION:\n")
111+
fmt.Printf(" %s\n\n", hookah.Version)
112+
fmt.Print("PROTOCOLS:\n")
113+
for _, p := range h.ListProtocols() {
114+
fmt.Printf(" %s\n", p.Usage)
115+
}
116+
fmt.Print("\n")
66117
}

examples/certstream/main.go

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
// Example of using hookah to create an input stream from the CertStream
1+
// Example of using hookah to create an input node from the CertStream
22
// WebSocket API (https://certstream.calidog.io/).
33
// The cert updates are filtered to remove heartbeat messages and processed by
44
// restricting the JSON fields and adding indentation.
5-
// These updates are then written to stdout.
5+
// These updates are then written to stdout node.
66
package main
77

88
import (
99
"encoding/json"
10-
"io"
1110
"log"
1211

1312
"github.com/wybiral/hookah"
13+
"github.com/wybiral/hookah/pkg/node"
1414
)
1515

1616
// CertStream JSON struct
@@ -32,28 +32,26 @@ type certUpdate struct {
3232
func main() {
3333
// Create hookah API instance
3434
h := hookah.New()
35-
// Create hookah input (certstream WebSocket API)
36-
r, err := h.NewInput("wss://certstream.calidog.io")
35+
// Create hookah node (certstream WebSocket API)
36+
r, err := h.NewNode("wss://certstream.calidog.io")
3737
if err != nil {
3838
log.Fatal(err)
3939
}
40-
defer r.Close()
41-
// Create hookah output (stdout)
42-
w, err := h.NewOutput("stdout")
40+
// Create hookah node (stdout)
41+
w, err := h.NewNode("stdout")
4342
if err != nil {
4443
log.Fatal(err)
4544
}
46-
defer w.Close()
4745
// Start stream
4846
stream(w, r)
4947
}
5048

5149
// Copy from reader to writer
5250
// Drops heartbeat messages, restricts fields, and formats JSON
53-
func stream(w io.Writer, r io.Reader) {
51+
func stream(w, r *node.Node) {
5452
var u certUpdate
55-
d := json.NewDecoder(r)
56-
e := json.NewEncoder(w)
53+
d := json.NewDecoder(r.R)
54+
e := json.NewEncoder(w.W)
5755
e.SetIndent("", " ")
5856
for {
5957
err := d.Decode(&u)

examples/customproto/main.go

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,64 @@
1-
// Example of using hookah with a custom input protocol. In this case the input
2-
// protocol is named numbers:// and it can accept "odd" or "even" as the
3-
// argument.
1+
// Example of using hookah with a custom protocol. In this case the protocol is
2+
// named numbers:// and it can accept "odd" or "even" as the argument.
43
package main
54

65
import (
76
"errors"
87
"fmt"
98
"io"
109
"log"
11-
"net/url"
1210
"time"
1311

1412
"github.com/wybiral/hookah"
13+
"github.com/wybiral/hookah/pkg/node"
1514
)
1615

1716
func main() {
1817
// Create hookah API instance
1918
h := hookah.New()
2019
// Register new protocol
21-
h.RegisterInput("numbers", "numbers://parity", numbersHandler)
22-
// Create hookah input (using new numbers:// protocol)
23-
r, err := h.NewInput("numbers://odd")
20+
h.RegisterProtocol("numbers", "numbers://parity", numbersHandler)
21+
// Create hookah node (using our new numbers:// protocol)
22+
r, err := h.NewNode("numbers://odd")
2423
if err != nil {
2524
log.Fatal(err)
2625
}
27-
defer r.Close()
28-
// Create hookah output (stdout)
29-
w, err := h.NewOutput("stdout")
26+
// Create hookah node (stdout)
27+
w, err := h.NewNode("stdout")
3028
if err != nil {
3129
log.Fatal(err)
3230
}
33-
defer w.Close()
3431
// Copy forever
35-
io.Copy(w, r)
32+
io.Copy(w.W, r.R)
3633
}
3734

38-
// struct type to implement interface on.
39-
type numbers struct {
40-
counter int64
41-
}
35+
// type to implement Reader interface on.
36+
type numbers int64
4237

43-
// Input handlers take an arg string and return an io.ReadCloser for the input
44-
// stream (or an error).
45-
func numbersHandler(arg string, opts url.Values) (io.ReadCloser, error) {
46-
var counter int64
38+
// Handlers take an arg string and return a Node
39+
func numbersHandler(arg string) (*node.Node, error) {
40+
var counter numbers
4741
if arg == "odd" {
4842
counter = 1
4943
} else if arg == "even" {
5044
counter = 2
5145
} else {
5246
return nil, errors.New("numbers requires: odd or even")
5347
}
54-
return &numbers{counter: counter}, nil
48+
// Node can have R: Reader, W: Writer, C: Closer
49+
// In this case it's just a Reader
50+
return &node.Node{R: &counter}, nil
5551
}
5652

57-
// Read method satisfies the io.ReadCloser interface
53+
// Read the next number (after delay) and increment counter.
5854
func (num *numbers) Read(b []byte) (int, error) {
5955
// Artificial delay
6056
time.Sleep(time.Second)
6157
// Format counter
62-
s := fmt.Sprintf("%d\n", num.counter)
58+
s := fmt.Sprintf("%d\n", *num)
6359
// Increment counter
64-
num.counter += 2
60+
*num += 2
6561
// Copy to byte array
6662
n := copy(b, []byte(s))
6763
return n, nil
6864
}
69-
70-
// Close method satisfies the io.ReadCloser interface
71-
func (num *numbers) Close() error {
72-
return nil
73-
}

0 commit comments

Comments
 (0)