112 lines
2.8 KiB
Go

// Program jsh exposes a trivial command-shell functionality via JSON-RPC for
// demonstration purposes.
//
// Usage:
// go build github.com/creachadair/jrpc2/cmd/examples/jsh
// ./jsh -port 8080
//
// See also cmd/examples/jcl/jcl.go.
package main
import (
"bytes"
"context"
"flag"
"fmt"
"log"
"net"
"os"
"os/exec"
"github.com/creachadair/jrpc2"
"github.com/creachadair/jrpc2/code"
"github.com/creachadair/jrpc2/handler"
"github.com/creachadair/jrpc2/jctx"
"github.com/creachadair/jrpc2/server"
)
// RunReq is a request to invoke a program.
type RunReq struct {
Args []string `json:"args"` // The command line to execute
Input []byte `json:"input"` // If nonempty, becomes the standard input of the subprocess
Stderr bool `json:"stderr"` // Whether to capture stderr from the subprocess
}
// RunResult is the result of executing a program.
type RunResult struct {
Success bool `json:"success"` // Whether the process succeeded (exit status 0)
Output []byte `json:"output,omitempty"` // The output from the process
}
// Run invokes the specified process and returns the result. It is not an RPC
// error if the process returns a nonzero exit status, unless the process fails
// to start at all.
func Run(ctx context.Context, req *RunReq) (*RunResult, error) {
if len(req.Args) == 0 || req.Args[0] == "" {
return nil, jrpc2.Errorf(code.InvalidParams, "missing command name")
}
if req.Args[0] == "cd" {
if len(req.Args) != 2 {
return nil, jrpc2.Errorf(code.InvalidParams, "wrong arguments for cd")
}
return &RunResult{
Success: os.Chdir(req.Args[1]) == nil,
}, nil
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
cmd := exec.CommandContext(ctx, req.Args[0], req.Args[1:]...)
if len(req.Input) != 0 {
cmd.Stdin = bytes.NewReader(req.Input)
}
run := cmd.Output
if req.Stderr {
run = cmd.CombinedOutput
}
out, err := run()
success := err == nil
if err != nil {
if ex, ok := err.(*exec.ExitError); ok && ex.Success() {
success = true
} else {
return nil, err
}
}
return &RunResult{
Success: success,
Output: out,
}, nil
}
var (
port = flag.Int("port", 0, "Service port")
logging = flag.Bool("log", false, "Enable verbose logging")
lw *log.Logger
)
func main() {
flag.Parse()
if *port <= 0 {
log.Fatal("You must specify a positive --port value")
} else if *logging {
lw = log.New(os.Stdout, "", log.LstdFlags|log.Lshortfile)
}
lst, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
if err != nil {
log.Fatalln("Listen:", err)
}
log.Printf("Listening for connections at %s...", lst.Addr())
server.Loop(lst, server.NewStatic(handler.Map{
"Run": handler.New(Run),
}), &server.LoopOptions{
ServerOptions: &jrpc2.ServerOptions{
AllowV1: true,
Logger: lw,
DecodeContext: jctx.Decode,
},
})
}