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