@@ -56,6 +56,7 @@ const (
56
56
57
57
type Options struct {
58
58
Filesystem afero.Fs
59
+ TempDir string
59
60
ExchangeToken func (ctx context.Context ) (string , error )
60
61
Client Client
61
62
ReconnectingPTYTimeout time.Duration
@@ -78,6 +79,9 @@ func New(options Options) io.Closer {
78
79
if options .Filesystem == nil {
79
80
options .Filesystem = afero .NewOsFs ()
80
81
}
82
+ if options .TempDir == "" {
83
+ options .TempDir = os .TempDir ()
84
+ }
81
85
if options .ExchangeToken == nil {
82
86
options .ExchangeToken = func (ctx context.Context ) (string , error ) {
83
87
return "" , nil
@@ -93,6 +97,7 @@ func New(options Options) io.Closer {
93
97
client : options .Client ,
94
98
exchangeToken : options .ExchangeToken ,
95
99
filesystem : options .Filesystem ,
100
+ tempDir : options .TempDir ,
96
101
stats : & Stats {},
97
102
}
98
103
server .init (ctx )
@@ -104,6 +109,7 @@ type agent struct {
104
109
client Client
105
110
exchangeToken func (ctx context.Context ) (string , error )
106
111
filesystem afero.Fs
112
+ tempDir string
107
113
108
114
reconnectingPTYs sync.Map
109
115
reconnectingPTYTimeout time.Duration
@@ -168,7 +174,7 @@ func (a *agent) run(ctx context.Context) error {
168
174
if err != nil {
169
175
return xerrors .Errorf ("fetch metadata: %w" , err )
170
176
}
171
- a .logger .Info (context . Background () , "fetched metadata" )
177
+ a .logger .Info (ctx , "fetched metadata" )
172
178
oldMetadata := a .metadata .Swap (metadata )
173
179
174
180
// The startup script should only execute on the first run!
@@ -202,7 +208,7 @@ func (a *agent) run(ctx context.Context) error {
202
208
a .closeMutex .Lock ()
203
209
network := a .network
204
210
a .closeMutex .Unlock ()
205
- if a . network == nil {
211
+ if network == nil {
206
212
a .logger .Debug (ctx , "creating tailnet" )
207
213
network , err = a .createTailnet (ctx , metadata .DERPMap )
208
214
if err != nil {
@@ -359,7 +365,7 @@ func (a *agent) runCoordinator(ctx context.Context, network *tailnet.Conn) error
359
365
return err
360
366
}
361
367
defer coordinator .Close ()
362
- a .logger .Info (context . Background () , "connected to coordination server" )
368
+ a .logger .Info (ctx , "connected to coordination server" )
363
369
sendNodes , errChan := tailnet .ServeCoordinator (coordinator , network .UpdateNodes )
364
370
network .SetNodeCallback (sendNodes )
365
371
select {
@@ -375,14 +381,14 @@ func (a *agent) runStartupScript(ctx context.Context, script string) error {
375
381
return nil
376
382
}
377
383
378
- writer , err := os .OpenFile (filepath .Join (os .TempDir (), "coder-startup-script.log" ), os .O_CREATE | os .O_RDWR , 0o600 )
384
+ a .logger .Info (ctx , "running startup script" , slog .F ("script" , script ))
385
+ writer , err := a .filesystem .OpenFile (filepath .Join (a .tempDir , "coder-startup-script.log" ), os .O_CREATE | os .O_RDWR , 0o600 )
379
386
if err != nil {
380
387
return xerrors .Errorf ("open startup script log file: %w" , err )
381
388
}
382
389
defer func () {
383
390
_ = writer .Close ()
384
391
}()
385
-
386
392
cmd , err := a .createCommand (ctx , script , nil )
387
393
if err != nil {
388
394
return xerrors .Errorf ("create command: %w" , err )
@@ -470,6 +476,12 @@ func (a *agent) init(ctx context.Context) {
470
476
},
471
477
SubsystemHandlers : map [string ]ssh.SubsystemHandler {
472
478
"sftp" : func (session ssh.Session ) {
479
+ ctx := session .Context ()
480
+
481
+ // Typically sftp sessions don't request a TTY, but if they do,
482
+ // we must ensure the gliderlabs/ssh CRLF emulation is disabled.
483
+ // Otherwise sftp will be broken. This can happen if a user sets
484
+ // `RequestTTY force` in their SSH config.
473
485
session .DisablePTYEmulation ()
474
486
475
487
var opts []sftp.ServerOption
@@ -478,22 +490,33 @@ func (a *agent) init(ctx context.Context) {
478
490
// https://github.com/coder/coder/issues/3620
479
491
u , err := user .Current ()
480
492
if err != nil {
481
- a . logger .Warn (ctx , "get sftp working directory failed, unable to get current user" , slog .Error (err ))
493
+ sshLogger .Warn (ctx , "get sftp working directory failed, unable to get current user" , slog .Error (err ))
482
494
} else {
483
495
opts = append (opts , sftp .WithServerWorkingDirectory (u .HomeDir ))
484
496
}
485
497
486
498
server , err := sftp .NewServer (session , opts ... )
487
499
if err != nil {
488
- a . logger . Debug (session . Context () , "initialize sftp server" , slog .Error (err ))
500
+ sshLogger . Debug (ctx , "initialize sftp server" , slog .Error (err ))
489
501
return
490
502
}
491
503
defer server .Close ()
504
+
492
505
err = server .Serve ()
493
506
if errors .Is (err , io .EOF ) {
507
+ // Unless we call `session.Exit(0)` here, the client won't
508
+ // receive `exit-status` because `(*sftp.Server).Close()`
509
+ // calls `Close()` on the underlying connection (session),
510
+ // which actually calls `channel.Close()` because it isn't
511
+ // wrapped. This causes sftp clients to receive a non-zero
512
+ // exit code. Typically sftp clients don't echo this exit
513
+ // code but `scp` on macOS does (when using the default
514
+ // SFTP backend).
515
+ _ = session .Exit (0 )
494
516
return
495
517
}
496
- a .logger .Debug (session .Context (), "sftp server exited with error" , slog .Error (err ))
518
+ sshLogger .Warn (ctx , "sftp server closed with error" , slog .Error (err ))
519
+ _ = session .Exit (1 )
497
520
},
498
521
},
499
522
}
@@ -538,25 +561,26 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri
538
561
return nil , xerrors .Errorf ("metadata is the wrong type: %T" , metadata )
539
562
}
540
563
564
+ // OpenSSH executes all commands with the users current shell.
565
+ // We replicate that behavior for IDE support.
566
+ caller := "-c"
567
+ if runtime .GOOS == "windows" {
568
+ caller = "/c"
569
+ }
570
+ args := []string {caller , rawCommand }
571
+
541
572
// gliderlabs/ssh returns a command slice of zero
542
573
// when a shell is requested.
543
- command := rawCommand
544
- if len (command ) == 0 {
545
- command = shell
574
+ if len (rawCommand ) == 0 {
575
+ args = []string {}
546
576
if runtime .GOOS != "windows" {
547
577
// On Linux and macOS, we should start a login
548
578
// shell to consume juicy environment variables!
549
- command += " -l"
579
+ args = append ( args , " -l")
550
580
}
551
581
}
552
582
553
- // OpenSSH executes all commands with the users current shell.
554
- // We replicate that behavior for IDE support.
555
- caller := "-c"
556
- if runtime .GOOS == "windows" {
557
- caller = "/c"
558
- }
559
- cmd := exec .CommandContext (ctx , shell , caller , command )
583
+ cmd := exec .CommandContext (ctx , shell , args ... )
560
584
cmd .Dir = metadata .Directory
561
585
if cmd .Dir == "" {
562
586
// Default to $HOME if a directory is not set!
0 commit comments