| package rcli |
| |
| // rcli (Remote Command-Line Interface) is a simple protocol for... |
| // serving command-line interfaces remotely. |
| // |
| // rcli can be used over any transport capable of a) sending binary streams in |
| // both directions, and b) capable of half-closing a connection. TCP and Unix sockets |
| // are the usual suspects. |
| |
| import ( |
| "flag" |
| "fmt" |
| "github.com/dotcloud/docker/term" |
| "io" |
| "log" |
| "net" |
| "os" |
| "reflect" |
| "strings" |
| ) |
| |
| type DockerConnOptions struct { |
| RawTerminal bool |
| } |
| |
| type DockerConn interface { |
| io.ReadWriteCloser |
| CloseWrite() error |
| CloseRead() error |
| GetOptions() *DockerConnOptions |
| SetOptionRawTerminal() |
| Flush() error |
| } |
| |
| type DockerLocalConn struct { |
| writer io.WriteCloser |
| savedState *term.State |
| } |
| |
| func NewDockerLocalConn(w io.WriteCloser) *DockerLocalConn { |
| return &DockerLocalConn{ |
| writer: w, |
| } |
| } |
| |
| func (c *DockerLocalConn) Read(b []byte) (int, error) { |
| return 0, fmt.Errorf("DockerLocalConn does not implement Read()") |
| } |
| |
| func (c *DockerLocalConn) Write(b []byte) (int, error) { return c.writer.Write(b) } |
| |
| func (c *DockerLocalConn) Close() error { |
| if c.savedState != nil { |
| RestoreTerminal(c.savedState) |
| c.savedState = nil |
| } |
| return c.writer.Close() |
| } |
| |
| func (c *DockerLocalConn) Flush() error { return nil } |
| |
| func (c *DockerLocalConn) CloseWrite() error { return nil } |
| |
| func (c *DockerLocalConn) CloseRead() error { return nil } |
| |
| func (c *DockerLocalConn) GetOptions() *DockerConnOptions { return nil } |
| |
| func (c *DockerLocalConn) SetOptionRawTerminal() { |
| if state, err := SetRawTerminal(); err != nil { |
| if os.Getenv("DEBUG") != "" { |
| log.Printf("Can't set the terminal in raw mode: %s", err) |
| } |
| } else { |
| c.savedState = state |
| } |
| } |
| |
| var UnknownDockerProto = fmt.Errorf("Only TCP is actually supported by Docker at the moment") |
| |
| func dialDocker(proto string, addr string) (DockerConn, error) { |
| conn, err := net.Dial(proto, addr) |
| if err != nil { |
| return nil, err |
| } |
| switch i := conn.(type) { |
| case *net.TCPConn: |
| return NewDockerTCPConn(i, true), nil |
| } |
| return nil, UnknownDockerProto |
| } |
| |
| func newDockerFromConn(conn net.Conn, client bool) (DockerConn, error) { |
| switch i := conn.(type) { |
| case *net.TCPConn: |
| return NewDockerTCPConn(i, client), nil |
| } |
| return nil, UnknownDockerProto |
| } |
| |
| func newDockerServerConn(conn net.Conn) (DockerConn, error) { |
| return newDockerFromConn(conn, false) |
| } |
| |
| 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 |
| |
| // FIXME: For reverse compatibility |
| func call(service Service, stdin io.ReadCloser, stdout DockerConn, args ...string) error { |
| return LocalCall(service, stdin, stdout, args...) |
| } |
| |
| func LocalCall(service Service, stdin io.ReadCloser, stdout DockerConn, args ...string) error { |
| if len(args) == 0 { |
| args = []string{"help"} |
| } |
| 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, flags.Args()[1:]...) |
| } |
| return fmt.Errorf("No such command: %s", 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 fmt.Errorf("No such command: %s", 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 |
| } |