github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/e2e/cli-plugins/socket_test.go (about) 1 package cliplugins 2 3 import ( 4 "bytes" 5 "io" 6 "os/exec" 7 "strings" 8 "syscall" 9 "testing" 10 "time" 11 12 "github.com/creack/pty" 13 "gotest.tools/v3/assert" 14 ) 15 16 // TestPluginSocketBackwardsCompatible executes a plugin binary 17 // that does not connect to the CLI plugin socket, simulating 18 // a plugin compiled against an older version of the CLI, and 19 // ensures that backwards compatibility is maintained. 20 func TestPluginSocketBackwardsCompatible(t *testing.T) { 21 run, _, cleanup := prepare(t) 22 defer cleanup() 23 24 t.Run("attached", func(t *testing.T) { 25 t.Run("the plugin gets signalled if attached to a TTY", func(t *testing.T) { 26 cmd := run("presocket", "test-no-socket") 27 command := exec.Command(cmd.Command[0], cmd.Command[1:]...) 28 29 ptmx, err := pty.Start(command) 30 assert.NilError(t, err, "failed to launch command with fake TTY") 31 32 // send a SIGINT to the process group after 1 second, since 33 // we're simulating an "attached TTY" scenario and a TTY would 34 // send a signal to the process group 35 go func() { 36 <-time.After(time.Second) 37 err := syscall.Kill(-command.Process.Pid, syscall.SIGINT) 38 assert.NilError(t, err, "failed to signal process group") 39 }() 40 bytes, err := io.ReadAll(ptmx) 41 if err != nil && !strings.Contains(err.Error(), "input/output error") { 42 t.Fatal("failed to get command output") 43 } 44 45 // the plugin is attached to the TTY, so the parent process 46 // ignores the received signal, and the plugin receives a SIGINT 47 // as well 48 assert.Equal(t, string(bytes), "received SIGINT\r\nexit after 3 seconds\r\n") 49 }) 50 51 // ensure that we don't break plugins that attempt to read from the TTY 52 // (see: https://github.com/moby/moby/issues/47073) 53 // (remove me if/when we decide to break compatibility here) 54 t.Run("the plugin can read from the TTY", func(t *testing.T) { 55 cmd := run("presocket", "tty") 56 command := exec.Command(cmd.Command[0], cmd.Command[1:]...) 57 58 ptmx, err := pty.Start(command) 59 assert.NilError(t, err, "failed to launch command with fake TTY") 60 _, _ = ptmx.Write([]byte("hello!")) 61 62 done := make(chan error) 63 go func() { 64 <-time.After(time.Second) 65 _, err := io.ReadAll(ptmx) 66 done <- err 67 }() 68 69 select { 70 case cmdErr := <-done: 71 if cmdErr != nil && !strings.Contains(cmdErr.Error(), "input/output error") { 72 t.Fatal("failed to get command output") 73 } 74 case <-time.After(5 * time.Second): 75 t.Fatal("timed out! plugin process probably stuck") 76 } 77 }) 78 }) 79 80 t.Run("detached", func(t *testing.T) { 81 t.Run("the plugin does not get signalled", func(t *testing.T) { 82 cmd := run("presocket", "test-no-socket") 83 command := exec.Command(cmd.Command[0], cmd.Command[1:]...) 84 t.Log(strings.Join(command.Args, " ")) 85 command.SysProcAttr = &syscall.SysProcAttr{ 86 Setpgid: true, 87 } 88 89 go func() { 90 <-time.After(time.Second) 91 // we're signalling the parent process directly and not 92 // the process group, since we're testing the case where 93 // the process is detached and not simulating a CTRL-C 94 // from a TTY 95 err := syscall.Kill(command.Process.Pid, syscall.SIGINT) 96 assert.NilError(t, err, "failed to signal process group") 97 }() 98 bytes, err := command.CombinedOutput() 99 t.Log("command output: " + string(bytes)) 100 assert.NilError(t, err, "failed to run command") 101 102 // the plugin process does not receive a SIGINT 103 // so it exits after 3 seconds and prints this message 104 assert.Equal(t, string(bytes), "exit after 3 seconds\n") 105 }) 106 107 t.Run("the main CLI exits after 3 signals", func(t *testing.T) { 108 cmd := run("presocket", "test-no-socket") 109 command := exec.Command(cmd.Command[0], cmd.Command[1:]...) 110 t.Log(strings.Join(command.Args, " ")) 111 command.SysProcAttr = &syscall.SysProcAttr{ 112 Setpgid: true, 113 } 114 115 go func() { 116 <-time.After(time.Second) 117 // we're signalling the parent process directly and not 118 // the process group, since we're testing the case where 119 // the process is detached and not simulating a CTRL-C 120 // from a TTY 121 err := syscall.Kill(command.Process.Pid, syscall.SIGINT) 122 assert.NilError(t, err, "failed to signal process group") 123 // TODO: look into CLI signal handling, it's currently necessary 124 // to add a short delay between each signal in order for the CLI 125 // process to consistently pick them all up. 126 time.Sleep(50 * time.Millisecond) 127 err = syscall.Kill(command.Process.Pid, syscall.SIGINT) 128 assert.NilError(t, err, "failed to signal process group") 129 time.Sleep(50 * time.Millisecond) 130 err = syscall.Kill(command.Process.Pid, syscall.SIGINT) 131 assert.NilError(t, err, "failed to signal process group") 132 }() 133 bytes, err := command.CombinedOutput() 134 assert.ErrorContains(t, err, "exit status 1") 135 136 // the plugin process does not receive a SIGINT and does 137 // the CLI cannot cancel it over the socket, so it kills 138 // the plugin process and forcefully exits 139 assert.Equal(t, string(bytes), "got 3 SIGTERM/SIGINTs, forcefully exiting\n") 140 }) 141 }) 142 } 143 144 func TestPluginSocketCommunication(t *testing.T) { 145 run, _, cleanup := prepare(t) 146 defer cleanup() 147 148 t.Run("attached", func(t *testing.T) { 149 t.Run("the socket is not closed + the plugin receives a signal due to pgid", func(t *testing.T) { 150 cmd := run("presocket", "test-socket") 151 command := exec.Command(cmd.Command[0], cmd.Command[1:]...) 152 153 ptmx, err := pty.Start(command) 154 assert.NilError(t, err, "failed to launch command with fake TTY") 155 156 // send a SIGINT to the process group after 1 second, since 157 // we're simulating an "attached TTY" scenario and a TTY would 158 // send a signal to the process group 159 go func() { 160 <-time.After(time.Second) 161 err := syscall.Kill(-command.Process.Pid, syscall.SIGINT) 162 assert.NilError(t, err, "failed to signal process group") 163 }() 164 bytes, err := io.ReadAll(ptmx) 165 if err != nil && !strings.Contains(err.Error(), "input/output error") { 166 t.Fatal("failed to get command output") 167 } 168 169 // the plugin is attached to the TTY, so the parent process 170 // ignores the received signal, and the plugin receives a SIGINT 171 // as well 172 assert.Equal(t, string(bytes), "received SIGINT\r\nexit after 3 seconds\r\n") 173 }) 174 }) 175 176 t.Run("detached", func(t *testing.T) { 177 t.Run("the plugin does not get signalled", func(t *testing.T) { 178 cmd := run("presocket", "test-socket") 179 command := exec.Command(cmd.Command[0], cmd.Command[1:]...) 180 outB := bytes.Buffer{} 181 command.Stdout = &outB 182 command.Stderr = &outB 183 command.SysProcAttr = &syscall.SysProcAttr{ 184 Setpgid: true, 185 } 186 187 // send a SIGINT to the process group after 1 second 188 go func() { 189 <-time.After(time.Second) 190 err := syscall.Kill(command.Process.Pid, syscall.SIGINT) 191 assert.NilError(t, err, "failed to signal CLI process") 192 }() 193 err := command.Run() 194 t.Log(outB.String()) 195 assert.ErrorContains(t, err, "exit status 2") 196 197 // the plugin does not get signalled, but it does get it's 198 // context cancelled by the CLI through the socket 199 assert.Equal(t, outB.String(), "context cancelled\n") 200 }) 201 202 t.Run("the main CLI exits after 3 signals", func(t *testing.T) { 203 cmd := run("presocket", "test-socket-ignore-context") 204 command := exec.Command(cmd.Command[0], cmd.Command[1:]...) 205 command.SysProcAttr = &syscall.SysProcAttr{ 206 Setpgid: true, 207 } 208 209 go func() { 210 <-time.After(time.Second) 211 // we're signalling the parent process directly and not 212 // the process group, since we're testing the case where 213 // the process is detached and not simulating a CTRL-C 214 // from a TTY 215 err := syscall.Kill(command.Process.Pid, syscall.SIGINT) 216 assert.NilError(t, err, "failed to signal CLI process") 217 // TODO: same as above TODO, CLI signal handling is not consistent 218 // with multiple signals without intervals 219 time.Sleep(50 * time.Millisecond) 220 err = syscall.Kill(command.Process.Pid, syscall.SIGINT) 221 assert.NilError(t, err, "failed to signal CLI process") 222 time.Sleep(50 * time.Millisecond) 223 err = syscall.Kill(command.Process.Pid, syscall.SIGINT) 224 assert.NilError(t, err, "failed to signal CLI processĀ§") 225 }() 226 bytes, err := command.CombinedOutput() 227 assert.ErrorContains(t, err, "exit status 1") 228 229 // the plugin process does not receive a SIGINT and does 230 // not exit after having it's context cancelled, so the CLI 231 // kills the plugin process and forcefully exits 232 assert.Equal(t, string(bytes), "got 3 SIGTERM/SIGINTs, forcefully exiting\n") 233 }) 234 }) 235 }