Explorar o código

Factored out docker/rcli (remote cli protocol), docker/fake (mocking utilities) and docker/future (real utilities which don't yet fit in the core)

Solomon Hykes %!s(int64=12) %!d(string=hai) anos
pai
achega
f3ffba7afe
Modificáronse 7 ficheiros con 343 adicións e 254 borrados
  1. 3 12
      docker/docker.go
  2. 49 242
      dockerd/dockerd.go
  3. 65 0
      fake/fake.go
  4. 39 0
      future/future.go
  5. 41 0
      rcli/http.go
  6. 62 0
      rcli/tcp.go
  7. 84 0
      rcli/types.go

+ 3 - 12
docker/docker.go

@@ -1,14 +1,12 @@
 package main
 
 import (
+	"github.com/dotcloud/docker/rcli"
 	"io"
-	"encoding/json"
 	"log"
 	"os"
-	"net"
-	"fmt"
 	"syscall"
-"unsafe"
+	"unsafe"
 )
 
 
@@ -172,17 +170,10 @@ func main() {
 		}
 		defer Restore(0, oldState)
 	}
-	cmd, err := json.Marshal(os.Args[1:])
+	conn, err := rcli.CallTCP(os.Getenv("DOCKER"), os.Args[1:]...)
 	if err != nil {
 		Fatal(err)
 	}
