github.phpd.cn/thought-machine/please@v12.2.0+incompatible/src/follow/grpc_client.go (about) 1 // +build !bootstrap 2 3 package follow 4 5 import ( 6 "fmt" 7 "io" 8 "time" 9 10 "golang.org/x/net/context" 11 "google.golang.org/grpc" 12 "google.golang.org/grpc/codes" 13 "google.golang.org/grpc/status" 14 15 "core" 16 pb "follow/proto/build_event" 17 "output" 18 ) 19 20 // Used to track the state of the remote connection. 21 var remoteClosed, remoteDisconnected bool 22 23 // ConnectClient connects a gRPC client to the given URL. 24 // It returns once the client has received that the remote build finished, 25 // returning true if that build was successful. 26 // It dies on any errors. 27 func ConnectClient(state *core.BuildState, url string, retries int, delay time.Duration) bool { 28 connectClient(state, url, retries, delay) 29 // Now run output, this will exit when the goroutine in connectClient() hits its end. 30 return runOutput(state) 31 } 32 33 // connectClient connects a gRPC client to the given URL. 34 // It is split out of the above for testing purposes. 35 func connectClient(state *core.BuildState, url string, retries int, delay time.Duration) { 36 var err error 37 for i := 0; i <= retries; i++ { 38 if err = connectSingleTry(state, url); err == nil { 39 return 40 } else if retries > 0 && i < retries { 41 log.Warning("Failed to connect to remote server, will retry in %s: %s", delay, err) 42 time.Sleep(delay) 43 } 44 } 45 log.Fatalf("%s", err) 46 } 47 48 // connectSingleTry performs one try at connecting the gRPC client. 49 func connectSingleTry(state *core.BuildState, url string) error { 50 // TODO(peterebden): TLS 51 conn, err := grpc.Dial(url, grpc.WithInsecure()) 52 if err != nil { 53 return err 54 } 55 client := pb.NewPlzEventsClient(conn) 56 // Get the deets of what the server is doing. 57 resp, err := client.ServerConfig(context.Background(), &pb.ServerConfigRequest{}) 58 if err != nil { 59 if s, ok := status.FromError(err); ok && s.Code() == codes.Unavailable { 60 // Slightly nicer version for an obvious failure which gets a bit technical by default 61 return fmt.Errorf("Failed to set up communication with remote server; check the address is correct and it's running") 62 } 63 return fmt.Errorf("Failed to set up communication with remote server: %s", err) 64 } 65 // Let the user know we're connected now and what it's up to. 66 output.PrintConnectionMessage(url, fromProtoBuildLabels(resp.OriginalTargets), resp.Tests, resp.Coverage) 67 // Update the config appropriately; the output code reads some of its fields. 68 state.Config.Please.NumThreads = int(resp.NumThreads) 69 state.NeedBuild = false // We're not actually building ourselves 70 state.NeedTests = resp.Tests 71 state.NeedCoverage = resp.Coverage 72 state.StartTime = time.Unix(0, resp.StartTime) 73 state.Config.Display.SystemStats = true 74 // Catch up on the last result of each thread 75 log.Info("Got %d initial build events, dispatching...", len(resp.LastEvents)) 76 for _, r := range resp.LastEvents { 77 streamEvent(state, r) 78 } 79 80 // Now start streaming events into it 81 stream, err := client.BuildEvents(context.Background(), &pb.BuildEventRequest{}) 82 if err != nil { 83 return fmt.Errorf("Error receiving build events: %s", err) 84 } 85 go func() { 86 for { 87 event, err := stream.Recv() 88 if err == io.EOF { 89 remoteClosed = true 90 break 91 } else if err != nil { 92 remoteDisconnected = true 93 log.Error("Error receiving build events: %s", err) 94 break 95 } 96 streamEvent(state, event) 97 } 98 log.Info("Reached end of server event stream, shutting down internal queue") 99 close(state.Results) 100 }() 101 log.Info("Established connection to remote server for build events") 102 // Stream back resource usage as well 103 go streamResources(state, client) 104 return nil 105 } 106 107 // streamEvent adds an event to our internal stream. 108 func streamEvent(state *core.BuildState, event *pb.BuildEventResponse) { 109 e := fromProto(event) 110 // Put a version of this target into our graph, it will help a lot of things later. 111 if t := state.Graph.Target(e.Label); t == nil { 112 t = state.Graph.AddTarget(core.NewBuildTarget(e.Label)) 113 t.Labels = event.Labels 114 } 115 state.Results <- e 116 state.SetTaskNumbers(event.NumActive, event.NumDone) 117 } 118 119 // streamResources receives system resource usage information from the server and copies 120 // them into the build state. 121 func streamResources(state *core.BuildState, client pb.PlzEventsClient) { 122 stream, err := client.ResourceUsage(context.Background(), &pb.ResourceUsageRequest{}) 123 if err != nil { 124 log.Error("Error receiving resource usage: %s", err) 125 return 126 } 127 for { 128 resources, err := stream.Recv() 129 if err == io.EOF { 130 break 131 } else if err != nil { 132 log.Error("Error receiving resource usage: %s", err) 133 break 134 } 135 state.Stats = resourceFromProto(resources) 136 } 137 } 138 139 // runOutput is just a wrapper around output.MonitorState for convenience in testing. 140 func runOutput(state *core.BuildState) bool { 141 success := output.MonitorState(state, state.Config.Please.NumThreads, state.Verbosity >= 4, false, false, state.NeedTests, false, false, "") 142 output.PrintDisconnectionMessage(success, remoteClosed, remoteDisconnected) 143 return success 144 }