go-imap icon indicating copy to clipboard operation
go-imap copied to clipboard

client: Dovecot startup failure hangs test

Open emersion opened this issue 1 year ago • 1 comments

If a bad config file is provided to Dovecot, the net.Pipe will not be closed and the test hangs.

emersion avatar Apr 25 '24 10:04 emersion

For some reason cmd.Wait() in a goroutine hangs after doveadm exits with an error... Seems like cmd.Process.Wait() works as expected though.

emersion avatar Apr 25 '24 10:04 emersion

Hm, it doesn't seem like passing an *os.File as stdin/stdout/stderr helps…

diff --git a/imapclient/dovecot_test.go b/imapclient/dovecot_test.go
index 10386004ec52..c9599eb0d3b2 100644
--- a/imapclient/dovecot_test.go
+++ b/imapclient/dovecot_test.go
@@ -7,13 +7,16 @@ import (
 	"os/exec"
 	"path/filepath"
 	"testing"
+	"syscall"
+
+	"log"
 )
 
 func newDovecotClientServerPair(t *testing.T) (net.Conn, io.Closer) {
 	tempDir := t.TempDir()
 
 	cfgFilename := filepath.Join(tempDir, "dovecot.conf")
-	cfg := `log_path      = "` + tempDir + `/dovecot.log"
+	cfg := `foolog_path      = "` + tempDir + `/dovecot.log"
 ssl           = no
 mail_home     = "` + tempDir + `/%u"
 mail_location = maildir:~/Mail
@@ -36,29 +39,71 @@ plugin {
 		t.Fatalf("failed to write Dovecot config: %v", err)
 	}
 
-	clientConn, serverConn := net.Pipe()
+	// Use an *os.File when spawning the child process so that cmd.Wait doesn't
+	// wait for the pipe copy to complete
+	clientSocketFile, serverSocketFile, err := createSocketPair()
+	if err != nil {
+		t.Fatalf("failed to create socket pair: %v", err)
+	}
 
 	cmd := exec.Command("doveadm", "-c", cfgFilename, "exec", "imap")
 	cmd.Env = []string{"USER=" + testUsername, "PATH=" + os.Getenv("PATH")}
 	cmd.Dir = tempDir
-	cmd.Stdin = serverConn
-	cmd.Stdout = serverConn
+	cmd.Stdin = serverSocketFile
+	cmd.Stdout = serverSocketFile
 	cmd.Stderr = os.Stderr
 	if err := cmd.Start(); err != nil {
 		t.Fatalf("failed to start Dovecot: %v", err)
 	}
 
-	return clientConn, &dovecotServer{cmd, serverConn}
+	cmdDone := make(chan struct{})
+	go func() {
+		defer close(cmdDone)
+		log.Print("BBB")
+		if err := cmd.Wait(); err != nil {
+			t.Fatalf("Dovecot failed: %v", err)
+		}
+		log.Print("CCC")
+	}()
+	t.Cleanup(func() {
+		<-cmdDone
+	})
+
+	log.Print("AAA")
+	return fileConn{clientSocketFile}, &dovecotServer{serverSocketFile}
 }
 
 type dovecotServer struct {
-	cmd  *exec.Cmd
-	conn net.Conn
+	io.Closer
 }
 
-func (srv *dovecotServer) Close() error {
-	if err := srv.conn.Close(); err != nil {
-		return err
+func createSocketPair() (*os.File, *os.File, error) {
+	fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM, 0)
+	if err != nil {
+		return nil, nil, err
 	}
-	return srv.cmd.Wait()
+	return os.NewFile(uintptr(fds[0]), "socketpair"), os.NewFile(uintptr(fds[1]), "socketpair"), nil
+}
+
+// fileConn behaves like net.FileConn, expect it closes the file.
+type fileConn struct {
+	*os.File
+}
+
+func (conn fileConn) LocalAddr() net.Addr {
+	return fileConnAddr{}
+}
+
+func (conn fileConn) RemoteAddr() net.Addr {
+	return fileConnAddr{}
+}
+
+type fileConnAddr struct{}
+
+func (fileConnAddr) Network() string {
+	return "file"
+}
+
+func (fileConnAddr) String() string {
+	return "file"
 }

emersion avatar Jul 08 '25 14:07 emersion