github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/client/fs_endpoint.go (about) 1 package client 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "math" 9 "net/http" 10 "os" 11 "path/filepath" 12 "sort" 13 "strconv" 14 "strings" 15 "syscall" 16 "time" 17 18 metrics "github.com/armon/go-metrics" 19 "github.com/hashicorp/go-msgpack/codec" 20 "github.com/hashicorp/nomad/acl" 21 "github.com/hashicorp/nomad/client/allocdir" 22 sframer "github.com/hashicorp/nomad/client/lib/streamframer" 23 cstructs "github.com/hashicorp/nomad/client/structs" 24 "github.com/hashicorp/nomad/helper" 25 "github.com/hashicorp/nomad/nomad/structs" 26 "github.com/hpcloud/tail/watch" 27 ) 28 29 var ( 30 allocIDNotPresentErr = fmt.Errorf("must provide a valid alloc id") 31 pathNotPresentErr = fmt.Errorf("must provide a file path") 32 taskNotPresentErr = fmt.Errorf("must provide task name") 33 logTypeNotPresentErr = fmt.Errorf("must provide log type (stdout/stderr)") 34 invalidOrigin = fmt.Errorf("origin must be start or end") 35 ) 36 37 const ( 38 // streamFramesBuffer is the number of stream frames that will be buffered 39 // before back pressure is applied on the stream framer. 40 streamFramesBuffer = 32 41 42 // streamFrameSize is the maximum number of bytes to send in a single frame 43 streamFrameSize = 64 * 1024 44 45 // streamHeartbeatRate is the rate at which a heartbeat will occur to detect 46 // a closed connection without sending any additional data 47 streamHeartbeatRate = 1 * time.Second 48 49 // streamBatchWindow is the window in which file content is batched before 50 // being flushed if the frame size has not been hit. 51 streamBatchWindow = 200 * time.Millisecond 52 53 // nextLogCheckRate is the rate at which we check for a log entry greater 54 // than what we are watching for. This is to handle the case in which logs 55 // rotate faster than we can detect and we have to rely on a normal 56 // directory listing. 57 nextLogCheckRate = 100 * time.Millisecond 58 59 // deleteEvent and truncateEvent are the file events that can be sent in a 60 // StreamFrame 61 deleteEvent = "file deleted" 62 truncateEvent = "file truncated" 63 64 // OriginStart and OriginEnd are the available parameters for the origin 65 // argument when streaming a file. They respectively offset from the start 66 // and end of a file. 67 OriginStart = "start" 68 OriginEnd = "end" 69 ) 70 71 // FileSystem endpoint is used for accessing the logs and filesystem of 72 // allocations. 73 type FileSystem struct { 74 c *Client 75 } 76 77 func NewFileSystemEndpoint(c *Client) *FileSystem { 78 f := &FileSystem{c} 79 f.c.streamingRpcs.Register("FileSystem.Logs", f.logs) 80 f.c.streamingRpcs.Register("FileSystem.Stream", f.stream) 81 return f 82 } 83 84 // handleStreamResultError is a helper for sending an error with a potential 85 // error code. The transmission of the error is ignored if the error has been 86 // generated by the closing of the underlying transport. 87 func handleStreamResultError(err error, code *int64, encoder *codec.Encoder) { 88 // Nothing to do as the conn is closed 89 if err == io.EOF || strings.Contains(err.Error(), "closed") { 90 return 91 } 92 93 encoder.Encode(&cstructs.StreamErrWrapper{ 94 Error: cstructs.NewRpcError(err, code), 95 }) 96 } 97 98 // List is used to list the contents of an allocation's directory. 99 func (f *FileSystem) List(args *cstructs.FsListRequest, reply *cstructs.FsListResponse) error { 100 defer metrics.MeasureSince([]string{"client", "file_system", "list"}, time.Now()) 101 102 alloc, err := f.c.GetAlloc(args.AllocID) 103 if err != nil { 104 return err 105 } 106 107 // Check namespace read-fs permission. 108 if aclObj, err := f.c.ResolveToken(args.QueryOptions.AuthToken); err != nil { 109 return err 110 } else if aclObj != nil && !aclObj.AllowNsOp(alloc.Namespace, acl.NamespaceCapabilityReadFS) { 111 return structs.ErrPermissionDenied 112 } 113 114 fs, err := f.c.GetAllocFS(args.AllocID) 115 if err != nil { 116 return err 117 } 118 files, err := fs.List(args.Path) 119 if err != nil { 120 return err 121 } 122 123 reply.Files = files 124 return nil 125 } 126 127 // Stat is used to stat a file in the allocation's directory. 128 func (f *FileSystem) Stat(args *cstructs.FsStatRequest, reply *cstructs.FsStatResponse) error { 129 defer metrics.MeasureSince([]string{"client", "file_system", "stat"}, time.Now()) 130 131 alloc, err := f.c.GetAlloc(args.AllocID) 132 if err != nil { 133 return err 134 } 135 136 // Check namespace read-fs permission. 137 if aclObj, err := f.c.ResolveToken(args.QueryOptions.AuthToken); err != nil { 138 return err 139 } else if aclObj != nil && !aclObj.AllowNsOp(alloc.Namespace, acl.NamespaceCapabilityReadFS) { 140 return structs.ErrPermissionDenied 141 } 142 143 fs, err := f.c.GetAllocFS(args.AllocID) 144 if err != nil { 145 return err 146 } 147 info, err := fs.Stat(args.Path) 148 if err != nil { 149 return err 150 } 151 152 reply.Info = info 153 return nil 154 } 155 156 // stream is is used to stream the contents of file in an allocation's 157 // directory. 158 func (f *FileSystem) stream(conn io.ReadWriteCloser) { 159 defer metrics.MeasureSince([]string{"client", "file_system", "stream"}, time.Now()) 160 defer conn.Close() 161 162 // Decode the arguments 163 var req cstructs.FsStreamRequest 164 decoder := codec.NewDecoder(conn, structs.MsgpackHandle) 165 encoder := codec.NewEncoder(conn, structs.MsgpackHandle) 166 167 if err := decoder.Decode(&req); err != nil { 168 handleStreamResultError(err, helper.Int64ToPtr(500), encoder) 169 return 170 } 171 172 if req.AllocID == "" { 173 handleStreamResultError(allocIDNotPresentErr, helper.Int64ToPtr(400), encoder) 174 return 175 } 176 alloc, err := f.c.GetAlloc(req.AllocID) 177 if err != nil { 178 handleStreamResultError(structs.NewErrUnknownAllocation(req.AllocID), helper.Int64ToPtr(404), encoder) 179 return 180 } 181 182 // Check read permissions 183 if aclObj, err := f.c.ResolveToken(req.QueryOptions.AuthToken); err != nil { 184 handleStreamResultError(err, helper.Int64ToPtr(403), encoder) 185 return 186 } else if aclObj != nil && !aclObj.AllowNsOp(alloc.Namespace, acl.NamespaceCapabilityReadFS) { 187 handleStreamResultError(structs.ErrPermissionDenied, helper.Int64ToPtr(403), encoder) 188 return 189 } 190 191 // Validate the arguments 192 if req.Path == "" { 193 handleStreamResultError(pathNotPresentErr, helper.Int64ToPtr(400), encoder) 194 return 195 } 196 switch req.Origin { 197 case "start", "end": 198 case "": 199 req.Origin = "start" 200 default: 201 handleStreamResultError(invalidOrigin, helper.Int64ToPtr(400), encoder) 202 return 203 } 204 205 fs, err := f.c.GetAllocFS(req.AllocID) 206 if err != nil { 207 code := helper.Int64ToPtr(500) 208 if structs.IsErrUnknownAllocation(err) { 209 code = helper.Int64ToPtr(404) 210 } 211 212 handleStreamResultError(err, code, encoder) 213 return 214 } 215 216 // Calculate the offset 217 fileInfo, err := fs.Stat(req.Path) 218 if err != nil { 219 handleStreamResultError(err, helper.Int64ToPtr(400), encoder) 220 return 221 } 222 if fileInfo.IsDir { 223 handleStreamResultError( 224 fmt.Errorf("file %q is a directory", req.Path), 225 helper.Int64ToPtr(400), encoder) 226 return 227 } 228 229 // If offsetting from the end subtract from the size 230 if req.Origin == "end" { 231 req.Offset = fileInfo.Size - req.Offset 232 if req.Offset < 0 { 233 req.Offset = 0 234 } 235 } 236 237 frames := make(chan *sframer.StreamFrame, streamFramesBuffer) 238 errCh := make(chan error) 239 var buf bytes.Buffer 240 frameCodec := codec.NewEncoder(&buf, structs.JsonHandle) 241 242 // Create the framer 243 framer := sframer.NewStreamFramer(frames, streamHeartbeatRate, streamBatchWindow, streamFrameSize) 244 framer.Run() 245 defer framer.Destroy() 246 247 // If we aren't following end as soon as we hit EOF 248 var eofCancelCh chan error 249 if !req.Follow { 250 eofCancelCh = make(chan error) 251 close(eofCancelCh) 252 } 253 254 ctx, cancel := context.WithCancel(context.Background()) 255 defer cancel() 256 257 // Start streaming 258 go func() { 259 if err := f.streamFile(ctx, req.Offset, req.Path, req.Limit, fs, framer, eofCancelCh); err != nil { 260 select { 261 case errCh <- err: 262 case <-ctx.Done(): 263 } 264 } 265 266 framer.Destroy() 267 }() 268 269 // Create a goroutine to detect the remote side closing 270 go func() { 271 for { 272 if _, err := conn.Read(nil); err != nil { 273 if err == io.EOF || err == io.ErrClosedPipe { 274 // One end of the pipe was explicitly closed, exit cleanly 275 cancel() 276 return 277 } 278 select { 279 case errCh <- err: 280 case <-ctx.Done(): 281 return 282 } 283 } 284 } 285 }() 286 287 var streamErr error 288 OUTER: 289 for { 290 select { 291 case streamErr = <-errCh: 292 break OUTER 293 case frame, ok := <-frames: 294 if !ok { 295 // frame may have been closed when an error 296 // occurred. Check once more for an error. 297 select { 298 case streamErr = <-errCh: 299 // There was a pending error! 300 default: 301 // No error, continue on 302 } 303 304 break OUTER 305 } 306 307 var resp cstructs.StreamErrWrapper 308 if req.PlainText { 309 resp.Payload = frame.Data 310 } else { 311 if err = frameCodec.Encode(frame); err != nil { 312 streamErr = err 313 break OUTER 314 } 315 316 resp.Payload = buf.Bytes() 317 buf.Reset() 318 } 319 320 if err := encoder.Encode(resp); err != nil { 321 streamErr = err 322 break OUTER 323 } 324 encoder.Reset(conn) 325 case <-ctx.Done(): 326 break OUTER 327 } 328 } 329 330 if streamErr != nil { 331 handleStreamResultError(streamErr, helper.Int64ToPtr(500), encoder) 332 return 333 } 334 } 335 336 // logs is is used to stream a task's logs. 337 func (f *FileSystem) logs(conn io.ReadWriteCloser) { 338 defer metrics.MeasureSince([]string{"client", "file_system", "logs"}, time.Now()) 339 defer conn.Close() 340 341 // Decode the arguments 342 var req cstructs.FsLogsRequest 343 decoder := codec.NewDecoder(conn, structs.MsgpackHandle) 344 encoder := codec.NewEncoder(conn, structs.MsgpackHandle) 345 346 if err := decoder.Decode(&req); err != nil { 347 handleStreamResultError(err, helper.Int64ToPtr(500), encoder) 348 return 349 } 350 351 if req.AllocID == "" { 352 handleStreamResultError(allocIDNotPresentErr, helper.Int64ToPtr(400), encoder) 353 return 354 } 355 alloc, err := f.c.GetAlloc(req.AllocID) 356 if err != nil { 357 handleStreamResultError(structs.NewErrUnknownAllocation(req.AllocID), helper.Int64ToPtr(404), encoder) 358 return 359 } 360 361 // Check read permissions 362 if aclObj, err := f.c.ResolveToken(req.QueryOptions.AuthToken); err != nil { 363 handleStreamResultError(err, nil, encoder) 364 return 365 } else if aclObj != nil { 366 readfs := aclObj.AllowNsOp(alloc.Namespace, acl.NamespaceCapabilityReadFS) 367 logs := aclObj.AllowNsOp(alloc.Namespace, acl.NamespaceCapabilityReadLogs) 368 if !readfs && !logs { 369 handleStreamResultError(structs.ErrPermissionDenied, nil, encoder) 370 return 371 } 372 } 373 374 // Validate the arguments 375 if req.Task == "" { 376 handleStreamResultError(taskNotPresentErr, helper.Int64ToPtr(400), encoder) 377 return 378 } 379 switch req.LogType { 380 case "stdout", "stderr": 381 default: 382 handleStreamResultError(logTypeNotPresentErr, helper.Int64ToPtr(400), encoder) 383 return 384 } 385 switch req.Origin { 386 case "start", "end": 387 case "": 388 req.Origin = "start" 389 default: 390 handleStreamResultError(invalidOrigin, helper.Int64ToPtr(400), encoder) 391 return 392 } 393 394 fs, err := f.c.GetAllocFS(req.AllocID) 395 if err != nil { 396 code := helper.Int64ToPtr(500) 397 if structs.IsErrUnknownAllocation(err) { 398 code = helper.Int64ToPtr(404) 399 } 400 401 handleStreamResultError(err, code, encoder) 402 return 403 } 404 405 allocState, err := f.c.GetAllocState(req.AllocID) 406 if err != nil { 407 code := helper.Int64ToPtr(500) 408 if structs.IsErrUnknownAllocation(err) { 409 code = helper.Int64ToPtr(404) 410 } 411 412 handleStreamResultError(err, code, encoder) 413 return 414 } 415 416 // Check that the task is there 417 taskState := allocState.TaskStates[req.Task] 418 if taskState == nil { 419 handleStreamResultError( 420 fmt.Errorf("unknown task name %q", req.Task), 421 helper.Int64ToPtr(400), 422 encoder) 423 return 424 } 425 426 if taskState.StartedAt.IsZero() { 427 handleStreamResultError( 428 fmt.Errorf("task %q not started yet. No logs available", req.Task), 429 helper.Int64ToPtr(404), 430 encoder) 431 return 432 } 433 434 ctx, cancel := context.WithCancel(context.Background()) 435 defer cancel() 436 437 frames := make(chan *sframer.StreamFrame, streamFramesBuffer) 438 errCh := make(chan error) 439 440 // Start streaming 441 go func() { 442 if err := f.logsImpl(ctx, req.Follow, req.PlainText, 443 req.Offset, req.Origin, req.Task, req.LogType, fs, frames); err != nil { 444 select { 445 case errCh <- err: 446 case <-ctx.Done(): 447 } 448 } 449 }() 450 451 // Create a goroutine to detect the remote side closing 452 go func() { 453 for { 454 if _, err := conn.Read(nil); err != nil { 455 if err == io.EOF || err == io.ErrClosedPipe { 456 // One end of the pipe was explicitly closed, exit cleanly 457 cancel() 458 return 459 } 460 select { 461 case errCh <- err: 462 case <-ctx.Done(): 463 } 464 return 465 } 466 } 467 }() 468 469 var streamErr error 470 buf := new(bytes.Buffer) 471 frameCodec := codec.NewEncoder(buf, structs.JsonHandle) 472 OUTER: 473 for { 474 select { 475 case streamErr = <-errCh: 476 break OUTER 477 case frame, ok := <-frames: 478 if !ok { 479 // framer may have been closed when an error 480 // occurred. Check once more for an error. 481 select { 482 case streamErr = <-errCh: 483 // There was a pending error! 484 default: 485 // No error, continue on 486 } 487 488 break OUTER 489 } 490 491 var resp cstructs.StreamErrWrapper 492 if req.PlainText { 493 resp.Payload = frame.Data 494 } else { 495 if err = frameCodec.Encode(frame); err != nil { 496 streamErr = err 497 break OUTER 498 } 499 frameCodec.Reset(buf) 500 501 resp.Payload = buf.Bytes() 502 buf.Reset() 503 } 504 505 if err := encoder.Encode(resp); err != nil { 506 streamErr = err 507 break OUTER 508 } 509 encoder.Reset(conn) 510 } 511 } 512 513 if streamErr != nil { 514 // If error has a Code, use it 515 var code int64 = 500 516 if codedErr, ok := streamErr.(interface{ Code() int }); ok { 517 code = int64(codedErr.Code()) 518 } 519 handleStreamResultError(streamErr, &code, encoder) 520 return 521 } 522 } 523 524 // logsImpl is used to stream the logs of a the given task. Output is sent on 525 // the passed frames channel and the method will return on EOF if follow is not 526 // true otherwise when the context is cancelled or on an error. 527 func (f *FileSystem) logsImpl(ctx context.Context, follow, plain bool, offset int64, 528 origin, task, logType string, 529 fs allocdir.AllocDirFS, frames chan<- *sframer.StreamFrame) error { 530 531 // Create the framer 532 framer := sframer.NewStreamFramer(frames, streamHeartbeatRate, streamBatchWindow, streamFrameSize) 533 framer.Run() 534 defer framer.Destroy() 535 536 // Path to the logs 537 logPath := filepath.Join(allocdir.SharedAllocName, allocdir.LogDirName) 538 539 // nextIdx is the next index to read logs from 540 var nextIdx int64 541 switch origin { 542 case "start": 543 nextIdx = 0 544 case "end": 545 nextIdx = math.MaxInt64 546 offset *= -1 547 default: 548 return invalidOrigin 549 } 550 551 for { 552 // Logic for picking next file is: 553 // 1) List log files 554 // 2) Pick log file closest to desired index 555 // 3) Open log file at correct offset 556 // 3a) No error, read contents 557 // 3b) If file doesn't exist, goto 1 as it may have been rotated out 558 entries, err := fs.List(logPath) 559 if err != nil { 560 return fmt.Errorf("failed to list entries: %v", err) 561 } 562 563 // If we are not following logs, determine the max index for the logs we are 564 // interested in so we can stop there. 565 maxIndex := int64(math.MaxInt64) 566 if !follow { 567 _, idx, _, err := findClosest(entries, maxIndex, 0, task, logType) 568 if err != nil { 569 return err 570 } 571 maxIndex = idx 572 } 573 574 logEntry, idx, openOffset, err := findClosest(entries, nextIdx, offset, task, logType) 575 if err != nil { 576 return err 577 } 578 579 var eofCancelCh chan error 580 exitAfter := false 581 if !follow && idx > maxIndex { 582 // Exceeded what was there initially so return 583 return nil 584 } else if !follow && idx == maxIndex { 585 // At the end 586 eofCancelCh = make(chan error) 587 close(eofCancelCh) 588 exitAfter = true 589 } else { 590 eofCancelCh = blockUntilNextLog(ctx, fs, logPath, task, logType, idx+1) 591 } 592 593 p := filepath.Join(logPath, logEntry.Name) 594 err = f.streamFile(ctx, openOffset, p, 0, fs, framer, eofCancelCh) 595 596 // Check if the context is cancelled 597 select { 598 case <-ctx.Done(): 599 return nil 600 default: 601 } 602 603 if err != nil { 604 // Check if there was an error where the file does not exist. That means 605 // it got rotated out from under us. 606 if os.IsNotExist(err) { 607 continue 608 } 609 610 // Check if the connection was closed 611 if err == syscall.EPIPE { 612 return nil 613 } 614 615 return fmt.Errorf("failed to stream %q: %v", p, err) 616 } 617 618 if exitAfter { 619 return nil 620 } 621 622 // defensively check to make sure StreamFramer hasn't stopped 623 // running to avoid tight loops with goroutine leaks as in 624 // #3342 625 select { 626 case <-framer.ExitCh(): 627 return nil 628 default: 629 } 630 631 // Since we successfully streamed, update the overall offset/idx. 632 offset = int64(0) 633 nextIdx = idx + 1 634 } 635 } 636 637 // streamFile is the internal method to stream the content of a file. If limit 638 // is greater than zero, the stream will end once that many bytes have been 639 // read. eofCancelCh is used to cancel the stream if triggered while at EOF. If 640 // the connection is broken an EPIPE error is returned 641 func (f *FileSystem) streamFile(ctx context.Context, offset int64, path string, limit int64, 642 fs allocdir.AllocDirFS, framer *sframer.StreamFramer, eofCancelCh chan error) error { 643 644 // Get the reader 645 file, err := fs.ReadAt(path, offset) 646 if err != nil { 647 return err 648 } 649 defer file.Close() 650 651 var fileReader io.Reader 652 if limit <= 0 { 653 fileReader = file 654 } else { 655 fileReader = io.LimitReader(file, limit) 656 } 657 658 // Create a tomb to cancel watch events 659 waitCtx, cancel := context.WithCancel(ctx) 660 defer cancel() 661 662 // Create a variable to allow setting the last event 663 var lastEvent string 664 665 // Only create the file change watcher once. But we need to do it after we 666 // read and reach EOF. 667 var changes *watch.FileChanges 668 669 // Start streaming the data 670 bufSize := int64(streamFrameSize) 671 if limit > 0 && limit < streamFrameSize { 672 bufSize = limit 673 } 674 data := make([]byte, bufSize) 675 OUTER: 676 for { 677 // Read up to the max frame size 678 n, readErr := fileReader.Read(data) 679 680 // Update the offset 681 offset += int64(n) 682 683 // Return non-EOF errors 684 if readErr != nil && readErr != io.EOF { 685 return readErr 686 } 687 688 // Send the frame 689 if n != 0 || lastEvent != "" { 690 if err := framer.Send(path, lastEvent, data[:n], offset); err != nil { 691 return parseFramerErr(err) 692 } 693 } 694 695 // Clear the last event 696 if lastEvent != "" { 697 lastEvent = "" 698 } 699 700 // Just keep reading since we aren't at the end of the file so we can 701 // avoid setting up a file event watcher. 702 if readErr == nil { 703 continue 704 } 705 706 // If EOF is hit, wait for a change to the file 707 if changes == nil { 708 changes, err = fs.ChangeEvents(waitCtx, path, offset) 709 if err != nil { 710 return err 711 } 712 } 713 714 for { 715 select { 716 case <-changes.Modified: 717 continue OUTER 718 case <-changes.Deleted: 719 return parseFramerErr(framer.Send(path, deleteEvent, nil, offset)) 720 case <-changes.Truncated: 721 // Close the current reader 722 if err := file.Close(); err != nil { 723 return err 724 } 725 726 // Get a new reader at offset zero 727 offset = 0 728 var err error 729 file, err = fs.ReadAt(path, offset) 730 if err != nil { 731 return err 732 } 733 defer file.Close() 734 735 if limit <= 0 { 736 fileReader = file 737 } else { 738 // Get the current limit 739 lr, ok := fileReader.(*io.LimitedReader) 740 if !ok { 741 return fmt.Errorf("unable to determine remaining read limit") 742 } 743 744 fileReader = io.LimitReader(file, lr.N) 745 } 746 747 // Store the last event 748 lastEvent = truncateEvent 749 continue OUTER 750 case <-framer.ExitCh(): 751 return nil 752 case <-ctx.Done(): 753 return nil 754 case err, ok := <-eofCancelCh: 755 if !ok { 756 return nil 757 } 758 759 return err 760 } 761 } 762 } 763 } 764 765 // blockUntilNextLog returns a channel that will have data sent when the next 766 // log index or anything greater is created. 767 func blockUntilNextLog(ctx context.Context, fs allocdir.AllocDirFS, logPath, task, logType string, nextIndex int64) chan error { 768 nextPath := filepath.Join(logPath, fmt.Sprintf("%s.%s.%d", task, logType, nextIndex)) 769 next := make(chan error, 1) 770 771 go func() { 772 eofCancelCh, err := fs.BlockUntilExists(ctx, nextPath) 773 if err != nil { 774 next <- err 775 close(next) 776 return 777 } 778 779 ticker := time.NewTicker(nextLogCheckRate) 780 defer ticker.Stop() 781 scanCh := ticker.C 782 for { 783 select { 784 case <-ctx.Done(): 785 next <- nil 786 close(next) 787 return 788 case err := <-eofCancelCh: 789 next <- err 790 close(next) 791 return 792 case <-scanCh: 793 entries, err := fs.List(logPath) 794 if err != nil { 795 next <- fmt.Errorf("failed to list entries: %v", err) 796 close(next) 797 return 798 } 799 800 indexes, err := logIndexes(entries, task, logType) 801 if err != nil { 802 next <- err 803 close(next) 804 return 805 } 806 807 // Scan and see if there are any entries larger than what we are 808 // waiting for. 809 for _, entry := range indexes { 810 if entry.idx >= nextIndex { 811 next <- nil 812 close(next) 813 return 814 } 815 } 816 } 817 } 818 }() 819 820 return next 821 } 822 823 // indexTuple and indexTupleArray are used to find the correct log entry to 824 // start streaming logs from 825 type indexTuple struct { 826 idx int64 827 entry *cstructs.AllocFileInfo 828 } 829 830 type indexTupleArray []indexTuple 831 832 func (a indexTupleArray) Len() int { return len(a) } 833 func (a indexTupleArray) Less(i, j int) bool { return a[i].idx < a[j].idx } 834 func (a indexTupleArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 835 836 // logIndexes takes a set of entries and returns a indexTupleArray of 837 // the desired log file entries. If the indexes could not be determined, an 838 // error is returned. 839 func logIndexes(entries []*cstructs.AllocFileInfo, task, logType string) (indexTupleArray, error) { 840 var indexes []indexTuple 841 prefix := fmt.Sprintf("%s.%s.", task, logType) 842 for _, entry := range entries { 843 if entry.IsDir { 844 continue 845 } 846 847 // If nothing was trimmed, then it is not a match 848 idxStr := strings.TrimPrefix(entry.Name, prefix) 849 if idxStr == entry.Name { 850 continue 851 } 852 853 // Convert to an int 854 idx, err := strconv.Atoi(idxStr) 855 if err != nil { 856 return nil, fmt.Errorf("failed to convert %q to a log index: %v", idxStr, err) 857 } 858 859 indexes = append(indexes, indexTuple{idx: int64(idx), entry: entry}) 860 } 861 862 return indexTupleArray(indexes), nil 863 } 864 865 // notFoundErr is returned when a log is requested but cannot be found. 866 // Implements agent.HTTPCodedError but does not reference it to avoid circular 867 // imports. 868 type notFoundErr struct { 869 taskName string 870 logType string 871 } 872 873 func (e notFoundErr) Error() string { 874 return fmt.Sprintf("log entry for task %q and log type %q not found", e.taskName, e.logType) 875 } 876 877 // Code returns a 404 to avoid returning a 500 878 func (e notFoundErr) Code() int { 879 return http.StatusNotFound 880 } 881 882 // findClosest takes a list of entries, the desired log index and desired log 883 // offset (which can be negative, treated as offset from end), task name and log 884 // type and returns the log entry, the log index, the offset to read from and a 885 // potential error. 886 func findClosest(entries []*cstructs.AllocFileInfo, desiredIdx, desiredOffset int64, 887 task, logType string) (*cstructs.AllocFileInfo, int64, int64, error) { 888 889 // Build the matching indexes 890 indexes, err := logIndexes(entries, task, logType) 891 if err != nil { 892 return nil, 0, 0, err 893 } 894 if len(indexes) == 0 { 895 return nil, 0, 0, notFoundErr{taskName: task, logType: logType} 896 } 897 898 // Binary search the indexes to get the desiredIdx 899 sort.Sort(indexes) 900 i := sort.Search(len(indexes), func(i int) bool { return indexes[i].idx >= desiredIdx }) 901 l := len(indexes) 902 if i == l { 903 // Use the last index if the number is bigger than all of them. 904 i = l - 1 905 } 906 907 // Get to the correct offset 908 offset := desiredOffset 909 idx := int64(i) 910 for { 911 s := indexes[idx].entry.Size 912 913 // Base case 914 if offset == 0 { 915 break 916 } else if offset < 0 { 917 // Going backwards 918 if newOffset := s + offset; newOffset >= 0 { 919 // Current file works 920 offset = newOffset 921 break 922 } else if idx == 0 { 923 // Already at the end 924 offset = 0 925 break 926 } else { 927 // Try the file before 928 offset = newOffset 929 idx -= 1 930 continue 931 } 932 } else { 933 // Going forward 934 if offset <= s { 935 // Current file works 936 break 937 } else if idx == int64(l-1) { 938 // Already at the end 939 offset = s 940 break 941 } else { 942 // Try the next file 943 offset = offset - s 944 idx += 1 945 continue 946 } 947 948 } 949 } 950 951 return indexes[idx].entry, indexes[idx].idx, offset, nil 952 } 953 954 // parseFramerErr takes an error and returns an error. The error will 955 // potentially change if it was caused by the connection being closed. 956 func parseFramerErr(err error) error { 957 if err == nil { 958 return nil 959 } 960 961 errMsg := err.Error() 962 963 if strings.Contains(errMsg, io.ErrClosedPipe.Error()) { 964 // The pipe check is for tests 965 return syscall.EPIPE 966 } 967 968 // The connection was closed by our peer 969 if strings.Contains(errMsg, syscall.EPIPE.Error()) || strings.Contains(errMsg, syscall.ECONNRESET.Error()) { 970 return syscall.EPIPE 971 } 972 973 // Windows version of ECONNRESET 974 //XXX(schmichael) I could find no existing error or constant to 975 // compare this against. 976 if strings.Contains(errMsg, "forcibly closed") { 977 return syscall.EPIPE 978 } 979 980 return err 981 }