@@ -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"
@@ -535,6 +539,94 @@ func (api *api) putWorkspaceAutostop(rw http.ResponseWriter, r *http.Request) {
535
539
}
536
540
}
537
541
542
+ func (api * api ) watchWorkspace (rw http.ResponseWriter , r * http.Request ) {
543
+ workspace := httpmw .WorkspaceParam (r )
544
+
545
+ c , err := websocket .Accept (rw , r , & websocket.AcceptOptions {
546
+ // Fix for Safari 15.1:
547
+ // There is a bug in latest Safari in which compressed web socket traffic
548
+ // isn't handled correctly. Turning off compression is a workaround:
549
+ // https://github.com/nhooyr/websocket/issues/218
550
+ CompressionMode : websocket .CompressionDisabled ,
551
+ })
552
+ if err != nil {
553
+ api .Logger .Warn (r .Context (), "accept websocket connection" , slog .Error (err ))
554
+ return
555
+ }
556
+ defer c .Close (websocket .StatusInternalError , "internal error" )
557
+
558
+ ctx := c .CloseRead (r .Context ())
559
+
560
+ // Send a heartbeat every 15 seconds to avoid the websocket being killed.
561
+ go func () {
562
+ ticker := time .NewTicker (time .Second * 15 )
563
+ defer ticker .Stop ()
564
+
565
+ for {
566
+ select {
567
+ case <- ctx .Done ():
568
+ return
569
+ case <- ticker .C :
570
+ err := c .Ping (ctx )
571
+ if err != nil {
572
+ return
573
+ }
574
+ }
575
+ }
576
+ }()
577
+
578
+ t := time .NewTicker (time .Second * 1 )
579
+ defer t .Stop ()
580
+ for {
581
+ select {
582
+ case <- t .C :
583
+ workspace , err := api .Database .GetWorkspaceByID (r .Context (), workspace .ID )
584
+ if err != nil {
585
+ _ = wsjson .Write (ctx , c , httpapi.Response {
586
+ Message : fmt .Sprintf ("get workspace: %s" , err ),
587
+ })
588
+ return
589
+ }
590
+ build , err := api .Database .GetWorkspaceBuildByWorkspaceIDWithoutAfter (r .Context (), workspace .ID )
591
+ if err != nil {
592
+ _ = wsjson .Write (ctx , c , httpapi.Response {
593
+ Message : fmt .Sprintf ("get workspace build: %s" , err ),
594
+ })
595
+ return
596
+ }
597
+ var (
598
+ group errgroup.Group
599
+ job database.ProvisionerJob
600
+ template database.Template
601
+ owner database.User
602
+ )
603
+ group .Go (func () (err error ) {
604
+ job , err = api .Database .GetProvisionerJobByID (r .Context (), build .JobID )
605
+ return err
606
+ })
607
+ group .Go (func () (err error ) {
608
+ template , err = api .Database .GetTemplateByID (r .Context (), workspace .TemplateID )
609
+ return err
610
+ })
611
+ group .Go (func () (err error ) {
612
+ owner , err = api .Database .GetUserByID (r .Context (), workspace .OwnerID )
613
+ return err
614
+ })
615
+ err = group .Wait ()
616
+ if err != nil {
617
+ _ = wsjson .Write (ctx , c , httpapi.Response {
618
+ Message : fmt .Sprintf ("fetch resource: %s" , err ),
619
+ })
620
+ return
621
+ }
622
+
623
+ _ = wsjson .Write (ctx , c , convertWorkspace (workspace , convertWorkspaceBuild (build , convertProvisionerJob (job )), template , owner ))
624
+ case <- ctx .Done ():
625
+ return
626
+ }
627
+ }
628
+ }
629
+
538
630
func convertWorkspaces (ctx context.Context , db database.Store , workspaces []database.Workspace ) ([]codersdk.Workspace , error ) {
539
631
workspaceIDs := make ([]uuid.UUID , 0 , len (workspaces ))
540
632
templateIDs := make ([]uuid.UUID , 0 , len (workspaces ))
0 commit comments