├── .github └── workflows │ ├── ci.yml │ └── lint.yml ├── .gitignore ├── .golangci.yml ├── Changes ├── LICENSE ├── README.md ├── cmd └── start_server │ └── start_server.go ├── env.go ├── env_test.go ├── go.mod ├── listener ├── listener.go └── listener_test.go ├── releng ├── Dockerfile ├── build-image.sh ├── build-server_starter.sh ├── build.sh ├── release-server_starter.sh └── release.sh ├── starter.go ├── starter_any.go ├── starter_test.go └── starter_windows.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | go: [ '1.15', '1.14' ] 10 | name: Go ${{ matrix.go }} test 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v2 14 | - name: Install Go stable version 15 | if: matrix.go != 'tip' 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: ${{ matrix.go }} 19 | - name: Install Go tip 20 | if: matrix.go == 'tip' 21 | run: | 22 | git clone --depth=1 https://go.googlesource.com/go $HOME/gotip 23 | cd $HOME/gotip/src 24 | ./make.bash 25 | echo "::set-env name=GOROOT::$HOME/gotip" 26 | echo "::add-path::$HOME/gotip/bin" 27 | echo "::add-path::$(go env GOPATH)/bin" 28 | - name: Test 29 | run: go test -v -race ./... 30 | - name: Upload code coverage to codecov 31 | if: matrix.go == '1.15' 32 | uses: codecov/codecov-action@v1 33 | with: 34 | file: ./coverage.out 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: [push, pull_request] 3 | jobs: 4 | golangci: 5 | name: lint 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: golangci/golangci-lint-action@v2 10 | with: 11 | version: v1.34.1 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | 3 | linters-settings: 4 | govet: 5 | enable-all: true 6 | disable: 7 | - shadow 8 | 9 | linters: 10 | enable-all: true 11 | disable: 12 | - dupl 13 | - exhaustive 14 | - exhaustivestruct 15 | - errorlint 16 | - funlen 17 | - gci 18 | - gochecknoglobals 19 | - gochecknoinits 20 | - gocognit 21 | - gocritic 22 | - gocyclo 23 | - godot 24 | - godox 25 | - goerr113 26 | - gofumpt 27 | - gomnd 28 | - gosec 29 | - lll 30 | - makezero 31 | - nakedret 32 | - nestif 33 | - nlreturn 34 | - paralleltest 35 | - testpackage 36 | - thelper 37 | - wrapcheck 38 | - wsl 39 | 40 | issues: 41 | exclude-rules: 42 | # not needed 43 | - path: /*.go 44 | text: "ST1003: should not use underscores in package names" 45 | linters: 46 | - stylecheck 47 | - path: /*.go 48 | text: "don't use an underscore in package name" 49 | linters: 50 | - golint 51 | - path: /*_test.go 52 | linters: 53 | - errcheck 54 | - path: /*_example_test.go 55 | linters: 56 | - forbidigo 57 | - path: cmd/start_server/start_server.go 58 | linters: 59 | - forbidigo 60 | 61 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50. 62 | max-issues-per-linter: 0 63 | 64 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 65 | max-same-issues: 0 66 | 67 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 4 | 0.0.2 Jan 06 2015 5 | - Check if given command is executable before starting start_server 6 | - Add support for unix domain sockets (kazeburo) 7 | 8 | 0.0.1 Jan 05 2015 9 | - Initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 lestrrat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | server-starter 2 | ================= 3 | 4 | Go port of ```start_server``` utility (a.k.a. [Server::Starter](https://metacpan.org/pod/Server::Starter)). 5 | 6 | [![Build Status](https://travis-ci.org/lestrrat-go/server-starter.png?branch=master)](https://travis-ci.org/lestrrat-go/server-starter) 7 | 8 | [![GoDoc](https://godoc.org/github.com/lestrrat-go/server-starter?status.svg)](https://godoc.org/github.com/lestrrat-go/server-starter) 9 | 10 | ## DESCRIPTION 11 | 12 | *note: this description is almost entirely taken from the original Server::Starter module* 13 | 14 | The ```start_server``` utility is a superdaemon for hot-deploying server programs. 15 | 16 | It is often a pain to write a server program that supports graceful restarts, with no resource leaks. Server::Starter solves the problem by splitting the task into two: ```start_server``` works as a superdaemon that binds to zero or more TCP ports or unix sockets, and repeatedly spawns the server program that actually handles the necessary tasks (for example, responding to incoming connections). The spawned server programs under ```start_server``` call accept(2) and handle the requests. 17 | 18 | To gracefully restart the server program, send SIGHUP to the superdaemon. The superdaemon spawns a new server program, and if (and only if) it starts up successfully, sends SIGTERM to the old server program. 19 | 20 | By using ```start_server``` it is much easier to write a hot-deployable server. Following are the only requirements a server program to be run under ```start_server``` should conform to: 21 | 22 | - receive file descriptors to listen to through an environment variable - perform a graceful shutdown when receiving SIGTERM 23 | 24 | Many PSGI servers support this. If you want your Go program to support it, you can look under the [listener](https://github.com/lestrrat-go/server-starter/tree/master/listener) directory for an implementation that also fills the ```net.Listener``` interface. 25 | 26 | ## INSTALLATION 27 | 28 | ``` 29 | go get github.com/lestrrat-go/server-starter/cmd/start_server 30 | ``` 31 | -------------------------------------------------------------------------------- /cmd/start_server/start_server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "reflect" 7 | "strings" 8 | "time" 9 | 10 | "github.com/jessevdk/go-flags" 11 | starter "github.com/lestrrat-go/server-starter" 12 | ) 13 | 14 | const version = "0.0.2" 15 | 16 | // nolint:maligned 17 | type options struct { 18 | OptArgs []string 19 | OptCommand string 20 | OptDir string `long:"dir" arg:"path" description:"working directory, start_server do chdir to before exec (optional)"` 21 | OptInterval int `long:"interval" arg:"seconds" description:"minimum interval (in seconds) to respawn the server program (default: 1)"` 22 | OptPorts []string `long:"port" arg:"(port|host:port)" description:"TCP port to listen to (if omitted, will not bind to any ports)"` 23 | OptPaths []string `long:"path" arg:"path" description:"path at where to listen using unix socket (optional)"` 24 | OptSignalOnHUP string `long:"signal-on-hup" arg:"Signal" description:"name of the signal to be sent to the server process when start_server\nreceives a SIGHUP (default: TERM). If you use this option, be sure to\nalso use '--signal-on-term' below."` 25 | OptSignalOnTERM string `long:"signal-on-term" arg:"Signal" description:"name of the signal to be sent to the server process when start_server\nreceives a SIGTERM (default: TERM)"` 26 | OptPidFile string `long:"pid-file" arg:"filename" description:"if set, writes the process id of the start_server process to the file"` 27 | OptStatusFile string `long:"status-file" arg:"filename" description:"if set, writes the status of the server process(es) to the file"` 28 | OptEnvdir string `long:"envdir" arg:"Envdir" description:"directory that contains environment variables to the server processes.\nIt is intended for use with \"envdir\" in \"daemontools\". This can be\noverwritten by environment variable \"ENVDIR\"."` 29 | OptEnableAutoRestart bool `long:"enable-auto-restart" description:"enables automatic restart by time. This can be overwritten by\nenvironment variable \"ENABLE_AUTO_RESTART\"." note:"unimplemented"` 30 | OptAutoRestartInterval int `long:"auto-restart-interval" arg:"seconds" description:"automatic restart interval (default 360). It is used with\n\"--enable-auto-restart\" option. This can be overwritten by environment\nvariable \"AUTO_RESTART_INTERVAL\"." note:"unimplemented"` 31 | OptKillOldDelay int `long:"kill-old-delay" arg:"seconds" description:"time to suspend to send a signal to the old worker. The default value is\n5 when \"--enable-auto-restart\" is set, 0 otherwise. This can be\noverwritten by environment variable \"KILL_OLD_DELAY\"."` 32 | OptRestart bool `long:"restart" description:"this is a wrapper command that reads the pid of the start_server process\nfrom --pid-file, sends SIGHUP to the process and waits until the\nserver(s) of the older generation(s) die by monitoring the contents of\nthe --status-file" note:"unimplemented"` 33 | OptHelp bool `long:"help" description:"prints this help"` 34 | OptVersion bool `long:"version" description:"prints the version number"` 35 | } 36 | 37 | func (o options) Args() []string { return o.OptArgs } 38 | func (o options) Command() string { return o.OptCommand } 39 | func (o options) Dir() string { return o.OptDir } 40 | func (o options) Interval() time.Duration { return time.Duration(o.OptInterval) * time.Second } 41 | func (o options) PidFile() string { return o.OptPidFile } 42 | func (o options) Ports() []string { return o.OptPorts } 43 | func (o options) Paths() []string { return o.OptPaths } 44 | func (o options) SignalOnHUP() os.Signal { return starter.SigFromName(o.OptSignalOnHUP) } 45 | func (o options) SignalOnTERM() os.Signal { return starter.SigFromName(o.OptSignalOnTERM) } 46 | func (o options) StatusFile() string { return o.OptStatusFile } 47 | 48 | func showHelp() { 49 | // The ONLY reason we're not using go-flags' help option is 50 | // because I wanted to tweak the format just a bit... but 51 | // there wasn't an easy way to do so 52 | os.Stderr.WriteString(` 53 | Usage: 54 | start_server [options] -- server-prog server-arg1 server-arg2 ... 55 | 56 | # start Plack using Starlet listening at TCP port 8000 57 | start_server --port=8000 -- plackup -s Starlet --max-workers=100 index.psgi 58 | 59 | Options: 60 | `) 61 | 62 | t := reflect.TypeOf(options{}) 63 | 64 | // This weird indexing stuff is done purely to keep ourselves 65 | // compatible with the original start_server program 66 | // (This is the order that the help is displayed in) 67 | names := []string{ 68 | "OptPorts", 69 | "OptPaths", 70 | "OptDir", 71 | "OptInterval", 72 | "OptSignalOnHUP", 73 | "OptSignalOnTERM", 74 | "OptPidFile", 75 | "OptStatusFile", 76 | "OptEnvdir", 77 | "OptEnableAutoRestart", 78 | "OptAutoRestartInterval", 79 | "OptKillOldDelay", 80 | "OptRestart", 81 | "OptHelp", 82 | "OptVersion", 83 | } 84 | 85 | for _, name := range names { 86 | f, ok := t.FieldByName(name) 87 | if !ok { 88 | continue 89 | } 90 | 91 | tag := f.Tag 92 | if tag == "" { 93 | continue 94 | } 95 | if s := tag.Get("long"); s != "" { 96 | fmt.Fprintf(os.Stderr, " --%s", s) 97 | if a := tag.Get("arg"); a != "" { 98 | fmt.Fprintf(os.Stderr, "=%s", a) 99 | } 100 | if tag.Get("note") == "unimplemented" { 101 | fmt.Fprintf(os.Stderr, " (UNIMPLEMENTED)") 102 | } 103 | fmt.Fprintf(os.Stderr, ":\n") 104 | } 105 | for _, l := range strings.Split(tag.Get("description"), "\n") { 106 | fmt.Fprintf(os.Stderr, " %s\n", l) 107 | } 108 | fmt.Fprintf(os.Stderr, "\n") 109 | } 110 | } 111 | 112 | func main() { 113 | os.Exit(_main()) 114 | } 115 | 116 | func _main() (st int) { 117 | st = 1 118 | 119 | opts := &options{OptInterval: -1} 120 | p := flags.NewParser(opts, flags.PrintErrors|flags.PassDoubleDash) 121 | args, err := p.Parse() 122 | if err != nil || opts.OptHelp { 123 | showHelp() 124 | return 125 | } 126 | 127 | if opts.OptVersion { 128 | fmt.Printf("%s\n", version) 129 | st = 0 130 | return 131 | } 132 | 133 | if opts.OptInterval < 0 { 134 | opts.OptInterval = 1 135 | } 136 | 137 | if len(args) == 0 { 138 | fmt.Fprintf(os.Stderr, "server program not specified\n") 139 | return 140 | } 141 | 142 | opts.OptCommand = args[0] 143 | if len(args) > 1 { 144 | opts.OptArgs = args[1:] 145 | } 146 | 147 | if opts.OptEnvdir != "" { 148 | os.Setenv("ENVDIR", opts.OptEnvdir) 149 | } 150 | 151 | s, err := starter.NewStarter(opts) 152 | if err != nil { 153 | fmt.Fprintf(os.Stderr, "error: %s\n", err) 154 | return 155 | } 156 | if err := s.Run(); err != nil { 157 | fmt.Fprintf(os.Stderr, "error: %s\n", err) 158 | return 159 | } 160 | st = 0 161 | return 162 | } 163 | -------------------------------------------------------------------------------- /env.go: -------------------------------------------------------------------------------- 1 | package starter 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | var errNoEnv = errors.New("no ENVDIR specified, or ENVDIR does not exist") 12 | 13 | func reloadEnv() (map[string]string, error) { 14 | dn := os.Getenv("ENVDIR") 15 | if dn == "" { 16 | return nil, errNoEnv 17 | } 18 | 19 | fi, err := os.Stat(dn) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | if !fi.IsDir() { 25 | return nil, err 26 | } 27 | 28 | var m map[string]string 29 | 30 | _ = filepath.Walk(dn, func(path string, fi os.FileInfo, err error) error { 31 | // Ignore errors 32 | if err != nil { 33 | return nil 34 | } 35 | 36 | // Don't go into directories 37 | if fi.IsDir() && dn != path { 38 | return filepath.SkipDir 39 | } 40 | 41 | f, err := os.Open(path) 42 | if err != nil { 43 | return nil 44 | } 45 | defer f.Close() 46 | 47 | envName := filepath.Base(path) 48 | scanner := bufio.NewScanner(f) 49 | if scanner.Scan() { 50 | if m == nil { 51 | m = make(map[string]string) 52 | } 53 | l := scanner.Text() 54 | m[envName] = strings.TrimSpace(l) 55 | } 56 | 57 | return nil 58 | }) 59 | 60 | if m == nil { 61 | return nil, errNoEnv 62 | } 63 | 64 | return m, nil 65 | } 66 | -------------------------------------------------------------------------------- /env_test.go: -------------------------------------------------------------------------------- 1 | package starter 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | ) 10 | 11 | func TestEnvdir(t *testing.T) { 12 | dir, err := ioutil.TempDir("", "starter_test") 13 | if err != nil { 14 | t.Errorf("Failed to create tempdir: %s", err) 15 | return 16 | } 17 | defer os.RemoveAll(dir) 18 | 19 | files := []string{"FOO", "BAR", "BAZ"} 20 | for _, fn := range files { 21 | longFn := filepath.Join(dir, fn) 22 | 23 | f, err := os.OpenFile(longFn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) 24 | if err != nil { 25 | t.Errorf("Failed to create file '%s': %s", fn, err) 26 | return 27 | } 28 | closed := false 29 | defer func() { 30 | if !closed { 31 | f.Close() 32 | } 33 | }() 34 | 35 | io.WriteString(f, fn) 36 | f.Close() 37 | closed = true 38 | 39 | // save old values and restore later, if any 40 | if old := os.Getenv(fn); old != "" { 41 | os.Setenv(fn, "") 42 | defer os.Setenv(fn, old) 43 | } 44 | } 45 | 46 | if old := os.Getenv("ENVDIR"); old != "" { 47 | defer os.Setenv("ENVDIR", old) 48 | } 49 | 50 | os.Setenv("ENVDIR", dir) 51 | m, err := reloadEnv() 52 | if err != nil { 53 | t.Errorf("reloadEnv failed: %s", err) 54 | return 55 | } 56 | 57 | for _, fn := range files { 58 | v, ok := m[fn] 59 | if !ok { 60 | t.Errorf("Expected environment variable '%s' to exist", fn) 61 | return 62 | } 63 | if v != fn { 64 | t.Errorf("Expected environment variable '%s' to be '%s'", fn, fn) 65 | return 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lestrrat-go/server-starter 2 | 3 | go 1.13 4 | 5 | require github.com/jessevdk/go-flags v1.4.0 6 | -------------------------------------------------------------------------------- /listener/listener.go: -------------------------------------------------------------------------------- 1 | package listener 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "os" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | const ServerStarterEnvVarName = "SERVER_STARTER_PORT" 14 | 15 | var ( 16 | ErrNoListeningTarget = errors.New("no listening target") 17 | ) 18 | 19 | // Listener is the interface for things that listen on file descriptors 20 | // specified by Start::Server / server_starter 21 | type Listener interface { 22 | Fd() uintptr 23 | Listen() (net.Listener, error) 24 | String() string 25 | } 26 | 27 | // List holds a list of Listeners. This is here just for convenience 28 | // so that you can do 29 | // list.String() 30 | // to get a string compatible with SERVER_STARTER_PORT 31 | type List []Listener 32 | 33 | func (ll List) String() string { 34 | list := make([]string, len(ll)) 35 | for i, l := range ll { 36 | list[i] = l.String() 37 | } 38 | return strings.Join(list, ";") 39 | } 40 | 41 | // TCPListener is a listener for ... tcp duh. 42 | type TCPListener struct { 43 | Addr string 44 | Port int 45 | fd uintptr 46 | } 47 | 48 | // UnixListener is a listener for unix sockets. 49 | type UnixListener struct { 50 | Path string 51 | fd uintptr 52 | } 53 | 54 | func (l TCPListener) String() string { 55 | if l.Addr == "0.0.0.0" { 56 | return fmt.Sprintf("%d=%d", l.Port, l.fd) 57 | } 58 | return fmt.Sprintf("%s:%d=%d", l.Addr, l.Port, l.fd) 59 | } 60 | 61 | // Fd returns the underlying file descriptor 62 | func (l TCPListener) Fd() uintptr { 63 | return l.fd 64 | } 65 | 66 | // Listen creates a new Listener 67 | func (l TCPListener) Listen() (net.Listener, error) { 68 | return net.FileListener(os.NewFile(l.Fd(), fmt.Sprintf("%s:%d", l.Addr, l.Port))) 69 | } 70 | 71 | func (l UnixListener) String() string { 72 | return fmt.Sprintf("%s=%d", l.Path, l.fd) 73 | } 74 | 75 | // Fd returns the underlying file descriptor 76 | func (l UnixListener) Fd() uintptr { 77 | return l.fd 78 | } 79 | 80 | // Listen creates a new Listener 81 | func (l UnixListener) Listen() (net.Listener, error) { 82 | return net.FileListener(os.NewFile(l.Fd(), l.Path)) 83 | } 84 | 85 | // Being lazy here... 86 | var reLooksLikeHostPort = regexp.MustCompile(`^(\d+):(\d+)$`) 87 | var reLooksLikePort = regexp.MustCompile(`^\d+$`) 88 | 89 | func parseListenTargets(str string) ([]Listener, error) { 90 | if str == "" { 91 | return nil, ErrNoListeningTarget 92 | } 93 | 94 | rawspec := strings.Split(str, ";") 95 | ret := make([]Listener, len(rawspec)) 96 | 97 | for i, pairString := range rawspec { 98 | pair := strings.Split(pairString, "=") 99 | hostPort := strings.TrimSpace(pair[0]) 100 | fdString := strings.TrimSpace(pair[1]) 101 | fd, err := strconv.ParseUint(fdString, 10, 0) 102 | if err != nil { 103 | return nil, fmt.Errorf("failed to parse '%s' as listen target: %s", pairString, err) 104 | } 105 | 106 | if matches := reLooksLikeHostPort.FindAllString(hostPort, -1); matches != nil { 107 | port, err := strconv.ParseInt(matches[1], 10, 0) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | ret[i] = TCPListener{ 113 | Addr: matches[0], 114 | Port: int(port), 115 | fd: uintptr(fd), 116 | } 117 | } else if match := reLooksLikePort.FindString(hostPort); match != "" { 118 | port, err := strconv.ParseInt(match, 10, 0) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | ret[i] = TCPListener{ 124 | Addr: "0.0.0.0", 125 | Port: int(port), 126 | fd: uintptr(fd), 127 | } 128 | } else { 129 | ret[i] = UnixListener{ 130 | Path: hostPort, 131 | fd: uintptr(fd), 132 | } 133 | } 134 | } 135 | 136 | return ret, nil 137 | } 138 | 139 | // GetPortsSpecification returns the value of SERVER_STARTER_PORT 140 | // environment variable 141 | func GetPortsSpecification() string { 142 | return os.Getenv(ServerStarterEnvVarName) 143 | } 144 | 145 | // Ports parses environment variable SERVER_STARTER_PORT 146 | func Ports() ([]Listener, error) { 147 | return parseListenTargets(GetPortsSpecification()) 148 | } 149 | 150 | // ListenAll parses environment variable SERVER_STARTER_PORT, and creates 151 | // net.Listener objects 152 | func ListenAll() ([]net.Listener, error) { 153 | targets, err := parseListenTargets(GetPortsSpecification()) 154 | if err != nil { 155 | return nil, err 156 | } 157 | 158 | ret := make([]net.Listener, len(targets)) 159 | for i, target := range targets { 160 | ret[i], err = target.Listen() 161 | if err != nil { 162 | // Close everything up to this listener 163 | for x := 0; x < i; x++ { 164 | ret[x].Close() 165 | } 166 | return nil, err 167 | } 168 | } 169 | return ret, nil 170 | } 171 | -------------------------------------------------------------------------------- /listener/listener_test.go: -------------------------------------------------------------------------------- 1 | package listener 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestPort(t *testing.T) { 9 | expect := List{ 10 | TCPListener{Addr: "127.0.0.1", Port: 9090, fd: 4}, 11 | TCPListener{Addr: "0.0.0.0", Port: 8080, fd: 5}, 12 | UnixListener{Path: "/foo/bar/baz.sock", fd: 6}, 13 | } 14 | 15 | os.Setenv("SERVER_STARTER_PORT", expect.String()) 16 | ports, err := Ports() 17 | if err != nil { 18 | t.Errorf("Failed to parse ports from env: %s", err) 19 | } 20 | 21 | for i, port := range ports { 22 | if port.Fd() != expect[i].Fd() { 23 | t.Errorf("parsed fd is not what we expected (expected %d, got %d)", expect[i].Fd(), port.Fd()) 24 | } 25 | } 26 | } 27 | 28 | func TestPortNoEnv(t *testing.T) { 29 | os.Setenv("SERVER_STARTER_PORT", "") 30 | 31 | ports, err := Ports() 32 | if err != ErrNoListeningTarget { 33 | t.Error("Ports must return error if no env") 34 | } 35 | 36 | if ports != nil { 37 | t.Errorf("Ports must return nil if no env") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /releng/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lestrrat-goxc:go1.4 2 | 3 | COPY build-server_starter.sh / 4 | COPY release-server_starter.sh / 5 | 6 | VOLUME ["/work/src/github.com/lestrrat-go/server-starter"] 7 | 8 | CMD echo "server_starter-docker built for go version 1.4" -------------------------------------------------------------------------------- /releng/build-image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | docker build -t server_starter-docker . -------------------------------------------------------------------------------- /releng/build-server_starter.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR=/work/src/github.com/lestrrat-go/server-starter 4 | 5 | pushd $DIR 6 | go get github.com/jessevdk/go-flags 7 | goxc \ 8 | -n start_server \ 9 | -tasks "xc archive" \ 10 | -bc "linux windows darwin" \ 11 | -wd $DIR \ 12 | -resources-include "README*,Changes" \ 13 | -d /work/artifacts -------------------------------------------------------------------------------- /releng/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | SSDIR=$(cd $(dirname $0)/..; pwd -P) 6 | id=$(echo $(date) $$| shasum | awk '{print $1}') 7 | docker run --rm \ 8 | --name server_starter-build-$id \ 9 | -v $SSDIR:/work/src/github.com/lestrrat-go/server-starter/ \ 10 | -e RESULTSDIR=/work/artifacts \ 11 | server_starter-docker \ 12 | ./build-server_starter.sh -------------------------------------------------------------------------------- /releng/release-server_starter.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ -z "$GITHUB_TOKEN" ]; then 6 | echo "GITHUB_TOKEN environment variable must be set" 7 | exit 1 8 | fi 9 | 10 | if [ -z "$GITHUB_USERNAME" ]; then 11 | echo "GITHUB_USERNAME environment variable must be set" 12 | exit 1 13 | fi 14 | 15 | if [ -z "$SS_VERSION" ]; then 16 | echo "SS_VERSION environment variable must be set" 17 | exit 1 18 | fi 19 | 20 | # Change directory to the project because that makes 21 | # things much easier 22 | cd /work/src/github.com/lestrrat-go/server-starter 23 | 24 | /build-server_starter.sh 25 | ghr --debug -p 1 --replace -u "$GITHUB_USERNAME" $SS_VERSION /work/artifacts/snapshot -------------------------------------------------------------------------------- /releng/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | SSDIR=$(cd $(dirname $0)/..; pwd -P) 6 | 7 | if [ -z "$SS_VERSION" ]; then 8 | echo "SS_VERSION must be specified" 9 | exit 1 10 | fi 11 | 12 | if [ -z "$GITHUB_TOKEN_FILE" ]; then 13 | GITHUB_TOKEN_FILE=github_token 14 | fi 15 | 16 | if [ ! -e "$GITHUB_TOKEN_FILE" ]; then 17 | echo "GITHUB_TOKEN_FILE does not exist" 18 | exit 1 19 | fi 20 | 21 | docker run --rm \ 22 | -v $SSDIR:/work/src/github.com/lestrrat-go/server-starter/ \ 23 | -e SS_VERSION=$SS_VERSION \ 24 | -e GITHUB_USERNAME=lestrrat \ 25 | -e GITHUB_TOKEN=`cat $GITHUB_TOKEN_FILE` \ 26 | server_starter-docker \ 27 | /release-server_starter.sh 28 | 29 | -------------------------------------------------------------------------------- /starter.go: -------------------------------------------------------------------------------- 1 | package starter 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | "os/exec" 8 | "os/signal" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | "syscall" 13 | "time" 14 | ) 15 | 16 | var niceSigNames map[syscall.Signal]string 17 | var niceNameToSigs map[string]syscall.Signal 18 | var successStatus syscall.WaitStatus 19 | var failureStatus syscall.WaitStatus 20 | 21 | func makeNiceSigNamesCommon() map[syscall.Signal]string { 22 | return map[syscall.Signal]string{ 23 | syscall.SIGABRT: "ABRT", 24 | syscall.SIGALRM: "ALRM", 25 | syscall.SIGBUS: "BUS", 26 | // syscall.SIGEMT: "EMT", 27 | syscall.SIGFPE: "FPE", 28 | syscall.SIGHUP: "HUP", 29 | syscall.SIGILL: "ILL", 30 | // syscall.SIGINFO: "INFO", 31 | syscall.SIGINT: "INT", 32 | // syscall.SIGIOT: "IOT", 33 | syscall.SIGKILL: "KILL", 34 | syscall.SIGPIPE: "PIPE", 35 | syscall.SIGQUIT: "QUIT", 36 | syscall.SIGSEGV: "SEGV", 37 | syscall.SIGTERM: "TERM", 38 | syscall.SIGTRAP: "TRAP", 39 | } 40 | } 41 | 42 | func makeNiceSigNames() map[syscall.Signal]string { 43 | return addPlatformDependentNiceSigNames(makeNiceSigNamesCommon()) 44 | } 45 | 46 | func init() { 47 | niceSigNames = makeNiceSigNames() 48 | niceNameToSigs = make(map[string]syscall.Signal) 49 | for sig, name := range niceSigNames { 50 | niceNameToSigs[name] = sig 51 | } 52 | } 53 | 54 | type listener struct { 55 | listener net.Listener 56 | spec string // path or port spec 57 | } 58 | 59 | type Config interface { 60 | Args() []string 61 | Command() string 62 | Dir() string // Directory to chdir to before executing the command 63 | Interval() time.Duration // Time between checks for liveness 64 | PidFile() string 65 | Ports() []string // Ports to bind to (addr:port or port, so it's a string) 66 | Paths() []string // Paths (UNIX domain socket) to bind to 67 | SignalOnHUP() os.Signal // Signal to send when HUP is received 68 | SignalOnTERM() os.Signal // Signal to send when TERM is received 69 | StatusFile() string 70 | } 71 | 72 | type Starter struct { 73 | interval time.Duration 74 | signalOnHUP os.Signal 75 | signalOnTERM os.Signal 76 | // you can't set this in go: backlog 77 | statusFile string 78 | pidFile string 79 | dir string 80 | ports []string 81 | paths []string 82 | listeners []listener 83 | generation int 84 | command string 85 | args []string 86 | mu sync.RWMutex 87 | } 88 | 89 | // NewStarter creates a new Starter object. Config parameter may NOT be 90 | // nil, as `Ports` and/or `Paths`, and `Command` are required 91 | func NewStarter(c Config) (*Starter, error) { 92 | if c == nil { 93 | return nil, fmt.Errorf("config argument must be non-nil") 94 | } 95 | 96 | var signalOnHUP os.Signal = syscall.SIGTERM 97 | var signalOnTERM os.Signal = syscall.SIGTERM 98 | if s := c.SignalOnHUP(); s != nil { 99 | signalOnHUP = s 100 | } 101 | if s := c.SignalOnTERM(); s != nil { 102 | signalOnTERM = s 103 | } 104 | 105 | if c.Command() == "" { 106 | return nil, fmt.Errorf("argument Command must be specified") 107 | } 108 | if _, err := exec.LookPath(c.Command()); err != nil { 109 | return nil, err 110 | } 111 | 112 | s := &Starter{ 113 | args: c.Args(), 114 | command: c.Command(), 115 | dir: c.Dir(), 116 | interval: c.Interval(), 117 | listeners: make([]listener, 0, len(c.Ports())+len(c.Paths())), 118 | pidFile: c.PidFile(), 119 | ports: c.Ports(), 120 | paths: c.Paths(), 121 | signalOnHUP: signalOnHUP, 122 | signalOnTERM: signalOnTERM, 123 | statusFile: c.StatusFile(), 124 | } 125 | 126 | return s, nil 127 | } 128 | 129 | func (s *Starter) Stop() { 130 | p, _ := os.FindProcess(os.Getpid()) 131 | _ = p.Signal(syscall.SIGTERM) 132 | } 133 | 134 | func grabExitStatus(st processState) syscall.WaitStatus { 135 | // Note: POSSIBLY non portable. seems to work on Unix/Windows 136 | // When/if this blows up, we will look for a cure 137 | exitSt, ok := st.Sys().(syscall.WaitStatus) 138 | if !ok { 139 | fmt.Fprintf(os.Stderr, "Oh no, you are running on a platform where ProcessState.Sys().(syscall.WaitStatus) doesn't work! We're doomed! Temporarily setting status to 255. Please contact the author about this\n") 140 | exitSt = failureStatus 141 | } 142 | return exitSt 143 | } 144 | 145 | type processState interface { 146 | Pid() int 147 | Sys() interface{} 148 | } 149 | type dummyProcessState struct { 150 | pid int 151 | status syscall.WaitStatus 152 | } 153 | 154 | func (d dummyProcessState) Pid() int { 155 | return d.pid 156 | } 157 | 158 | func (d dummyProcessState) Sys() interface{} { 159 | return d.status 160 | } 161 | 162 | func signame(s os.Signal) string { 163 | if ss, ok := s.(syscall.Signal); ok { 164 | return niceSigNames[ss] 165 | } 166 | return "UNKNOWN" 167 | } 168 | 169 | // SigFromName returns the signal corresponding to the given signal name string. 170 | // If the given name string is not defined, it returns nil. 171 | func SigFromName(n string) os.Signal { 172 | n = strings.TrimPrefix(strings.ToUpper(n), "SIG") 173 | if sig, ok := niceNameToSigs[n]; ok { 174 | return sig 175 | } 176 | return nil 177 | } 178 | 179 | func setEnv() { 180 | if os.Getenv("ENVDIR") == "" { 181 | return 182 | } 183 | 184 | m, err := reloadEnv() 185 | if err != nil && err != errNoEnv { 186 | // do something 187 | fmt.Fprintf(os.Stderr, "failed to load from envdir: %s\n", err) 188 | } 189 | 190 | for k, v := range m { 191 | os.Setenv(k, v) 192 | } 193 | } 194 | 195 | func parsePortSpec(addr string) (string, int, error) { 196 | i := strings.IndexByte(addr, ':') 197 | portPart := "" 198 | if i < 0 { 199 | portPart = addr 200 | addr = "" 201 | } else { 202 | portPart = addr[i+1:] 203 | addr = addr[:i] 204 | } 205 | 206 | port, err := strconv.ParseInt(portPart, 10, 64) 207 | if err != nil { 208 | return "", -1, err 209 | } 210 | 211 | return addr, int(port), nil 212 | } 213 | 214 | func (s *Starter) Run() error { 215 | // nolint:errcheck 216 | defer s.Teardown() 217 | 218 | if s.pidFile != "" { 219 | f, err := os.OpenFile(s.pidFile, os.O_EXCL|os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) 220 | if err != nil { 221 | return err 222 | } 223 | 224 | if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil { 225 | return err 226 | } 227 | fmt.Fprintf(f, "%d", os.Getpid()) 228 | defer func() { 229 | os.Remove(f.Name()) 230 | f.Close() 231 | }() 232 | } 233 | 234 | for _, addr := range s.ports { 235 | var l net.Listener 236 | 237 | host, port, err := parsePortSpec(addr) 238 | if err != nil { 239 | fmt.Fprintf(os.Stderr, "failed to parse addr spec '%s': %s", addr, err) 240 | return err 241 | } 242 | 243 | hostport := fmt.Sprintf("%s:%d", host, port) 244 | l, err = net.Listen("tcp4", hostport) 245 | if err != nil { 246 | fmt.Fprintf(os.Stderr, "failed to listen to %s:%s\n", hostport, err) 247 | return err 248 | } 249 | 250 | spec := "" 251 | if host == "" { 252 | spec = fmt.Sprintf("%d", port) 253 | } else { 254 | spec = fmt.Sprintf("%s:%d", host, port) 255 | } 256 | s.mu.Lock() 257 | s.listeners = append(s.listeners, listener{listener: l, spec: spec}) 258 | s.mu.Unlock() 259 | } 260 | 261 | for _, path := range s.paths { 262 | var l net.Listener 263 | if fl, err := os.Lstat(path); err == nil && fl.Mode()&os.ModeSocket == os.ModeSocket { 264 | fmt.Fprintf(os.Stderr, "removing existing socket file:%s\n", path) 265 | err = os.Remove(path) 266 | if err != nil { 267 | fmt.Fprintf(os.Stderr, "failed to remove existing socket file:%s:%s\n", path, err) 268 | return err 269 | } 270 | } 271 | _ = os.Remove(path) 272 | l, err := net.Listen("unix", path) 273 | if err != nil { 274 | fmt.Fprintf(os.Stderr, "failed to listen file:%s:%s\n", path, err) 275 | return err 276 | } 277 | s.mu.Lock() 278 | s.listeners = append(s.listeners, listener{listener: l, spec: path}) 279 | s.mu.Unlock() 280 | } 281 | 282 | s.generation = 0 283 | os.Setenv("SERVER_STARTER_GENERATION", fmt.Sprintf("%d", s.generation)) 284 | 285 | // XXX Not portable 286 | sigCh := make(chan os.Signal, 1) 287 | signal.Notify(sigCh, 288 | syscall.SIGHUP, 289 | syscall.SIGINT, 290 | syscall.SIGTERM, 291 | syscall.SIGQUIT, 292 | ) 293 | 294 | // Okay, ready to launch the program now... 295 | setEnv() 296 | workerCh := make(chan processState) 297 | p := s.StartWorker(sigCh, workerCh) 298 | oldWorkers := make(map[int]int) 299 | var sigReceived os.Signal 300 | var sigToSend os.Signal 301 | 302 | statusCh := make(chan map[int]int) 303 | go func(fn string, ch chan map[int]int) { 304 | for wmap := range ch { 305 | if fn == "" { 306 | continue 307 | } 308 | 309 | f, err := os.OpenFile(fn, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 310 | if err != nil { 311 | continue 312 | } 313 | 314 | for gen, pid := range wmap { 315 | fmt.Fprintf(f, "%d:%d\n", gen, pid) 316 | } 317 | 318 | f.Close() 319 | } 320 | }(s.statusFile, statusCh) 321 | 322 | defer func() { 323 | if p != nil { 324 | oldWorkers[p.Pid] = s.generation 325 | } 326 | 327 | fmt.Fprintf(os.Stderr, "received %s, sending %s to all workers:", 328 | signame(sigReceived), 329 | signame(sigToSend), 330 | ) 331 | size := len(oldWorkers) 332 | i := 0 333 | for pid := range oldWorkers { 334 | i++ 335 | fmt.Fprintf(os.Stderr, "%d", pid) 336 | if i < size { 337 | fmt.Fprintf(os.Stderr, ",") 338 | } 339 | } 340 | fmt.Fprintf(os.Stderr, "\n") 341 | 342 | for pid := range oldWorkers { 343 | worker, err := os.FindProcess(pid) 344 | if err != nil { 345 | continue 346 | } 347 | _ = worker.Signal(sigToSend) 348 | } 349 | 350 | for len(oldWorkers) > 0 { 351 | st := <-workerCh 352 | fmt.Fprintf(os.Stderr, "worker %d died, status:%d\n", st.Pid(), grabExitStatus(st)) 353 | delete(oldWorkers, st.Pid()) 354 | } 355 | fmt.Fprintf(os.Stderr, "exiting\n") 356 | }() 357 | 358 | // var lastRestartTime time.Time 359 | for { // outer loop 360 | setEnv() 361 | 362 | // Just wait for the worker to exit, or for us to receive a signal 363 | for { 364 | status := make(map[int]int) 365 | for pid, gen := range oldWorkers { 366 | status[gen] = pid 367 | } 368 | status[s.generation] = p.Pid 369 | statusCh <- status 370 | // restart = 2: force restart 371 | // restart = 1 and no workers: force restart 372 | // restart = 0: no restart 373 | restart := 0 374 | 375 | select { 376 | case st := <-workerCh: 377 | // oops, the worker exited? check for its pid 378 | if p.Pid == st.Pid() { // current worker 379 | exitSt := grabExitStatus(st) 380 | fmt.Fprintf(os.Stderr, "worker %d died unexpectedly with status %d, restarting\n", p.Pid, exitSt) 381 | p = s.StartWorker(sigCh, workerCh) 382 | // lastRestartTime = time.Now() 383 | } else { 384 | exitSt := grabExitStatus(st) 385 | fmt.Fprintf(os.Stderr, "old worker %d died, status:%d\n", st.Pid(), exitSt) 386 | delete(oldWorkers, st.Pid()) 387 | } 388 | case sigReceived = <-sigCh: 389 | // Temporary fix 390 | switch sigReceived { 391 | case syscall.SIGHUP: 392 | // When we receive a HUP signal, we need to spawn a new worker 393 | fmt.Fprintf(os.Stderr, "received HUP (num_old_workers=TODO)\n") 394 | restart = 1 395 | sigToSend = s.signalOnHUP 396 | case syscall.SIGTERM: 397 | sigToSend = s.signalOnTERM 398 | return nil 399 | default: 400 | sigToSend = syscall.SIGTERM 401 | return nil 402 | } 403 | } 404 | 405 | if restart > 1 || restart > 0 && len(oldWorkers) == 0 { 406 | fmt.Fprintf(os.Stderr, "spawning a new worker (num_old_workers=TODO)\n") 407 | oldWorkers[p.Pid] = s.generation 408 | p = s.StartWorker(sigCh, workerCh) 409 | fmt.Fprintf(os.Stderr, "new worker is now running, sending %s to old workers:", signame(sigToSend)) 410 | size := len(oldWorkers) 411 | if size == 0 { 412 | fmt.Fprintf(os.Stderr, "none\n") 413 | } else { 414 | i := 0 415 | for pid := range oldWorkers { 416 | i++ 417 | fmt.Fprintf(os.Stderr, "%d", pid) 418 | if i < size { 419 | fmt.Fprintf(os.Stderr, ",") 420 | } 421 | } 422 | fmt.Fprintf(os.Stderr, "\n") 423 | 424 | killOldDelay := getKillOldDelay() 425 | fmt.Fprintf(os.Stderr, "sleep %d secs\n", int(killOldDelay/time.Second)) 426 | if killOldDelay > 0 { 427 | time.Sleep(killOldDelay) 428 | } 429 | 430 | fmt.Fprintf(os.Stderr, "killing old workers\n") 431 | 432 | for pid := range oldWorkers { 433 | worker, err := os.FindProcess(pid) 434 | if err != nil { 435 | continue 436 | } 437 | _ = worker.Signal(s.signalOnHUP) 438 | } 439 | } 440 | } 441 | } 442 | } 443 | 444 | // nolint:govet 445 | return nil 446 | } 447 | 448 | func getKillOldDelay() time.Duration { 449 | // Ignore errors. 450 | delay, _ := strconv.ParseInt(os.Getenv("KILL_OLD_DELAY"), 10, 0) 451 | autoRestart, _ := strconv.ParseBool(os.Getenv("ENABLE_AUTO_RESTART")) 452 | if autoRestart && delay == 0 { 453 | delay = 5 454 | } 455 | 456 | return time.Duration(delay) * time.Second 457 | } 458 | 459 | type WorkerState int 460 | 461 | const ( 462 | WorkerStarted WorkerState = iota 463 | ErrFailedToStart 464 | ) 465 | 466 | // StartWorker starts the actual command. 467 | func (s *Starter) StartWorker(sigCh chan os.Signal, ch chan processState) *os.Process { 468 | // Don't give up until we're running. 469 | for { 470 | pid := -1 471 | cmd := exec.Command(s.command, s.args...) 472 | if s.dir != "" { 473 | cmd.Dir = s.dir 474 | } 475 | cmd.Stdout = os.Stdout 476 | cmd.Stderr = os.Stderr 477 | 478 | // This whole section here basically sets up the env 479 | // var and the file descriptors that are inherited by the 480 | // external process 481 | files := make([]*os.File, len(s.ports)+len(s.paths)) 482 | ports := make([]string, len(s.ports)+len(s.paths)) 483 | s.mu.RLock() 484 | for i, l := range s.listeners { 485 | // file descriptor numbers in ExtraFiles turn out to be 486 | // index + 3, so we can just hard code it 487 | var f *os.File 488 | var err error 489 | switch l.listener.(type) { 490 | case *net.TCPListener: 491 | f, err = l.listener.(*net.TCPListener).File() 492 | case *net.UnixListener: 493 | f, err = l.listener.(*net.UnixListener).File() 494 | default: 495 | panic("Unknown listener type") 496 | } 497 | if err != nil { 498 | panic(err) 499 | } 500 | defer f.Close() 501 | ports[i] = fmt.Sprintf("%s=%d", l.spec, i+3) 502 | files[i] = f 503 | } 504 | s.mu.RUnlock() 505 | cmd.ExtraFiles = files 506 | 507 | s.generation++ 508 | os.Setenv("SERVER_STARTER_PORT", strings.Join(ports, ";")) 509 | os.Setenv("SERVER_STARTER_GENERATION", fmt.Sprintf("%d", s.generation)) 510 | 511 | // Now start! 512 | if err := cmd.Start(); err != nil { 513 | fmt.Fprintf(os.Stderr, "failed to exec %s: %s\n", cmd.Path, err) 514 | } else { 515 | // Save pid... 516 | pid = cmd.Process.Pid 517 | fmt.Fprintf(os.Stderr, "starting new worker %d\n", pid) 518 | 519 | // Wait for interval before checking if the process is alive 520 | tch := time.After(s.interval) 521 | sigs := []os.Signal{} 522 | for loop := true; loop; { 523 | select { 524 | case <-tch: 525 | // bail out 526 | loop = false 527 | case sig := <-sigCh: 528 | sigs = append(sigs, sig) 529 | } 530 | } 531 | // if received any signals, during the wait, we bail out 532 | gotSig := false 533 | if len(sigs) > 0 { 534 | for _, sig := range sigs { 535 | // we need to resend these signals so it can be caught in the 536 | // main routine... 537 | go func(sig os.Signal) { 538 | sigCh <- sig 539 | }(sig) 540 | if sysSig, ok := sig.(syscall.Signal); ok { 541 | if sysSig != syscall.SIGHUP { 542 | gotSig = true 543 | } 544 | } 545 | } 546 | } 547 | 548 | // Check if we can find a process by its pid 549 | p := findWorker(pid) 550 | if gotSig || p != nil { 551 | // No error? We were successful! Make sure we capture 552 | // the program exiting 553 | go func() { 554 | err := cmd.Wait() 555 | if err != nil { 556 | ch <- err.(*exec.ExitError).ProcessState 557 | } else { 558 | ch <- &dummyProcessState{pid: pid, status: successStatus} 559 | } 560 | }() 561 | // Bail out 562 | return p 563 | } 564 | } 565 | // If we fall through here, we prematurely exited :/ 566 | // Make sure to wait to release resources 567 | _ = cmd.Wait() 568 | for _, f := range cmd.ExtraFiles { 569 | f.Close() 570 | } 571 | 572 | fmt.Fprintf(os.Stderr, "new worker %d seems to have failed to start\n", pid) 573 | } 574 | 575 | // never reached 576 | //nolint:govet 577 | return nil 578 | } 579 | 580 | func (s *Starter) Teardown() error { 581 | if s.statusFile != "" { 582 | os.Remove(s.statusFile) 583 | } 584 | 585 | s.mu.RLock() 586 | for _, l := range s.listeners { 587 | l.listener.Close() 588 | } 589 | s.mu.RUnlock() 590 | 591 | return nil 592 | } 593 | -------------------------------------------------------------------------------- /starter_any.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package starter 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | ) 9 | 10 | func init() { 11 | failureStatus = syscall.WaitStatus(255) 12 | successStatus = syscall.WaitStatus(0) 13 | } 14 | 15 | func addPlatformDependentNiceSigNames(v map[syscall.Signal]string) map[syscall.Signal]string { 16 | v[syscall.SIGCHLD] = "CHLD" 17 | v[syscall.SIGCONT] = "CONT" 18 | v[syscall.SIGIO] = "IO" 19 | v[syscall.SIGPROF] = "PROF" 20 | v[syscall.SIGSTOP] = "STOP" 21 | v[syscall.SIGSYS] = "SYS" 22 | v[syscall.SIGTSTP] = "TSTP" 23 | v[syscall.SIGTTIN] = "TTIN" 24 | v[syscall.SIGTTOU] = "TTOU" 25 | v[syscall.SIGURG] = "URG" 26 | v[syscall.SIGUSR1] = "USR1" 27 | v[syscall.SIGUSR2] = "USR2" 28 | v[syscall.SIGVTALRM] = "VTALRM" 29 | v[syscall.SIGWINCH] = "WINCH" 30 | v[syscall.SIGXCPU] = "XCPU" 31 | v[syscall.SIGXFSZ] = "GXFSZ" 32 | return v 33 | } 34 | 35 | func findWorker(pid int) *os.Process { 36 | var wstatus syscall.WaitStatus 37 | waitpid, _ := syscall.Wait4(pid, &wstatus, syscall.WNOHANG, nil) 38 | if waitpid <= 0 { 39 | p, _ := os.FindProcess(pid) 40 | return p 41 | } 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /starter_test.go: -------------------------------------------------------------------------------- 1 | package starter 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "log" 8 | "net" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "regexp" 13 | "strings" 14 | "syscall" 15 | "testing" 16 | "time" 17 | ) 18 | 19 | var echoServerTxt = `package main 20 | 21 | import ( 22 | "fmt" 23 | "io" 24 | "net/http" 25 | "os" 26 | "os/signal" 27 | "syscall" 28 | "time" 29 | "github.com/lestrrat-go/server-starter/listener" 30 | ) 31 | 32 | func main() { 33 | listeners, err := listener.ListenAll() 34 | if err != nil { 35 | fmt.Fprintf(os.Stderr, "Failed to listen: %s\n", err) 36 | os.Exit(1) 37 | } 38 | 39 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 40 | io.Copy(w, r.Body) 41 | }) 42 | for _, l := range listeners { 43 | http.Serve(l, handler) 44 | } 45 | 46 | loop := false 47 | sigCh := make(chan os.Signal) 48 | signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGHUP) 49 | for loop { 50 | select { 51 | case <-sigCh: 52 | loop = false 53 | default: 54 | time.Sleep(time.Second) 55 | } 56 | } 57 | } 58 | ` 59 | 60 | type config struct { 61 | args []string 62 | command string 63 | dir string 64 | interval int 65 | pidfile string 66 | ports []string 67 | paths []string 68 | sigonhup string 69 | sigonterm string 70 | statusfile string 71 | } 72 | 73 | func (c config) Args() []string { return c.args } 74 | func (c config) Command() string { return c.command } 75 | func (c config) Dir() string { return c.dir } 76 | func (c config) Interval() time.Duration { return time.Duration(c.interval) * time.Second } 77 | func (c config) PidFile() string { return c.pidfile } 78 | func (c config) Ports() []string { return c.ports } 79 | func (c config) Paths() []string { return c.paths } 80 | func (c config) SignalOnHUP() os.Signal { return SigFromName(c.sigonhup) } 81 | func (c config) SignalOnTERM() os.Signal { return SigFromName(c.sigonterm) } 82 | func (c config) StatusFile() string { return c.statusfile } 83 | 84 | func TestRun(t *testing.T) { 85 | dir, err := ioutil.TempDir("", fmt.Sprintf("server-starter-test-%d", os.Getpid())) 86 | if err != nil { 87 | t.Errorf("Failed to create temp directory: %s", err) 88 | return 89 | } 90 | defer os.RemoveAll(dir) 91 | 92 | srcFile := filepath.Join(dir, "echod.go") 93 | f, err := os.OpenFile(srcFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) 94 | if err != nil { 95 | t.Errorf("Failed to create %s: %s", srcFile, err) 96 | return 97 | } 98 | io.WriteString(f, echoServerTxt) 99 | f.Close() 100 | 101 | _, lastComp := filepath.Split(dir) 102 | cmd := exec.Command("go", "mod", "init", "github.com/lestrrat-go/server-starter/"+lastComp) 103 | cmd.Dir = dir 104 | if output, err := cmd.CombinedOutput(); err != nil { 105 | t.Logf("%s", output) 106 | t.Errorf("failed to run go mod init: %s", err) 107 | return 108 | } 109 | 110 | cmd = exec.Command("go", "build", "-o", filepath.Join(dir, "echod"), ".") 111 | cmd.Dir = dir 112 | if output, err := cmd.CombinedOutput(); err != nil { 113 | t.Errorf("Failed to compile %s: %s\n%s", dir, err, output) 114 | return 115 | } 116 | 117 | ports := []string{"9090", "8080"} 118 | sd, err := NewStarter(&config{ 119 | ports: ports, 120 | command: filepath.Join(dir, "echod"), 121 | }) 122 | if err != nil { 123 | t.Errorf("Failed to create starter: %s", err) 124 | return 125 | } 126 | 127 | doneCh := make(chan struct{}) 128 | readyCh := make(chan struct{}) 129 | go func() { 130 | defer func() { doneCh <- struct{}{} }() 131 | time.AfterFunc(500*time.Millisecond, func() { 132 | readyCh <- struct{}{} 133 | }) 134 | if err := sd.Run(); err != nil { 135 | t.Errorf("sd.Run() failed: %s", err) 136 | } 137 | t.Logf("Exiting...") 138 | }() 139 | 140 | <-readyCh 141 | 142 | for _, port := range ports { 143 | _, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%s", port)) 144 | if err != nil { 145 | t.Errorf("Error connecing to port '%s': %s", port, err) 146 | } 147 | } 148 | 149 | time.AfterFunc(time.Second, sd.Stop) 150 | <-doneCh 151 | 152 | log.Printf("Checking ports...") 153 | 154 | patterns := make([]string, len(ports)) 155 | for i, port := range ports { 156 | patterns[i] = fmt.Sprintf(`%s=\d+`, port) 157 | } 158 | pattern := regexp.MustCompile(strings.Join(patterns, ";")) 159 | 160 | if envPort := os.Getenv("SERVER_STARTER_PORT"); !pattern.MatchString(envPort) { 161 | t.Errorf("SERVER_STARTER_PORT: Expected '%s', but got '%s'", pattern, envPort) 162 | } 163 | } 164 | 165 | func TestSigFromName(t *testing.T) { 166 | for sig, name := range niceSigNames { 167 | if got := SigFromName(name); sig != got { 168 | t.Errorf("%v: wants '%v' but got '%v'", name, sig, got) 169 | } 170 | } 171 | 172 | variants := map[string]syscall.Signal{ 173 | "SIGTERM": syscall.SIGTERM, 174 | "sigterm": syscall.SIGTERM, 175 | "Hup": syscall.SIGHUP, 176 | } 177 | for name, sig := range variants { 178 | if got := SigFromName(name); sig != got { 179 | t.Errorf("%v: wants '%v' but got '%v'", name, sig, got) 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /starter_windows.go: -------------------------------------------------------------------------------- 1 | package starter 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | ) 7 | 8 | func init() { 9 | failureStatus = syscall.WaitStatus{ExitCode: 255} 10 | successStatus = syscall.WaitStatus{ExitCode: 0} 11 | } 12 | 13 | func addPlatformDependentNiceSigNames(v map[syscall.Signal]string) map[syscall.Signal]string { 14 | return v 15 | } 16 | 17 | func findWorker(pid int) *os.Process { 18 | p, err := os.FindProcess(pid) 19 | if err != nil { 20 | return p 21 | } 22 | return nil 23 | } 24 | --------------------------------------------------------------------------------