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