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