@@ -13,6 +13,7 @@ import (
13
13
"errors"
14
14
"fmt"
15
15
"io"
16
+ "io/fs"
16
17
"io/ioutil"
17
18
"net"
18
19
"net/http"
@@ -35,6 +36,7 @@ import (
35
36
"github.com/soheilhy/cmux"
36
37
"golang.org/x/crypto/ssh"
37
38
"golang.org/x/sys/unix"
39
+ "golang.org/x/xerrors"
38
40
"google.golang.org/grpc"
39
41
"google.golang.org/protobuf/proto"
40
42
@@ -278,6 +280,10 @@ func Run(options ...RunOption) {
278
280
// - in headless task we can not use reaper, because it breaks headlessTaskFailed report
279
281
if ! cfg .isHeadless () {
280
282
go reaper (terminatingReaper )
283
+
284
+ // We need to checkout dotfiles first, because they may be changing the path which affects the IDE.
285
+ // TODO(cw): provide better feedback if the IDE start fails because of the dotfiles (provide any feedback at all).
286
+ installDotfiles (ctx , termMuxSrv , cfg )
281
287
}
282
288
283
289
var ideWG sync.WaitGroup
@@ -364,6 +370,143 @@ func Run(options ...RunOption) {
364
370
wg .Wait ()
365
371
}
366
372
373
+ func installDotfiles (ctx context.Context , term * terminal.MuxTerminalService , cfg * Config ) {
374
+ repo := cfg .DotfileRepo
375
+ if repo == "" {
376
+ return
377
+ }
378
+
379
+ const dotfilePath = "/home/gitpod/.dotfiles"
380
+ if _ , err := os .Stat (dotfilePath ); err == nil {
381
+ // dotfile path exists already - nothing to do here
382
+ return
383
+ }
384
+
385
+ prep := func (cfg * Config , out io.Writer , name string , args ... string ) * exec.Cmd {
386
+ cmd := exec .Command (name , args ... )
387
+ cmd .Dir = "/home/gitpod"
388
+ cmd .Env = buildChildProcEnv (cfg , nil )
389
+ cmd .SysProcAttr = & syscall.SysProcAttr {
390
+ // All supervisor children run as gitpod user. The environment variables we produce are also
391
+ // gitpod user specific.
392
+ Credential : & syscall.Credential {
393
+ Uid : gitpodUID ,
394
+ Gid : gitpodGID ,
395
+ },
396
+ }
397
+ cmd .Stdout = out
398
+ cmd .Stderr = out
399
+ return cmd
400
+ }
401
+
402
+ err := func () (err error ) {
403
+ out , err := os .OpenFile ("/home/gitpod/.dotfiles.log" , os .O_CREATE | os .O_TRUNC | os .O_WRONLY , 0644 )
404
+ if err != nil {
405
+ return err
406
+ }
407
+ defer out .Close ()
408
+
409
+ defer func () {
410
+ if err != nil {
411
+ out .WriteString (fmt .Sprintf ("# dotfile init failed: %s\n " , err .Error ()))
412
+ }
413
+ }()
414
+
415
+ done := make (chan error , 1 )
416
+ go func () {
417
+ done <- prep (cfg , out , "git" , "clone" , "--depth=1" , repo , "/home/gitpod/.dotfiles" ).Run ()
418
+ close (done )
419
+ }()
420
+ select {
421
+ case err := <- done :
422
+ if err != nil {
423
+ return err
424
+ }
425
+ case <- time .After (120 * time .Second ):
426
+ return xerrors .Errorf ("dotfiles repo clone did not finish within two minutes" )
427
+ }
428
+
429
+ // at this point we have the dotfile repo cloned, let's try and install it
430
+ var candidates = []string {
431
+ "install.sh" ,
432
+ "install" ,
433
+ "bootstrap.sh" ,
434
+ "bootstrap" ,
435
+ "script/bootstrap" ,
436
+ "setup.sh" ,
437
+ "setup" ,
438
+ "script/setup" ,
439
+ }
440
+ for _ , c := range candidates {
441
+ fn := filepath .Join (dotfilePath , c )
442
+ stat , err := os .Stat (fn )
443
+ if err != nil {
444
+ _ , _ = out .WriteString (fmt .Sprintf ("# installation script candidate %s is not available\n " , fn ))
445
+ continue
446
+ }
447
+ if stat .IsDir () {
448
+ _ , _ = out .WriteString (fmt .Sprintf ("# installation script candidate %s is a directory\n " , fn ))
449
+ continue
450
+ }
451
+ if stat .Mode ()& 0111 == 0 {
452
+ _ , _ = out .WriteString (fmt .Sprintf ("# installation script candidate %s is not executable\n " , fn ))
453
+ continue
454
+ }
455
+
456
+ _ , _ = out .WriteString (fmt .Sprintf ("# executing installation script candidate %s\n " , fn ))
457
+
458
+ // looks like we've found a candidate, let's run it
459
+ cmd := prep (cfg , out , "/bin/sh" , "-c" , "exec " + fn )
460
+ err = cmd .Start ()
461
+ if err != nil {
462
+ return err
463
+ }
464
+ done := make (chan error , 1 )
465
+ go func () {
466
+ done <- cmd .Wait ()
467
+ close (done )
468
+ }()
469
+
470
+ select {
471
+ case err = <- done :
472
+ return err
473
+ case <- time .After (120 * time .Second ):
474
+ cmd .Process .Kill ()
475
+ return xerrors .Errorf ("installation process %s tool longer than 120 seconds" , fn )
476
+ }
477
+ }
478
+
479
+ // no installation script candidate was found, let's try and symlink this stuff
480
+ err = filepath .Walk (dotfilePath , func (path string , info fs.FileInfo , err error ) error {
481
+ homeFN := filepath .Join ("/home/gitpod" , strings .TrimPrefix (path , dotfilePath ))
482
+ if _ , err := os .Stat (homeFN ); err == nil {
483
+ // homeFN exists already - do nothing
484
+ return nil
485
+ }
486
+
487
+ if info .IsDir () {
488
+ err = os .MkdirAll (homeFN , info .Mode ().Perm ())
489
+ if err != nil {
490
+ return err
491
+ }
492
+ return nil
493
+ }
494
+
495
+ // write some feedback to the terminal
496
+ out .WriteString (fmt .Sprintf ("# echo linking %s -> %s\n " , path , homeFN ))
497
+
498
+ return os .Symlink (path , homeFN )
499
+ })
500
+
501
+ return nil
502
+ }()
503
+ if err != nil {
504
+ // installing the dotfiles failed for some reason - we must tell the user
505
+ // TODO(cw): tell the user
506
+ log .WithError (err ).Warn ("installing dotfiles failed" )
507
+ }
508
+ }
509
+
367
510
func createGitpodService (cfg * Config , tknsrv api.TokenServiceServer ) * gitpod.APIoverJSONRPC {
368
511
endpoint , host , err := cfg .GitpodAPIEndpoint ()
369
512
if err != nil {
@@ -1223,7 +1366,7 @@ func socketActivationForDocker(ctx context.Context, wg *sync.WaitGroup, term *te
1223
1366
cmd .ExtraFiles = []* os.File {socketFD }
1224
1367
alias , err := term .Start (cmd , terminal.TermOptions {
1225
1368
Annotations : map [string ]string {
1226
- "supervisor" : "true" ,
1369
+ "gitpod. supervisor" : "true" ,
1227
1370
},
1228
1371
LogToStdout : true ,
1229
1372
})
0 commit comments