@@ -199,72 +199,125 @@ func TestAgent_Stats_Magic(t *testing.T) {
199
199
if runtime .GOOS != "linux" {
200
200
t .Skip ("JetBrains tracking is only supported on Linux" )
201
201
}
202
+ tests := []struct {
203
+ name string
204
+ network string
205
+ address string
206
+ }{
207
+ {"IPV4" , "tcp4" , "127.0.0.1" },
208
+ {"IPV4Localhost" , "tcp4" , "localhost" },
209
+ {"IPV6" , "tcp6" , "[::1]" },
210
+ }
211
+ for _ , test := range tests {
212
+ test := test
213
+ t .Run (test .name , func (t * testing.T ) {
214
+ t .Parallel ()
215
+ verifyJetBrainsTracking (t , test .network , test .address )
216
+ })
217
+ }
218
+ })
219
+ }
202
220
203
- ctx := testutil .Context (t , testutil .WaitLong )
221
+ func spawnEchoServer (t * testing.T , args ... string ) (string , * exec.Cmd ) {
222
+ _ , b , _ , ok := runtime .Caller (0 )
223
+ require .True (t , ok )
224
+ dir := filepath .Join (filepath .Dir (b ), "../scripts/echoserver/main.go" )
225
+ //nolint:gosec
226
+ echoServerCmd := exec .Command ("go" , append ([]string {"run" , dir }, args ... )... )
204
227
205
- // JetBrains tracking works by looking at the process name listening on the
206
- // forwarded port. If the process's command line includes the magic string
207
- // we are looking for, then we assume it is a JetBrains editor. So when we
208
- // connect to the port we must ensure the process includes that magic string
209
- // to fool the agent into thinking this is JetBrains. To do this we need to
210
- // spawn an external process (in this case a simple echo server) so we can
211
- // control the process name. The -D here is just to mimic how Java options
212
- // are set but is not necessary as the agent looks only for the magic
213
- // string itself anywhere in the command.
214
- _ , b , _ , ok := runtime .Caller (0 )
215
- require .True (t , ok )
216
- dir := filepath .Join (filepath .Dir (b ), "../scripts/echoserver/main.go" )
217
- echoServerCmd := exec .Command ("go" , "run" , dir ,
218
- "-D" , agentssh .MagicProcessCmdlineJetBrains )
219
- stdout , err := echoServerCmd .StdoutPipe ()
220
- require .NoError (t , err )
221
- err = echoServerCmd .Start ()
222
- require .NoError (t , err )
223
- defer echoServerCmd .Process .Kill ()
228
+ stderr , err := echoServerCmd .StderrPipe ()
229
+ require .NoError (t , err )
230
+ go func () {
231
+ sc := bufio .NewScanner (stderr )
232
+ for sc .Scan () {
233
+ t .Logf ("echo server stderr: %s" , sc .Text ())
234
+ }
235
+ t .Logf ("echo server exited" )
236
+ }()
224
237
225
- // The echo server prints its port as the first line.
226
- sc := bufio .NewScanner (stdout )
227
- sc .Scan ()
228
- remotePort := sc .Text ()
238
+ stdout , err := echoServerCmd .StdoutPipe ()
239
+ require .NoError (t , err )
240
+ err = echoServerCmd .Start ()
241
+ require .NoError (t , err )
242
+ t .Cleanup (func () {
243
+ _ = stderr .Close ()
244
+ _ = echoServerCmd .Process .Kill ()
245
+ })
229
246
230
- //nolint:dogsled
231
- conn , _ , stats , _ , _ := setupAgent ( t , agentsdk. Manifest {}, 0 )
232
- sshClient , err := conn . SSHClient ( ctx )
233
- require . NoError ( t , err )
247
+ // The echo server prints its port as the first line.
248
+ sc := bufio . NewScanner ( stdout )
249
+ sc . Scan ( )
250
+ remotePort := sc . Text ( )
234
251
235
- tunneledConn , err := sshClient .Dial ("tcp" , fmt .Sprintf ("127.0.0.1:%s" , remotePort ))
236
- require .NoError (t , err )
237
- t .Cleanup (func () {
238
- // always close on failure of test
239
- _ = conn .Close ()
240
- _ = tunneledConn .Close ()
241
- })
252
+ return remotePort , echoServerCmd
253
+ }
242
254
243
- require .Eventuallyf (t , func () bool {
244
- s , ok := <- stats
245
- t .Logf ("got stats with conn open: ok=%t, ConnectionCount=%d, SessionCountJetBrains=%d" ,
246
- ok , s .ConnectionCount , s .SessionCountJetBrains )
247
- return ok && s .ConnectionCount > 0 &&
248
- s .SessionCountJetBrains == 1
249
- }, testutil .WaitLong , testutil .IntervalFast ,
250
- "never saw stats with conn open" ,
251
- )
255
+ func verifyJetBrainsTracking (t * testing.T , network , address string ) {
256
+ ctx := testutil .Context (t , testutil .WaitLong )
252
257
253
- // Kill the server and connection after checking for the echo.
254
- requireEcho (t , tunneledConn )
255
- _ = echoServerCmd . Process . Kill ( )
256
- _ = tunneledConn . Close ( )
258
+ //nolint:dogsled
259
+ conn , _ , stats , _ , _ := setupAgent (t , agentsdk. Manifest {}, 0 )
260
+ sshClient , err := conn . SSHClient ( ctx )
261
+ require . NoError ( t , err )
257
262
258
- require .Eventuallyf (t , func () bool {
259
- s , ok := <- stats
260
- t .Logf ("got stats after disconnect %t, %d" ,
261
- ok , s .SessionCountJetBrains )
262
- return ok &&
263
- s .SessionCountJetBrains == 0
264
- }, testutil .WaitLong , testutil .IntervalFast ,
265
- "never saw stats after conn closes" ,
266
- )
263
+ // On Linux it is permissible to listen on the same port on different
264
+ // addresses, and when this happens `localhost` can route to the ipv6 loopback
265
+ // address instead of the ipv4 address. Since we make requests to coderd with
266
+ // `localhost` this behavior can cause those requests to go to the ipv6 echo
267
+ // server instead of coderd when there is a port collision. Listening on ipv4
268
+ // here occupies the port, preventing coderd from using it and running into
269
+ // this issue.
270
+ port := "0"
271
+ if network == "tcp6" {
272
+ port , _ = spawnEchoServer (t , "tcp4" , "0" )
273
+ }
274
+
275
+ // JetBrains tracking works by looking at the process name listening on
276
+ // the forwarded port. If the process's command line includes the magic
277
+ // string we are looking for, then we assume it is a JetBrains editor.
278
+ // So when we connect to the port we must ensure the process includes
279
+ // that magic string to fool the agent into thinking this is JetBrains.
280
+ // To do this we need to spawn an external process (in this case a
281
+ // simple echo server) so we can control the process name. The -D here
282
+ // is just to mimic how Java options are set but is not necessary as the
283
+ // agent looks only for the magic string itself anywhere in the command.
284
+ gotPort , echoServerCmd := spawnEchoServer (t , network , port , "-D" , agentssh .MagicProcessCmdlineJetBrains )
285
+ if port != "0" {
286
+ require .Equal (t , gotPort , port ) // Sanity check.
287
+ }
288
+
289
+ tunneledConn , err := sshClient .Dial (network , fmt .Sprintf ("%s:%s" , address , gotPort ))
290
+ require .NoError (t , err )
291
+ t .Cleanup (func () {
292
+ // always close on failure of test
293
+ _ = conn .Close ()
294
+ _ = tunneledConn .Close ()
267
295
})
296
+
297
+ require .Eventuallyf (t , func () bool {
298
+ s , ok := <- stats
299
+ t .Logf ("got stats with conn open: ok=%t, ConnectionCount=%d, SessionCountJetBrains=%d" ,
300
+ ok , s .ConnectionCount , s .SessionCountJetBrains )
301
+ return ok && s .ConnectionCount > 0 &&
302
+ s .SessionCountJetBrains == 1
303
+ }, testutil .WaitLong , testutil .IntervalFast ,
304
+ "never saw stats with conn open" ,
305
+ )
306
+
307
+ // Kill the server and connection after checking for the echo.
308
+ requireEcho (t , tunneledConn )
309
+ _ = echoServerCmd .Process .Kill ()
310
+ _ = tunneledConn .Close ()
311
+
312
+ require .Eventuallyf (t , func () bool {
313
+ s , ok := <- stats
314
+ t .Logf ("got stats after disconnect %t, %d" ,
315
+ ok , s .SessionCountJetBrains )
316
+ return ok &&
317
+ s .SessionCountJetBrains == 0
318
+ }, testutil .WaitLong , testutil .IntervalFast ,
319
+ "never saw stats after conn closes" ,
320
+ )
268
321
}
269
322
270
323
func TestAgent_SessionExec (t * testing.T ) {
0 commit comments