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 }