-	conn, err := net.Dial("tcp", os.Getenv("DOCKER"))
-	if err != nil {
-		Fatal(err)
-	}
-	if _, err := fmt.Fprintln(conn, string(cmd)); err != nil {
-		Fatal(err)
-	}
 	go func() {
 		if _, err := io.Copy(os.Stdout, conn); err != nil {
 			Fatal(err)

+ 49 - 242
dockerd/dockerd.go

@@ -1,64 +1,56 @@
 package main
 
 import (
+	"github.com/dotcloud/docker/rcli"
+	"github.com/dotcloud/docker/fake"
+	"github.com/dotcloud/docker/future"
 	"bufio"
 	"errors"
 	"log"
 	"io"
 	"io/ioutil"
-	"net"
-	"net/url"
-	"net/http"
 	"os/exec"
 	"flag"
-	"reflect"
 	"fmt"
 	"github.com/kr/pty"
-	"path"
 	"strings"
-	"time"
-	"math/rand"
-	"crypto/sha256"
 	"bytes"
 	"text/tabwriter"
 	"sort"
 	"os"
-	"archive/tar"
-	"encoding/json"
+	"time"
 )
 
-func (docker *Docker) CmdHelp(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
-	if len(args) == 0 {
-		fmt.Fprintf(stdout, "Usage: docker COMMAND [arg...]\n\nA self-sufficient runtime for linux containers.\n\nCommands:\n")
-		for _, cmd := range [][]interface{}{
-			{"run", "Run a command in a container"},
-			{"list", "Display a list of containers"},
-			{"get", "Download a tarball and create a container from it"},
-			{"put", "Upload a tarball and create a container from it"},
-			{"rm", "Remove containers"},
-			{"wait", "Wait for the state of a container to change"},
-			{"stop", "Stop a running container"},
-			{"logs", "Fetch the logs of a container"},
-			{"diff", "Inspect changes on a container's filesystem"},
-			{"fork", "Duplicate a container"},
-			{"attach", "Attach to the standard inputs and outputs of a running container"},
-			{"info", "Display system-wide information"},
-			{"web", "Generate a web UI"},
-		} {
-			fmt.Fprintf(stdout, "    %-10.10s%s\n", cmd...)
-		}
-	} else {
-		if method := docker.getMethod(args[0]); method == nil {
-			return errors.New("No such command: " + args[0])
-		} else {
-			method(stdin, stdout, "--help")
-		}
+
+func (docker *Docker) Name() string {
+	return "docker"
+}
+
+func (docker *Docker) Help() string {
+	help := "Usage: docker COMMAND [arg...]\n\nA self-sufficient runtime for linux containers.\n\nCommands:\n"
+	for _, cmd := range [][]interface{}{
+		{"run", "Run a command in a container"},
+		{"list", "Display a list of containers"},
+		{"pull", "Download a tarball and create a container from it"},
+		{"put", "Upload a tarball and create a container from it"},
+		{"rm", "Remove containers"},
+		{"wait", "Wait for the state of a container to change"},
+		{"stop", "Stop a running container"},
+		{"logs", "Fetch the logs of a container"},
+		{"diff", "Inspect changes on a container's filesystem"},
+		{"commit", "Save the state of a container"},
+		{"attach", "Attach to the standard inputs and outputs of a running container"},
+		{"info", "Display system-wide information"},
+		{"web", "Generate a web UI"},
+	} {
+		help += fmt.Sprintf("    %-10.10s%s\n", cmd...)
 	}
-	return nil
+	return help
 }
 
+
 func (docker *Docker) CmdList(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
-	flags := Subcmd(stdout, "list", "[OPTIONS] [NAME]", "List containers")
+	flags := rcli.Subcmd(stdout, "list", "[OPTIONS] [NAME]", "List containers")
 	limit := flags.Int("l", 0, "Only show the N most recent versions of each name")
 	quiet := flags.Bool("q", false, "only show numeric IDs")
 	flags.Parse(args)
@@ -91,7 +83,7 @@ func (docker *Docker) CmdList(stdin io.ReadCloser, stdout io.Writer, args ...str
 				for idx, field := range []string{
 					/* NAME */	container.Name,
 					/* ID */	container.Id,
-					/* CREATED */	humanDuration(time.Now().Sub(container.Created)) + " ago",
+					/* CREATED */	future.HumanDuration(time.Now().Sub(container.Created)) + " ago",
 					/* SOURCE */	container.Source,
 					/* SIZE */	fmt.Sprintf("%.1fM", float32(container.Size) / 1024 / 1024),
 					/* CHANGES */	fmt.Sprintf("%.1fM", float32(container.BytesChanged) / 1024 / 1024),
@@ -130,7 +122,7 @@ func (docker *Docker) findContainer(name string) (*Container, bool) {
 
 
 func (docker *Docker) CmdRm(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
-	flags := Subcmd(stdout, "rm", "[OPTIONS] CONTAINER", "Remove a container")
+	flags := rcli.Subcmd(stdout, "rm", "[OPTIONS] CONTAINER", "Remove a container")
 	if err := flags.Parse(args); err != nil {
 		return nil
 	}
@@ -142,7 +134,7 @@ func (docker *Docker) CmdRm(stdin io.ReadCloser, stdout io.Writer, args ...strin
 	return nil
 }
 
-func (docker *Docker) CmdGet(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
+func (docker *Docker) CmdPull(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
 	if len(args) < 1 {
 		return errors.New("Not enough arguments")
 	}
@@ -162,8 +154,8 @@ func (docker *Docker) CmdPut(stdin io.ReadCloser, stdout io.Writer, args ...stri
 	return nil
 }
 
-func (docker *Docker) CmdFork(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
-	flags := Subcmd(stdout,
+func (docker *Docker) CmdCommit(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
+	flags := rcli.Subcmd(stdout,
 		"fork", "[OPTIONS] CONTAINER [DEST]",
 		"Duplicate a container")
 	// FIXME "-r" to reset changes in the new container
@@ -187,7 +179,7 @@ func (docker *Docker) CmdFork(stdin io.ReadCloser, stdout io.Writer, args ...str
 }
 
 func (docker *Docker) CmdTar(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
-	flags := Subcmd(stdout,
+	flags := rcli.Subcmd(stdout,
 		"tar", "CONTAINER",
 		"Stream the contents of a container as a tar archive")
 	if err := flags.Parse(args); err != nil {
@@ -196,13 +188,13 @@ func (docker *Docker) CmdTar(stdin io.ReadCloser, stdout io.Writer, args ...stri
 	name := flags.Arg(0)
 	if _, exists := docker.findContainer(name); exists {
 		// Stream the entire contents of the container (basically a volatile snapshot)
-		return WriteFakeTar(stdout)
+		return fake.WriteFakeTar(stdout)
 	}
 	return errors.New("No such container: " + name)
 }
 
 func (docker *Docker) CmdDiff(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
-	flags := Subcmd(stdout,
+	flags := rcli.Subcmd(stdout,
 		"diff", "CONTAINER [OPTIONS]",
 		"Inspect changes on a container's filesystem")
 	fl_diff := flags.Bool("d", true, "Show changes in diff format")
@@ -238,7 +230,7 @@ index 2dae694..e43caca 100644
 --- a/dockerd/dockerd.go
 +++ b/dockerd/dockerd.go
 @@ -158,6 +158,7 @@ func (docker *Docker) CmdDiff(stdin io.ReadCloser, stdout io.Writer, args ...str
-        flags := Subcmd(stdout,
+        flags := rcli.Subcmd(stdout,
                 "diff", "CONTAINER [OPTIONS]",
                 "Inspect changes on a container's filesystem")
 +       fl_diff := flags.Bool("d", true, "Show changes in diff format")
@@ -290,12 +282,11 @@ func (c *ByDate) Del(id string) {
 
 
 func (docker *Docker) addContainer(name string, source string, size uint) *Container {
-	// Generate a fake random size
 	if size == 0 {
-		size = uint(rand.Int31n(142 * 1024 * 1024))
+		size = fake.RandomContainerSize()
 	}
 	c := &Container{
-		Id:		randomId(),
+		Id:		fake.RandomId(),
 		Name:		name,
 		Created:	time.Now(),
 		Source:		source,
@@ -330,7 +321,7 @@ func (docker *Docker) rm(id string) (*Container, error) {
 
 
 func (docker *Docker) CmdLogs(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
-	flags := Subcmd(stdout, "logs", "[OPTIONS] CONTAINER", "Fetch the logs of a container")
+	flags := rcli.Subcmd(stdout, "logs", "[OPTIONS] CONTAINER", "Fetch the logs of a container")
 	if err := flags.Parse(args); err != nil {
 		return nil
 	}
@@ -349,7 +340,7 @@ func (docker *Docker) CmdLogs(stdin io.ReadCloser, stdout io.Writer, args ...str
 }
 
 func (docker *Docker) CmdRun(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
-	flags := Subcmd(stdout, "run", "[OPTIONS] CONTAINER COMMAND [ARG...]", "Run a command in a container")
+	flags := rcli.Subcmd(stdout, "run", "[OPTIONS] CONTAINER COMMAND [ARG...]", "Run a command in a container")
 	fl_attach := flags.Bool("a", false, "Attach stdin and stdout")
 	if err := flags.Parse(args); err != nil {
 		return nil
@@ -396,60 +387,21 @@ func startCommand(cmd *exec.Cmd, interactive bool) (io.WriteCloser, io.ReadClose
 	return stdin, stdout, nil
 }
 
-func (docker *Docker) ListenAndServeTCP(addr string) error {
-	listener, err := net.Listen("tcp", addr)
-	if err != nil {
-		return err
-	}
-	defer listener.Close()
-	for {
-		if conn, err := listener.Accept(); err != nil {
-			return err
-		} else {
-			go func() {
-				if err := docker.serve(conn); err != nil {
-					log.Printf("Error: " + err.Error() + "\n")
-					fmt.Fprintf(conn, "Error: " + err.Error() + "\n")
-				}
-				conn.Close()
-			}()
-		}
-	}
-	return nil
-}
-
-func (docker *Docker) ListenAndServeHTTP(addr string) error {
-	return http.ListenAndServe(addr, docker)
-}
-
 
 func main() {
-	rand.Seed(time.Now().UTC().UnixNano())
+	fake.Seed()
 	flag.Parse()
 	docker := New()
 	go func() {
-		if err := docker.ListenAndServeHTTP(":8080"); err != nil {
+		if err := rcli.ListenAndServeHTTP(":8080", docker); err != nil {
 			log.Fatal(err)
 		}
 	}()
-	if err := docker.ListenAndServeTCP(":4242"); err != nil {
+	if err := rcli.ListenAndServeTCP(":4242", docker); err != nil {
 		log.Fatal(err)
 	}
 }
 
-func (docker *Docker) serve(conn net.Conn) error {
-	r := bufio.NewReader(conn)
-	var args []string
-	if line, err := r.ReadString('\n'); err != nil {
-		return err
-	} else if err := json.Unmarshal([]byte(line), &args); err != nil {
-		return err
-	} else {
-		return docker.Call(ioutil.NopCloser(r), conn, args...)
-	}
-	return nil
-}
-
 func New() *Docker {
 	return &Docker{
 		containersByName: make(map[string]*ByDate),
@@ -457,43 +409,6 @@ func New() *Docker {
 	}
 }
 
-type AutoFlush struct {
-	http.ResponseWriter
-}
-
-func (w *AutoFlush) Write(data []byte) (int, error) {
-	ret, err := w.ResponseWriter.Write(data)
-	if flusher, ok := w.ResponseWriter.(http.Flusher); ok {
-		flusher.Flush()
-	}
-	return ret, err
-}
-
-func (docker *Docker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	cmd, args := URLToCall(r.URL)
-	if err := docker.Call(r.Body, &AutoFlush{w}, append([]string{cmd}, args...)...); err != nil {
-		fmt.Fprintf(w, "Error: " + err.Error() + "\n")
-	}
-}
-
-func (docker *Docker) Call(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
-	flags := flag.NewFlagSet("docker", flag.ContinueOnError)
-	flags.SetOutput(stdout)
-	flags.Usage = func() { docker.CmdHelp(stdin, stdout) }
-	if err := flags.Parse(args); err != nil {
-		return err
-	}
-	cmd := flags.Arg(0)
-	log.Printf("%s\n", strings.Join(append(append([]string{"docker"}, cmd), args[1:]...), " "))
-	if cmd == "" {
-		cmd = "help"
-	}
-	method := docker.getMethod(cmd)
-	if method != nil {
-		return method(stdin, stdout, args[1:]...)
-	}
-	return errors.New("No such command: " + cmd)
-}
 
 func (docker *Docker) CmdMirror(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
 	_, err := io.Copy(stdout, stdin)
@@ -517,7 +432,7 @@ func (docker *Docker) CmdDebug(stdin io.ReadCloser, stdout io.Writer, args ...st
 }
 
 func (docker *Docker) CmdWeb(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
-	flags := Subcmd(stdout, "web", "[OPTIONS]", "A web UI for docker")
+	flags := rcli.Subcmd(stdout, "web", "[OPTIONS]", "A web UI for docker")
 	showurl := flags.Bool("u", false, "Return the URL of the web UI")
 	if err := flags.Parse(args); err != nil {
 		return nil
@@ -534,25 +449,6 @@ func (docker *Docker) CmdWeb(stdin io.ReadCloser, stdout io.Writer, args ...stri
 	return nil
 }
 
-func (docker *Docker) getMethod(name string) Cmd {
-	methodName := "Cmd"+strings.ToUpper(name[:1])+strings.ToLower(name[1:])
-	method, exists := reflect.TypeOf(docker).MethodByName(methodName)
-	if !exists {
-		return nil
-	}
-	return func(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
-		ret := method.Func.CallSlice([]reflect.Value{
-			reflect.ValueOf(docker),
-			reflect.ValueOf(stdin),
-			reflect.ValueOf(stdout),
-			reflect.ValueOf(args),
-		})[0].Interface()
-		if ret == nil {
-			return nil
-		}
-		return ret.(error)
-	}
-}
 
 func Go(f func() error) chan error {
 	ch := make(chan error)
@@ -596,8 +492,8 @@ func (c *Container) Run(command string, args []string, stdin io.ReadCloser, stdo
 	cmd := exec.Command(c.Cmd, c.Args...)
 	cmd_stdin, cmd_stdout, err := startCommand(cmd, true)
 	// ADD FAKE RANDOM CHANGES
-	c.FilesChanged = uint(rand.Int31n(42))
-	c.BytesChanged = uint(rand.Int31n(24 * 1024 * 1024))
+	c.FilesChanged = fake.RandomFilesChanged()
+	c.BytesChanged = fake.RandomBytesChanged()
 	if err != nil {
 		return err
 	}
@@ -643,92 +539,3 @@ func (c *Container) CmdString() string {
 	return strings.Join(append([]string{c.Cmd}, c.Args...), " ")
 }
 
-type Cmd func(io.ReadCloser, io.Writer, ...string) error
-type CmdMethod func(*Docker, io.ReadCloser, io.Writer, ...string) error
-
-// Use this key to encode an RPC call into an URL,
-// eg. domain.tld/path/to/method?q=get_user&q=gordon
-const ARG_URL_KEY = "q"
-
-func URLToCall(u *url.URL) (method string, args []string) {
-	return path.Base(u.Path), u.Query()[ARG_URL_KEY]
-}
-
-
-func randomBytes() io.Reader {
-	return bytes.NewBuffer([]byte(fmt.Sprintf("%x", rand.Int())))
-}
-
-func ComputeId(content io.Reader) (string, error) {
-	h := sha256.New()
-	if _, err := io.Copy(h, content); err != nil {
-		return "", err
-	}
-	return fmt.Sprintf("%x", h.Sum(nil)[:8]), nil
-}
-
-func randomId() string {
-	id, _ := ComputeId(randomBytes()) // can't fail
-	return id
-}
-
-
-func humanDuration(d time.Duration) string {
-	if seconds := int(d.Seconds()); seconds < 1 {
-		return "Less than a second"
-	} else if seconds < 60 {
-		return fmt.Sprintf("%d seconds", seconds)
-	} else if minutes := int(d.Minutes()); minutes == 1 {
-		return "About a minute"
-	} else if minutes < 60 {
-		return fmt.Sprintf("%d minutes", minutes)
-	} else if hours := int(d.Hours()); hours  == 1{
-		return "About an hour"
-	} else if hours < 48 {
-		return fmt.Sprintf("%d hours", hours)
-	} else if hours < 24 * 7 * 2 {
-		return fmt.Sprintf("%d days", hours / 24)
-	} else if hours < 24 * 30 * 3 {
-		return fmt.Sprintf("%d weeks", hours / 24 / 7)
-	} else if hours < 24 * 365 * 2 {
-		return fmt.Sprintf("%d months", hours / 24 / 30)
-	}
-	return fmt.Sprintf("%d years", d.Hours() / 24 / 365)
-}
-
-func Subcmd(output io.Writer, name, signature, description string) *flag.FlagSet {
-	flags := flag.NewFlagSet(name, flag.ContinueOnError)
-	flags.SetOutput(output)
-	flags.Usage = func() {
-		fmt.Fprintf(output, "\nUsage: docker %s %s\n\n%s\n\n", name, signature, description)
-		flags.PrintDefaults()
-	}
-	return flags
-}
-
-
-func WriteFakeTar(dst io.Writer) error {
-	if data, err := FakeTar(); err != nil {
-		return err
-	} else if _, err := io.Copy(dst, data); err != nil {
-		return err
-	}
-	return nil
-}
-
-func FakeTar() (io.Reader, error) {
-	content := []byte("Hello world!\n")
-	buf := new(bytes.Buffer)
-	tw := tar.NewWriter(buf)
-	for _, name := range []string {"/etc/postgres/postgres.conf", "/etc/passwd", "/var/log/postgres", "/var/log/postgres/postgres.conf"} {
-		hdr := new(tar.Header)
-		hdr.Size = int64(len(content))
-		hdr.Name = name
-		if err := tw.WriteHeader(hdr); err != nil {
-			return nil, err
-		}
-		tw.Write([]byte(content))
-	}
-	tw.Close()
-	return buf, nil
-}

+ 65 - 0
fake/fake.go

@@ -0,0 +1,65 @@
+package fake
+
+import (
+	"github.com/dotcloud/docker/future"
+	"bytes"
+	"math/rand"
+	"time"
+	"io"
+	"archive/tar"
+	"fmt"
+)
+
+func Seed() {
+	rand.Seed(time.Now().UTC().UnixNano())
+}
+
+func randomBytes() io.Reader {
+	return bytes.NewBuffer([]byte(fmt.Sprintf("%x", rand.Int())))
+}
+
+func FakeTar() (io.Reader, error) {
+	content := []byte("Hello world!\n")
+	buf := new(bytes.Buffer)
+	tw := tar.NewWriter(buf)
+	for _, name := range []string {"/etc/postgres/postgres.conf", "/etc/passwd", "/var/log/postgres", "/var/log/postgres/postgres.conf"} {
+		hdr := new(tar.Header)
+		hdr.Size = int64(len(content))
+		hdr.Name = name
+		if err := tw.WriteHeader(hdr); err != nil {
+			return nil, err
+		}
+		tw.Write([]byte(content))
+	}
+	tw.Close()
+	return buf, nil
+}
+
+
+func WriteFakeTar(dst io.Writer) error {
+	if data, err := FakeTar(); err != nil {
+		return err
+	} else if _, err := io.Copy(dst, data); err != nil {
+		return err
+	}
+	return nil
+}
+
+
+func RandomId() string {
+	id, _ := future.ComputeId(randomBytes()) // can't fail
+	return id
+}
+
+
+func RandomBytesChanged() uint {
+	return uint(rand.Int31n(24 * 1024 * 1024))
+}
+
+func RandomFilesChanged() uint {
+	return uint(rand.Int31n(42))
+}
+
+func RandomContainerSize() uint {
+	return uint(rand.Int31n(142 * 1024 * 1024))
+}

+ 39 - 0
future/future.go

@@ -0,0 +1,39 @@
+package future
+
+import (
+	"crypto/sha256"
+	"io"
+	"fmt"
+	"time"
+)
+
+func ComputeId(content io.Reader) (string, error) {
+	h := sha256.New()
+	if _, err := io.Copy(h, content); err != nil {
+		return "", err
+	}
+	return fmt.Sprintf("%x", h.Sum(nil)[:8]), nil
+}
+
+func HumanDuration(d time.Duration) string {
+	if seconds := int(d.Seconds()); seconds < 1 {
+		return "Less than a second"
+	} else if seconds < 60 {
+		return fmt.Sprintf("%d seconds", seconds)
+	} else if minutes := int(d.Minutes()); minutes == 1 {
+		return "About a minute"
+	} else if minutes < 60 {
+		return fmt.Sprintf("%d minutes", minutes)
+	} else if hours := int(d.Hours()); hours  == 1{
+		return "About an hour"
+	} else if hours < 48 {
+		return fmt.Sprintf("%d hours", hours)
+	} else if hours < 24 * 7 * 2 {
+		return fmt.Sprintf("%d days", hours / 24)
+	} else if hours < 24 * 30 * 3 {
+		return fmt.Sprintf("%d weeks", hours / 24 / 7)
+	} else if hours < 24 * 365 * 2 {
+		return fmt.Sprintf("%d months", hours / 24 / 30)
+	}
+	return fmt.Sprintf("%d years", d.Hours() / 24 / 365)
+}

+ 41 - 0
rcli/http.go

@@ -0,0 +1,41 @@
+package rcli
+
+import (
+	"net/http"
+	"net/url"
+	"path"
+	"fmt"
+)
+
+
+// Use this key to encode an RPC call into an URL,
+// eg. domain.tld/path/to/method?q=get_user&q=gordon
+const ARG_URL_KEY = "q"
+
+func URLToCall(u *url.URL) (method string, args []string) {
+	return path.Base(u.Path), u.Query()[ARG_URL_KEY]
+}
+
+
+func ListenAndServeHTTP(addr string, service Service) error {
+	return http.ListenAndServe(addr, http.HandlerFunc(
+		func (w http.ResponseWriter, r *http.Request) {
+			cmd, args := URLToCall(r.URL)
+			if err := call(service, r.Body, &AutoFlush{w}, append([]string{cmd}, args...)...); err != nil {
+				fmt.Fprintf(w, "Error: " + err.Error() + "\n")
+			}
+		}))
+}
+
+
+type AutoFlush struct {
+	http.ResponseWriter
+}
+
+func (w *AutoFlush) Write(data []byte) (int, error) {
+	ret, err := w.ResponseWriter.Write(data)
+	if flusher, ok := w.ResponseWriter.(http.Flusher); ok {
+		flusher.Flush()
+	}
+	return ret, err
+}

+ 62 - 0
rcli/tcp.go

@@ -0,0 +1,62 @@
+package rcli
+
+import (
+	"io"
+	"io/ioutil"
+	"net"
+	"log"
+	"fmt"
+	"encoding/json"
+	"bufio"
+)
+
+func CallTCP(addr string, args ...string) (io.ReadWriteCloser, error) {
+	cmd, err := json.Marshal(args)
+	if err != nil {
+		return nil, err
+	}
+	conn, err := net.Dial("tcp", addr)
+	if err != nil {
+		return nil, err
+	}
+	if _, err := fmt.Fprintln(conn, string(cmd)); err != nil {
+		return nil, err
+	}
+	return conn, nil
+}
+
+func ListenAndServeTCP(addr string, service Service) error {
+	listener, err := net.Listen("tcp", addr)
+	if err != nil {
+		return err
+	}
+	defer listener.Close()
+	for {
+		if conn, err := listener.Accept(); err != nil {
+			return err
+		} else {
+			go func() {
+				if err := Serve(conn, service); err != nil {
+					log.Printf("Error: " + err.Error() + "\n")
+					fmt.Fprintf(conn, "Error: " + err.Error() + "\n")
+				}
+				conn.Close()
+			}()
+		}
+	}
+	return nil
+}
+
+func Serve(conn io.ReadWriter, service Service) error {
+	r := bufio.NewReader(conn)
+	var args []string
+	if line, err := r.ReadString('\n'); err != nil {
+		return err
+	} else if err := json.Unmarshal([]byte(line), &args); err != nil {
+		return err
+	} else {
+		return call(service, ioutil.NopCloser(r), conn, args...)
+	}
+	return nil
+}
+

+ 84 - 0
rcli/types.go

@@ -0,0 +1,84 @@
+package rcli
+
+import (
+	"fmt"
+	"io"
+	"reflect"
+	"flag"
+	"log"
+	"strings"
+	"errors"
+)
+
+type Service interface {
+	Name() string
+	Help() string
+}
+
+type Cmd func(io.ReadCloser, io.Writer, ...string) error
+type CmdMethod func(Service, io.ReadCloser, io.Writer, ...string) error
+
+
+func call(service Service, stdin io.ReadCloser, stdout io.Writer, args ...string) error {
+	flags := flag.NewFlagSet("main", flag.ContinueOnError)
+	flags.SetOutput(stdout)
+	flags.Usage = func() { stdout.Write([]byte(service.Help())) }
+	if err := flags.Parse(args); err != nil {
+		return err
+	}
+	cmd := flags.Arg(0)
+	log.Printf("%s\n", strings.Join(append(append([]string{service.Name()}, cmd), flags.Args()[1:]...), " "))
+	if cmd == "" {
+		cmd = "help"
+	}
+	method := getMethod(service, cmd)
+	if method != nil {
+		return method(stdin, stdout, args[1:]...)
+	}
+	return errors.New("No such command: " + cmd)
+}
+
+func getMethod(service Service, name string) Cmd {
+	if name == "help" {
+		return func(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
+			if len(args) == 0 {
+				stdout.Write([]byte(service.Help()))
+			} else {
+				if method := getMethod(service, args[0]); method == nil {
+					return errors.New("No such command: " + args[0])
+				} else {
+					method(stdin, stdout, "--help")
+				}
+			}
+			return nil
+		}
+	}
+	methodName := "Cmd"+strings.ToUpper(name[:1])+strings.ToLower(name[1:])
+	method, exists := reflect.TypeOf(service).MethodByName(methodName)
+	if !exists {
+		return nil
+	}
+	return func(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
+		ret := method.Func.CallSlice([]reflect.Value{
+			reflect.ValueOf(service),
+			reflect.ValueOf(stdin),
+			reflect.ValueOf(stdout),
+			reflect.ValueOf(args),
+		})[0].Interface()
+		if ret == nil {
+			return nil
+		}
+		return ret.(error)
+	}
+}
+
+func Subcmd(output io.Writer, name, signature, description string) *flag.FlagSet {
+	flags := flag.NewFlagSet(name, flag.ContinueOnError)
+	flags.SetOutput(output)
+	flags.Usage = func() {
+		fmt.Fprintf(output, "\nUsage: docker %s %s\n\n%s\n\n", name, signature, description)
+		flags.PrintDefaults()
+	}
+	return flags
+}
+