1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
|
package tempredis
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"strings"
"syscall"
)
const (
// ready is the string redis-server prints to stdout after starting
// successfully.
ready = "The server is now ready to accept connections"
)
// Server encapsulates the configuration, starting, and stopping of a single
// redis-server process that is reachable via a local Unix socket.
type Server struct {
dir string
config Config
cmd *exec.Cmd
stdout io.Reader
stdoutBuf bytes.Buffer
stderr io.Reader
}
// Start initiates a new redis-server process configured with the given
// configuration. redis-server will listen on a temporary local Unix socket. An
// error is returned if redis-server is unable to successfully start for any
// reason.
func Start(config Config) (server *Server, err error) {
if config == nil {
config = Config{}
}
dir, err := ioutil.TempDir(os.TempDir(), "tempredis")
if err != nil {
return nil, err
}
if _, ok := config["unixsocket"]; !ok {
config["unixsocket"] = fmt.Sprintf("%s/%s", dir, "redis.sock")
}
if _, ok := config["port"]; !ok {
config["port"] = "0"
}
server = &Server{
dir: dir,
config: config,
}
err = server.start()
if err != nil {
return server, err
}
// Block until Redis is ready to accept connections.
err = server.waitFor(ready)
return server, err
}
func (s *Server) start() (err error) {
if s.cmd != nil {
return fmt.Errorf("redis-server has already been started")
}
s.cmd = exec.Command("redis-server", "-")
stdin, _ := s.cmd.StdinPipe()
s.stdout, _ = s.cmd.StdoutPipe()
s.stderr, _ = s.cmd.StderrPipe()
err = s.cmd.Start()
if err == nil {
err = writeConfig(s.config, stdin)
}
return err
}
func writeConfig(config Config, w io.WriteCloser) (err error) {
for key, value := range config {
if value == "" {
value = "\"\""
}
_, err = fmt.Fprintf(w, "%s %s\n", key, value)
if err != nil {
return err
}
}
return w.Close()
}
// waitFor blocks until redis-server prints the given string to stdout.
func (s *Server) waitFor(search string) (err error) {
var line string
scanner := bufio.NewScanner(s.stdout)
for scanner.Scan() {
line = scanner.Text()
fmt.Fprintf(&s.stdoutBuf, "%s\n", line)
if strings.Contains(line, search) {
return nil
}
}
err = scanner.Err()
if err == nil {
err = io.EOF
}
return err
}
// Socket returns the full path to the local redis-server Unix socket.
func (s *Server) Socket() string {
return s.config.Socket()
}
// Stdout blocks until redis-server returns and then returns the full stdout
// output.
func (s *Server) Stdout() string {
io.Copy(&s.stdoutBuf, s.stdout)
return s.stdoutBuf.String()
}
// Stderr blocks until redis-server returns and then returns the full stdout
// output.
func (s *Server) Stderr() string {
bytes, _ := ioutil.ReadAll(s.stderr)
return string(bytes)
}
// Term gracefully shuts down redis-server. It returns an error if redis-server
// fails to terminate.
func (s *Server) Term() (err error) {
return s.signalAndCleanup(syscall.SIGTERM)
}
// Kill forcefully shuts down redis-server. It returns an error if redis-server
// fails to die.
func (s *Server) Kill() (err error) {
return s.signalAndCleanup(syscall.SIGKILL)
}
func (s *Server) signalAndCleanup(sig syscall.Signal) error {
s.cmd.Process.Signal(sig)
_, err := s.cmd.Process.Wait()
os.RemoveAll(s.dir)
return err
}
|