|
|
@@ -207,10 +207,7 @@ func (s *Server) accept(ln net.Listener, sem chan struct{}) {
|
|
|
func (s *Server) handle(conn net.Conn) {
|
|
|
defer conn.Close()
|
|
|
|
|
|
- var (
|
|
|
- args []string
|
|
|
- replied bool
|
|
|
- )
|
|
|
+ var args []string
|
|
|
defer func() {
|
|
|
if r := recover(); r != nil {
|
|
|
s.l.Error("Recovered from panic serving a local control command",
|
|
|
@@ -218,11 +215,7 @@ func (s *Server) handle(conn net.Conn) {
|
|
|
"args", args,
|
|
|
"stack", string(debug.Stack()),
|
|
|
)
|
|
|
- // A panic in a command's AfterReply arrives here after the answer has gone
|
|
|
- // out; the client has its reply, so it is not sent a second one.
|
|
|
- if !replied {
|
|
|
- s.reply(conn, &Response{Version: ProtocolVersion, Error: "command failed"})
|
|
|
- }
|
|
|
+ s.reply(conn, &Response{Version: ProtocolVersion, Error: "command failed"})
|
|
|
}
|
|
|
}()
|
|
|
|
|
|
@@ -259,25 +252,17 @@ func (s *Server) handle(conn net.Conn) {
|
|
|
_ = conn.SetReadDeadline(time.Time{})
|
|
|
_ = conn.SetWriteDeadline(time.Now().Add(writeTimeout))
|
|
|
|
|
|
- res, after := s.serve(req)
|
|
|
- s.reply(conn, res)
|
|
|
- replied = true
|
|
|
-
|
|
|
- // A command that would cut off its own reply acts only now, once the answer is out.
|
|
|
- if after != nil {
|
|
|
- after()
|
|
|
- }
|
|
|
+ s.reply(conn, s.serve(req))
|
|
|
}
|
|
|
|
|
|
-// serve runs one request and describes the outcome. after, when not nil, is the
|
|
|
-// command's AfterReply, which the caller runs once the response has been written.
|
|
|
+// serve runs one request and describes the outcome.
|
|
|
//
|
|
|
// Nothing bounds a command's run; a context would have to reach every Run. What is
|
|
|
// bounded is the silence: a command still running at slowAfter is named in the log, with
|
|
|
// the slot it is holding, so a socket that has gone "busy" can be traced to the commands
|
|
|
// that wedged it. The timer is stopped however the command ends, a panic included, so
|
|
|
// the line is only ever written about a command that is still running.
|
|
|
-func (s *Server) serve(req *Request) (res *Response, after func()) {
|
|
|
+func (s *Server) serve(req *Request) *Response {
|
|
|
slow := time.AfterFunc(s.slowAfter, func() {
|
|
|
s.l.Warn("Local control command is still running and holding a connection slot",
|
|
|
"command", req.Command,
|
|
|
@@ -288,66 +273,79 @@ func (s *Server) serve(req *Request) (res *Response, after func()) {
|
|
|
})
|
|
|
defer slow.Stop()
|
|
|
|
|
|
- res = &Response{Version: ProtocolVersion}
|
|
|
+ res := &Response{Version: ProtocolVersion}
|
|
|
reg := s.registry()
|
|
|
|
|
|
if req.Describe {
|
|
|
schema, err := json.Marshal(reg.Schema())
|
|
|
if err != nil {
|
|
|
res.Error = err.Error()
|
|
|
- return res, nil
|
|
|
+ return res
|
|
|
}
|
|
|
|
|
|
res.Result = schema
|
|
|
- return res, nil
|
|
|
+ return res
|
|
|
}
|
|
|
|
|
|
if req.Command == "" {
|
|
|
res.Error = "no command given"
|
|
|
- return res, nil
|
|
|
+ return res
|
|
|
}
|
|
|
|
|
|
params, err := stringParams(req.Params)
|
|
|
if err != nil {
|
|
|
res.Error = err.Error()
|
|
|
- return res, nil
|
|
|
+ return res
|
|
|
}
|
|
|
|
|
|
format, err := parseFormat(req.Format)
|
|
|
if err != nil {
|
|
|
res.Error = err.Error()
|
|
|
- return res, nil
|
|
|
+ return res
|
|
|
}
|
|
|
|
|
|
cmd, err := reg.Resolve(commands.Name(req.Command))
|
|
|
if err != nil {
|
|
|
s.l.Debug("Local control command refused", "command", req.Command, "error", err)
|
|
|
res.Error = err.Error()
|
|
|
- return res, nil
|
|
|
+ return res
|
|
|
}
|
|
|
|
|
|
r, err := commands.Invoke(commands.Request{Command: cmd, Params: params, Args: req.Args})
|
|
|
if err != nil {
|
|
|
s.l.Debug("Local control command failed", "command", req.Command, "error", err)
|
|
|
res.Error = err.Error()
|
|
|
- return res, nil
|
|
|
+ return res
|
|
|
}
|
|
|
|
|
|
- after = cmd.AfterReply
|
|
|
+ // A command that acts does so now, before the one reply this surface gives, and the
|
|
|
+ // reply carries both halves: the line Run promised and the outcome Then produced.
|
|
|
+ // In JSON only the outcome travels, since the promise is for a person to read.
|
|
|
+ var preface []byte
|
|
|
+ if cmd.Then != nil {
|
|
|
+ if format == commands.FormatText && !r.Empty() {
|
|
|
+ buf := &bytes.Buffer{}
|
|
|
+ if err := commands.Render(cmd, r, format, buf); err == nil {
|
|
|
+ preface = buf.Bytes()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ r = cmd.Then()
|
|
|
+ }
|
|
|
|
|
|
// A command with nothing to say gets an empty, successful reply.
|
|
|
if r.Empty() {
|
|
|
- return res, after
|
|
|
+ res.Text = preface
|
|
|
+ return res
|
|
|
}
|
|
|
|
|
|
- return render(res, cmd, r, format), after
|
|
|
+ return render(res, cmd, r, format, preface)
|
|
|
}
|
|
|
|
|
|
// render turns a command's Result into the response, in the format the caller asked for.
|
|
|
// Both formats come from commands.Render, which is where ssh and the control socket
|
|
|
// meet: the JSON a program reads here is byte for byte what -json prints at an ssh
|
|
|
// prompt, and the text is the command's own rendering, which only the daemon can run.
|
|
|
-func render(res *Response, cmd *commands.Command, r commands.Result, format commands.Format) *Response {
|
|
|
+func render(res *Response, cmd *commands.Command, r commands.Result, format commands.Format, preface []byte) *Response {
|
|
|
buf := &bytes.Buffer{}
|
|
|
if err := commands.Render(cmd, r, format, buf); err != nil {
|
|
|
res.Error = err.Error()
|
|
|
@@ -378,7 +376,7 @@ func render(res *Response, cmd *commands.Command, r commands.Result, format comm
|
|
|
}
|
|
|
|
|
|
// The bytes the command wrote, as they are; see Response.Text.
|
|
|
- res.Text = buf.Bytes()
|
|
|
+ res.Text = append(preface, buf.Bytes()...)
|
|
|
return res
|
|
|
}
|
|
|
|