From a4bdf249dbfb0f35e0a35dad6a38db371b126800 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 28 Jun 2019 15:39:41 -0600 Subject: Caddy 2 gets a CLI! And admin endpoint is now configurable via JSON --- cmd/commands.go | 205 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 cmd/commands.go (limited to 'cmd/commands.go') diff --git a/cmd/commands.go b/cmd/commands.go new file mode 100644 index 0000000..4745af7 --- /dev/null +++ b/cmd/commands.go @@ -0,0 +1,205 @@ +package caddycmd + +import ( + "crypto/rand" + "flag" + "fmt" + "io/ioutil" + "log" + "net" + "os" + "os/exec" + "path/filepath" + + "github.com/caddyserver/caddy" + "github.com/mitchellh/go-ps" +) + +func cmdStart() (int, error) { + startCmd := flag.NewFlagSet("start", flag.ExitOnError) + startCmdConfigFlag := startCmd.String("config", "", "Configuration file") + startCmd.Parse(os.Args[2:]) + + // open a listener to which the child process will connect when + // it is ready to confirm that it has successfully started + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return 1, fmt.Errorf("opening listener for success confirmation: %v", err) + } + defer ln.Close() + + // craft the command with a pingback address and with a + // pipe for its stdin, so we can tell it our confirmation + // code that we expect so that some random port scan at + // the most unfortunate time won't fool us into thinking + // the child succeeded (i.e. the alternative is to just + // wait for any connection on our listener, but better to + // ensure it's the process we're expecting - we can be + // sure by giving it some random bytes and having it echo + // them back to us) + cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String()) + if *startCmdConfigFlag != "" { + cmd.Args = append(cmd.Args, "--config", *startCmdConfigFlag) + } + stdinpipe, err := cmd.StdinPipe() + if err != nil { + return 1, fmt.Errorf("creating stdin pipe: %v", err) + } + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // generate the random bytes we'll send to the child process + expect := make([]byte, 32) + _, err = rand.Read(expect) + if err != nil { + return 1, fmt.Errorf("generating random confirmation bytes: %v", err) + } + + // begin writing the confirmation bytes to the child's + // stdin; use a goroutine since the child hasn't been + // started yet, and writing sychronously would result + // in a deadlock + go func() { + stdinpipe.Write(expect) + stdinpipe.Close() + }() + + // start the process + err = cmd.Start() + if err != nil { + return 1, fmt.Errorf("starting caddy process: %v", err) + } + + // there are two ways we know we're done: either + // the process will connect to our listener, or + // it will exit with an error + success, exit := make(chan struct{}), make(chan error) + + // in one goroutine, we await the success of the child process + go func() { + for { + conn, err := ln.Accept() + if err != nil { + log.Println(err) + break + } + err = handlePingbackConn(conn, expect) + if err == nil { + close(success) + break + } + log.Println(err) + } + }() + + // in another goroutine, we await the failure of the child process + go func() { + err = cmd.Wait() // don't send on this line! Wait blocks, but send starts before it unblocks + exit <- err // sending on separate line ensures select won't trigger until after Wait unblocks + }() + + // when one of the goroutines unblocks, we're done and can exit + select { + case <-success: + fmt.Println("Successfully started Caddy") + case err := <-exit: + return 1, fmt.Errorf("caddy process exited with error: %v", err) + } + + return 0, nil +} + +func cmdRun() (int, error) { + runCmd := flag.NewFlagSet("run", flag.ExitOnError) + runCmdConfigFlag := runCmd.String("config", "", "Configuration file") + runCmdPingbackFlag := runCmd.String("pingback", "", "Echo confirmation bytes to this address on success") + runCmd.Parse(os.Args[2:]) + + // if a config file was specified for bootstrapping + // the server instance, load it now + var config []byte + if *runCmdConfigFlag != "" { + var err error + config, err = ioutil.ReadFile(*runCmdConfigFlag) + if err != nil { + return 1, fmt.Errorf("reading config file: %v", err) + } + } + + // start the admin endpoint along with any initial config + err := caddy.StartAdmin(config) + if err != nil { + return 0, fmt.Errorf("starting caddy administration endpoint: %v", err) + } + defer caddy.StopAdmin() + + // if we are to report to another process the successful start + // of the server, do so now by echoing back contents of stdin + if *runCmdPingbackFlag != "" { + confirmationBytes, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return 1, fmt.Errorf("reading confirmation bytes from stdin: %v", err) + } + conn, err := net.Dial("tcp", *runCmdPingbackFlag) + if err != nil { + return 1, fmt.Errorf("dialing confirmation address: %v", err) + } + defer conn.Close() + _, err = conn.Write(confirmationBytes) + if err != nil { + return 1, fmt.Errorf("writing confirmation bytes to %s: %v", *runCmdPingbackFlag, err) + } + } + + select {} +} + +func cmdStop() (int, error) { + processList, err := ps.Processes() + if err != nil { + return 1, fmt.Errorf("listing processes: %v", err) + } + thisProcName := filepath.Base(os.Args[0]) + var found bool + for _, p := range processList { + // the process we're looking for should have the same name but different PID + if p.Executable() == thisProcName && p.Pid() != os.Getpid() { + found = true + fmt.Printf("pid=%d\n", p.Pid()) + fmt.Printf("Graceful stop...") + if err := gracefullyStopProcess(p.Pid()); err != nil { + return 1, err + } + } + } + if !found { + return 1, fmt.Errorf("Caddy is not running") + } + fmt.Println(" success") + return 0, nil +} + +func cmdVersion() (int, error) { + goModule := getGoBuildModule() + if goModule.Sum != "" { + // a build with a known version will also have a checksum + fmt.Printf("%s (%s)\n", goModule.Version, goModule.Sum) + } else { + fmt.Println(goModule.Version) + } + return 0, nil +} + +func cmdListModules() (int, error) { + for _, m := range caddy.Modules() { + fmt.Println(m) + } + return 0, nil +} + +func cmdEnviron() (int, error) { + for _, v := range os.Environ() { + fmt.Println(v) + } + return 0, nil +} -- cgit v1.2.3