github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/platform/runtime/setup/buildlog/buildlog.go (about) 1 package buildlog 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "strings" 8 "sync" 9 "time" 10 11 "github.com/ActiveState/cli/pkg/buildplan" 12 "github.com/ActiveState/cli/pkg/platform/runtime/setup/events" 13 "github.com/go-openapi/strfmt" 14 "github.com/gorilla/websocket" 15 16 "github.com/ActiveState/cli/internal/constants" 17 "github.com/ActiveState/cli/internal/errs" 18 "github.com/ActiveState/cli/internal/locale" 19 "github.com/ActiveState/cli/internal/logging" 20 "github.com/ActiveState/cli/pkg/platform/api/buildlogstream" 21 ) 22 23 // verboseLogging is true if the user provided an environment variable for it 24 var verboseLogging = os.Getenv(constants.LogBuildVerboseEnvVarName) == "true" 25 26 type recipeRequest struct { 27 RecipeID string `json:"recipeID"` 28 } 29 30 type artifactRequest struct { 31 ArtifactID string `json:"artifactID"` 32 } 33 34 // BuildLogConnector describes how to interact with a build log connection 35 type BuildLogConnector interface { 36 ReadJSON(interface{}) error 37 WriteJSON(interface{}) error 38 } 39 40 type Events interface { 41 BuildStarting(total int) 42 BuildFinished() 43 ArtifactBuildStarting(artifactID strfmt.UUID) 44 ArtifactBuildCached(artifactID strfmt.UUID, logURI string) 45 ArtifactBuildCompleted(artifactID strfmt.UUID, logURI string) 46 ArtifactBuildFailed(artifactID strfmt.UUID, logURI string, errorMessage string) 47 ArtifactBuildProgress(artifact strfmt.UUID, timestamp string, message string, facility, pipeName, source string) 48 Heartbeat(time.Time) 49 } 50 51 // BuildError designates a build log build error. 52 type BuildError struct { 53 *locale.LocalizedError 54 } 55 56 type ArtifactBuildError struct { 57 *errs.WrapperError 58 Artifact *buildplan.Artifact 59 Message *ArtifactFailedMessage 60 } 61 62 // EventHandlerError designates an error in the event handler for reporting progress. 63 type EventHandlerError struct { 64 *errs.WrapperError 65 } 66 67 // BuildLog is an implementation of a build log 68 type BuildLog struct { 69 ch chan *buildplan.Artifact 70 errCh chan error 71 conn *websocket.Conn 72 } 73 74 // New creates a new BuildLog instance that allows us to wait for incoming build log information 75 // artifactMap comprises all artifacts (from the runtime closure) that are in the recipe, alreadyBuilt is set of artifact IDs that have already been built in the past 76 func New(ctx context.Context, artifactMap buildplan.ArtifactIDMap, eventHandler events.Handler, recipeID strfmt.UUID, logFilePath string) (*BuildLog, error) { 77 conn, err := buildlogstream.Connect(ctx) 78 if err != nil { 79 return nil, errs.Wrap(err, "Could not connect to build-log streamer build updates") 80 } 81 bl, err := NewWithCustomConnections(artifactMap, conn, eventHandler, recipeID, logFilePath) 82 if err != nil { 83 conn.Close() 84 85 return nil, err 86 } 87 bl.conn = conn 88 return bl, nil 89 } 90 91 // NewWithCustomConnections creates a new BuildLog instance with all physical connections managed by the caller 92 func NewWithCustomConnections(artifactMap buildplan.ArtifactIDMap, 93 conn BuildLogConnector, eventHandler events.Handler, 94 recipeID strfmt.UUID, logFilePath string) (*BuildLog, error) { 95 96 ch := make(chan *buildplan.Artifact) 97 errCh := make(chan error) 98 99 if err := handleEvent(eventHandler, events.BuildStarted{logFilePath}); err != nil { 100 return nil, errs.Wrap(err, "Could not handle BuildStarted event") 101 } 102 103 go func() { 104 defer close(ch) 105 defer close(errCh) 106 107 // It is currently possible for the buildlogstreamer to send the same event twice. 108 // This happens, when nomad looses track of a build job and the HC re-schedules it. 109 // The following code is used to identify duplicate events. 110 uniqueEvents := make(map[string]struct{}) 111 observed := func(id ...string) bool { 112 idStr := strings.Join(id, ".") 113 _, ok := uniqueEvents[idStr] 114 if !ok { 115 uniqueEvents[idStr] = struct{}{} 116 } 117 return ok 118 } 119 120 artifactsDone := make(map[strfmt.UUID]struct{}) 121 122 // Set up log file 123 logMutex := &sync.Mutex{} 124 logfile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY, 0644) 125 if err != nil { 126 errCh <- errs.Wrap(err, "Could not open build log file") 127 return 128 } 129 defer logfile.Close() 130 writeLogFile := func(artifactID strfmt.UUID, msg string) error { 131 logMutex.Lock() 132 defer logMutex.Unlock() 133 name := artifactID.String() 134 if a, ok := artifactMap[artifactID]; ok { 135 name = a.Name() + " (" + artifactID.String() + ")" 136 } 137 if name != "" { 138 name = name + ": " 139 } 140 if _, err := logfile.WriteString(name + msg + "\n"); err != nil { 141 return errs.Wrap(err, "Could not write string to build log file") 142 } 143 if err := logfile.Sync(); err != nil { 144 return errs.Wrap(err, "Could not sync build log file") 145 } 146 return nil 147 } 148 149 stillWaiting := func() []string { 150 result := []string{} 151 for id := range artifactMap { 152 if _, done := artifactsDone[id]; !done { 153 name := id.String() 154 if a, ok := artifactMap[id]; ok { 155 name = a.Name() + " (" + id.String() + ")" 156 } 157 result = append(result, name) 158 } 159 } 160 return result 161 } 162 163 buildSuccess := func() { 164 if err := writeLogFile("", "Build Succeeded"); err != nil { 165 errCh <- errs.Wrap(err, "Could not write to build log file") 166 } 167 if err := handleEvent(eventHandler, events.BuildSuccess{}); err != nil { 168 errCh <- errs.Wrap(err, "Could not handle BuildSuccess event") 169 } 170 } 171 172 var artifactErr error 173 for { 174 var msg Message 175 err := conn.ReadJSON(&msg) 176 if err != nil { 177 // This should bubble up and logging it is just an extra measure to help with debugging 178 logging.Debug("Encountered error: %s", errs.JoinMessage(err)) 179 errCh <- err 180 return 181 } 182 if verboseLogging { 183 logging.Debug("Received response: %s", msg.MessageTypeValue()) 184 } 185 186 switch msg.MessageType() { 187 case BuildStarted: 188 if observed(msg.MessageTypeValue()) { 189 break 190 } 191 if err := writeLogFile("", "Build Started"); err != nil { 192 errCh <- errs.Wrap(err, "Could not write to build log file") 193 } 194 case BuildFailed: 195 if observed(msg.MessageTypeValue()) { 196 break 197 } 198 m := msg.messager.(BuildFailedMessage) 199 if err := writeLogFile("", m.ErrorMessage); err != nil { 200 errCh <- errs.Wrap(err, "Could not write to build log file") 201 } 202 if err := handleEvent(eventHandler, events.BuildFailure{m.ErrorMessage}); err != nil { 203 errCh <- errs.Wrap(err, "Could not handle BuildFailure event") 204 } 205 errCh <- &BuildError{locale.WrapError(artifactErr, "err_logstream_build_failed", "Build failed with error message: {{.V0}}.", m.ErrorMessage)} 206 return 207 case BuildSucceeded: 208 if observed(msg.MessageTypeValue()) { 209 break 210 } 211 buildSuccess() 212 return 213 case ArtifactStarted: 214 m := msg.messager.(ArtifactMessage) 215 // NOTE: fix to ignore current noop "final pkg artifact" 216 if m.ArtifactID == recipeID { 217 break 218 } 219 220 _, ok := artifactMap[m.ArtifactID] 221 if !ok { 222 logging.Debug("Ignoring ArtifactStarted %s as we are not monitoring this artifact", m.ArtifactID) 223 break 224 } 225 226 if observed(msg.MessageTypeValue(), m.ArtifactID.String()) { 227 break 228 } 229 230 if err := writeLogFile(m.ArtifactID, "Artifact Build Started"); err != nil { 231 errCh <- errs.Wrap(err, "Could not write to build log file") 232 } 233 234 if err := handleEvent(eventHandler, events.ArtifactBuildStarted{m.ArtifactID, m.CacheHit}); err != nil { 235 errCh <- errs.Wrap(err, "Could not handle ArtifactBuildStarted event") 236 } 237 238 // if verbose build logging is requested: Also subscribe to log messages for this artifacts 239 // you don't want to do this by default as the log size can be quite large 240 if verboseLogging { 241 logging.Debug("requesting updates for artifact %s", m.ArtifactID.String()) 242 request := artifactRequest{ArtifactID: m.ArtifactID.String()} 243 if err := conn.WriteJSON(request); err != nil { 244 errCh <- errs.Wrap(err, "Could not start artifact log request") 245 return 246 } 247 } 248 case ArtifactSucceeded: 249 m := msg.messager.(ArtifactSucceededMessage) 250 251 // NOTE: fix to ignore current noop "final pkg artifact" 252 if m.ArtifactID == recipeID { 253 break 254 } 255 256 ad, ok := artifactMap[m.ArtifactID] 257 if !ok { 258 logging.Debug("Ignoring ArtifactSucceeded %s as we are not monitoring this artifact", m.ArtifactID) 259 break 260 } 261 262 if observed(msg.MessageTypeValue(), m.ArtifactID.String()) { 263 break 264 } 265 266 artifactsDone[m.ArtifactID] = struct{}{} 267 268 if err := writeLogFile(m.ArtifactID, fmt.Sprintf(strings.TrimSpace(` 269 Artifact Build Succeeded. 270 Payload URI: %s 271 Log URI: %s 272 Used cache: %v 273 `), m.ArtifactURI, m.LogURI, m.CacheHit)); err != nil { 274 errCh <- errs.Wrap(err, "Could not write to build log file") 275 } 276 277 if m.ArtifactURI == "" { 278 errCh <- errs.Wrap(err, "Received artifact succeeded event without artifact URL: %+v", m) 279 return 280 } 281 282 ad.SetDownload(m.ArtifactURI, m.ArtifactChecksum) 283 284 ch <- ad 285 286 if err := handleEvent(eventHandler, events.ArtifactBuildSuccess{m.ArtifactID, m.LogURI}); err != nil { 287 errCh <- errs.Wrap(err, "Could not handle ArtifactBuildSuccess event") 288 return 289 } 290 291 // Because we still use the recipe ID for buildlogstreamer we will end up waiting for artifacts that 292 // aren't actually required for our runtime. To address this we effectively send the success event 293 // and stop monitoring the buildlogstreamer when we've received events for all our artifacts. 294 // This can be dropped once buildlostreamer speaks buildplans. 295 if len(stillWaiting()) == 0 { 296 buildSuccess() 297 return 298 } 299 case ArtifactFailed: 300 m := msg.messager.(ArtifactFailedMessage) 301 302 ad, ok := artifactMap[m.ArtifactID] 303 if !ok { 304 logging.Debug("Ignoring ArtifactFailed %s as we are not monitoring this artifact", m.ArtifactID) 305 break 306 } 307 308 if observed(msg.MessageTypeValue(), m.ArtifactID.String()) { 309 break 310 } 311 312 artifactsDone[m.ArtifactID] = struct{}{} 313 314 if err := writeLogFile(m.ArtifactID, fmt.Sprintf(strings.TrimSpace(` 315 Artifact Build Failed. 316 Error Message: %s 317 Log URI: %s 318 `), m.ErrorMessage, m.LogURI)); err != nil { 319 errCh <- errs.Wrap(err, "Could not write to build log file") 320 } 321 322 artifactErr = locale.WrapError(artifactErr, "err_artifact_failed", "Failed to build \"{{.V0}}\", error reported: {{.V1}}.", ad.Name(), m.ErrorMessage) 323 324 if err := handleEvent(eventHandler, events.ArtifactBuildFailure{m.ArtifactID, m.LogURI, m.ErrorMessage}); err != nil { 325 errCh <- errs.Wrap(err, "Could not handle ArtifactBuildFailure event") 326 return 327 } 328 329 errCh <- &ArtifactBuildError{ 330 errs.New("artifact build failed"), 331 ad, 332 &m, 333 } 334 335 case ArtifactProgress: 336 m := msg.messager.(ArtifactProgressMessage) 337 338 _, ok := artifactMap[m.ArtifactID] 339 if !ok { 340 break 341 } 342 343 if _, ok := artifactsDone[m.ArtifactID]; ok { 344 // ignore progress reports for artifacts that have finished 345 break 346 } 347 348 if err := writeLogFile(m.ArtifactID, "Log: "+m.Body.Message); err != nil { 349 errCh <- errs.Wrap(err, "Could not write to log file") 350 return 351 } 352 353 if err := handleEvent(eventHandler, events.ArtifactBuildProgress{ 354 m.ArtifactID, 355 m.Timestamp, 356 m.Body.Facility, 357 m.PipeName, 358 m.Body.Message, 359 m.Source, 360 }); err != nil { 361 errCh <- errs.Wrap(err, "Could not handle ArtifactBuildFailure event") 362 return 363 } 364 case Heartbeat: 365 waiting := stillWaiting() 366 msg := fmt.Sprintf("Heartbeat (still waiting for %d: %s)", len(waiting), strings.Join(waiting, ", ")) 367 if err := writeLogFile("", msg); err != nil { 368 errCh <- errs.Wrap(err, "Could not write to log file") 369 return 370 } 371 372 } 373 } 374 }() 375 376 logging.Debug("sending websocket request for %s", recipeID.String()) 377 request := recipeRequest{RecipeID: recipeID.String()} 378 if err := conn.WriteJSON(request); err != nil { 379 return nil, errs.Wrap(err, "Could not write websocket request") 380 } 381 382 return &BuildLog{ 383 ch: ch, 384 errCh: errCh, 385 }, nil 386 } 387 388 // Wait waits for the build log to close because the build is done and all downloadable artifacts are here 389 func (bl *BuildLog) Wait() error { 390 var rerr error 391 var errors []error 392 for err := range bl.errCh { 393 if rerr == nil { 394 rerr = errs.New("failed build") 395 } 396 rerr = errs.Pack(rerr, err) 397 } 398 if len(errors) > 0 { 399 return errors[0] 400 } 401 return nil 402 } 403 404 func (bl *BuildLog) Close() error { 405 if bl.conn != nil { 406 if err := bl.conn.Close(); err != nil { 407 return errs.Wrap(err, "Failed to close websocket connection") 408 } 409 } 410 return nil 411 } 412 413 // BuiltArtifactsChannel returns the channel to listen for downloadable artifacts on 414 func (bl *BuildLog) BuiltArtifactsChannel() <-chan *buildplan.Artifact { 415 return bl.ch 416 } 417 418 func handleEvent(handler events.Handler, ev events.Eventer) error { 419 err := handler.Handle(ev) 420 if err != nil { 421 return &EventHandlerError{errs.Wrap(err, "Error handling event: %v", errs.JoinMessage(err))} 422 } 423 return nil 424 }