Matthew Holt
6 years ago
8 changed files with 374 additions and 30 deletions
@ -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 |
||||
|
} |
@ -0,0 +1,88 @@ |
|||||
|
package caddycmd |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"flag" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"io/ioutil" |
||||
|
"log" |
||||
|
"net" |
||||
|
"os" |
||||
|
"runtime/debug" |
||||
|
) |
||||
|
|
||||
|
// Main executes the main function of the caddy command.
|
||||
|
func Main() { |
||||
|
if len(os.Args) <= 1 { |
||||
|
fmt.Println(usageString()) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
subcommand, ok := commands[os.Args[1]] |
||||
|
if !ok { |
||||
|
fmt.Printf("%q is not a valid command\n", os.Args[1]) |
||||
|
os.Exit(2) |
||||
|
} |
||||
|
|
||||
|
if exitCode, err := subcommand(); err != nil { |
||||
|
log.Println(err) |
||||
|
os.Exit(exitCode) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// commandFunc is a function that executes
|
||||
|
// a subcommand. It returns an exit code and
|
||||
|
// any associated error.
|
||||
|
type commandFunc func() (int, error) |
||||
|
|
||||
|
var commands = map[string]commandFunc{ |
||||
|
"start": cmdStart, |
||||
|
"stop": cmdStop, |
||||
|
"run": cmdRun, |
||||
|
"version": cmdVersion, |
||||
|
"list-modules": cmdListModules, |
||||
|
"environ": cmdEnviron, |
||||
|
} |
||||
|
|
||||
|
func usageString() string { |
||||
|
buf := new(bytes.Buffer) |
||||
|
buf.WriteString("usage: caddy <command> [<args>]") |
||||
|
flag.CommandLine.SetOutput(buf) |
||||
|
flag.CommandLine.PrintDefaults() |
||||
|
return buf.String() |
||||
|
} |
||||
|
|
||||
|
// handlePingbackConn reads from conn and ensures it matches
|
||||
|
// the bytes in expect, or returns an error if it doesn't.
|
||||
|
func handlePingbackConn(conn net.Conn, expect []byte) error { |
||||
|
defer conn.Close() |
||||
|
confirmationBytes, err := ioutil.ReadAll(io.LimitReader(conn, 32)) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
if !bytes.Equal(confirmationBytes, expect) { |
||||
|
return fmt.Errorf("wrong confirmation: %x", confirmationBytes) |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// getGoBuildModule returns the build info of Caddy
|
||||
|
// from debug.BuildInfo (requires Go modules). If
|
||||
|
// no version information is available, a non-nil
|
||||
|
// value will still be returned, but with an
|
||||
|
// unknown version.
|
||||
|
func getGoBuildModule() *debug.Module { |
||||
|
bi, ok := debug.ReadBuildInfo() |
||||
|
if ok { |
||||
|
// The recommended way to build Caddy involves
|
||||
|
// creating a separate main module, which
|
||||
|
// TODO: track related Go issue: https://github.com/golang/go/issues/29228
|
||||
|
for _, mod := range bi.Deps { |
||||
|
if mod.Path == "github.com/mholt/caddy" { |
||||
|
return mod |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return &debug.Module{Version: "unknown"} |
||||
|
} |
@ -0,0 +1,16 @@ |
|||||
|
// +build !windows
|
||||
|
|
||||
|
package caddycmd |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"syscall" |
||||
|
) |
||||
|
|
||||
|
func gracefullyStopProcess(pid int) error { |
||||
|
err := syscall.Kill(pid, syscall.SIGINT) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("kill: %v", err) |
||||
|
} |
||||
|
return nil |
||||
|
} |
@ -0,0 +1,15 @@ |
|||||
|
package caddycmd |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"os/exec" |
||||
|
"strconv" |
||||
|
) |
||||
|
|
||||
|
func gracefullyStopProcess(pid int) error { |
||||
|
cmd := exec.Command("taskkill", "/pid", strconv.Itoa(pid)) |
||||
|
if err := cmd.Run(); err != nil { |
||||
|
return fmt.Errorf("taskkill: %v", err) |
||||
|
} |
||||
|
return nil |
||||
|
} |
@ -1,26 +0,0 @@ |
|||||
package caddycmd |
|
||||
|
|
||||
import ( |
|
||||
"flag" |
|
||||
"log" |
|
||||
|
|
||||
"github.com/caddyserver/caddy" |
|
||||
) |
|
||||
|
|
||||
// Main executes the main function of the caddy command.
|
|
||||
func Main() { |
|
||||
flag.Parse() |
|
||||
|
|
||||
err := caddy.StartAdmin(*listenAddr) |
|
||||
if err != nil { |
|
||||
log.Fatal(err) |
|
||||
} |
|
||||
defer caddy.StopAdmin() |
|
||||
|
|
||||
log.Println("Caddy 2 admin endpoint listening on", *listenAddr) |
|
||||
|
|
||||
select {} |
|
||||
} |
|
||||
|
|
||||
// TODO: for dev only
|
|
||||
var listenAddr = flag.String("listen", ":1234", "The admin endpoint listener address") |
|
Loading…
Reference in new issue