@@ -7,12 +7,16 @@ import (
7
7
"errors"
8
8
"fmt"
9
9
"net/http"
10
+ "time"
10
11
12
+ "cdr.dev/slog"
11
13
"github.com/go-chi/chi/v5"
12
14
"github.com/google/uuid"
13
15
"github.com/moby/moby/pkg/namesgenerator"
14
16
"golang.org/x/sync/errgroup"
15
17
"golang.org/x/xerrors"
18
+ "nhooyr.io/websocket"
19
+ "nhooyr.io/websocket/wsjson"
16
20
17
21
"github.com/coder/coder/coderd/autobuild/schedule"
18
22
"github.com/coder/coder/coderd/database"
@@ -496,6 +500,94 @@ func (api *api) putWorkspaceAutostop(rw http.ResponseWriter, r *http.Request) {
496
500
}
497
501
}
498
502
503
+ func (api * api ) watchWorkspace (rw http.ResponseWriter , r * http.Request ) {
504
+ workspace := httpmw .WorkspaceParam (r )
505
+
506
+ c , err := websocket .Accept (rw , r , & websocket.AcceptOptions {
507
+ // Fix for Safari 15.1:
508
+ // There is a bug in latest Safari in which compressed web socket traffic
509
+ // isn't handled correctly. Turning off compression is a workaround:
510
+ // https://github.com/nhooyr/websocket/issues/218
511
+ CompressionMode : websocket .CompressionDisabled ,
512
+ })
513
+ if err != nil {
514
+ api .Logger .Warn (r .Context (), "accept websocket connection" , slog .Error (err ))
515
+ return
516
+ }
517
+ defer c .Close (websocket .StatusInternalError , "internal error" )
518
+
519
+ ctx := c .CloseRead (r .Context ())
520
+
521
+ // Send a heartbeat every 15 seconds to avoid the websocket being killed.
522
+ go func () {
523
+ ticker := time .NewTicker (time .Second * 15 )
524
+ defer ticker .Stop ()
525
+
526
+ for {
527
+ select {
528
+ case <- ctx .Done ():
529
+ return
530
+ case <- ticker .C :
531
+ err := c .Ping (ctx )
532
+ if err != nil {
533
+ return
534
+ }
535
+ }
536
+ }
537
+ }()
538
+
539
+ t := time .NewTicker (time .Second * 1 )
540
+ defer t .Stop ()
541
+ for {
542
+ select {
543
+ case <- t .C :
544
+ workspace , err := api .Database .GetWorkspaceByID (r .Context (), workspace .ID )
545
+ if err != nil {
546
+ _ = wsjson .Write (ctx , c , httpapi.Response {
547
+ Message : fmt .Sprintf ("get workspace: %s" , err ),
548
+ })
549
+ return
550
+ }
551
+ build , err := api .Database .GetWorkspaceBuildByWorkspaceIDWithoutAfter (r .Context (), workspace .ID )
552
+ if err != nil {
553
+ _ = wsjson .Write (ctx , c , httpapi.Response {
554
+ Message : fmt .Sprintf ("get workspace build: %s" , err ),
555
+ })
556
+ return
557
+ }
558
+ var (
559
+ group errgroup.Group
560
+ job database.ProvisionerJob
561
+ template database.Template
562
+ owner database.User
563
+ )
564
+ group .Go (func () (err error ) {
565
+ job , err = api .Database .GetProvisionerJobByID (r .Context (), build .JobID )
566
+ return err
567
+ })
568
+ group .Go (func () (err error ) {
569
+ template , err = api .Database .GetTemplateByID (r .Context (), workspace .TemplateID )
570
+ return err
571
+ })
572
+ group .Go (func () (err error ) {
573
+ owner , err = api .Database .GetUserByID (r .Context (), workspace .OwnerID )
574
+ return err
575
+ })
576
+ err = group .Wait ()
577
+ if err != nil {
578
+ _ = wsjson .Write (ctx , c , httpapi.Response {
579
+ Message : fmt .Sprintf ("fetch resource: %s" , err ),
580
+ })
581
+ return
582
+ }
583
+
584
+ _ = wsjson .Write (ctx , c , convertWorkspace (workspace , convertWorkspaceBuild (build , convertProvisionerJob (job )), template , owner ))
585
+ case <- ctx .Done ():
586
+ return
587
+ }
588
+ }
589
+ }
590
+
499
591
func convertWorkspaces (ctx context.Context , db database.Store , workspaces []database.Workspace ) ([]codersdk.Workspace , error ) {
500
592
workspaceIDs := make ([]uuid.UUID , 0 , len (workspaces ))
501
593
templateIDs := make ([]uuid.UUID , 0 , len (workspaces ))
0 commit comments