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