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