caddy/cmd/commands.go

206 lines
5.5 KiB
Go

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
}