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