112 lines
2.8 KiB
Go
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,
|
||
|
},
|
||
|
})
|
||
|
}
|