github.com/diptanu/nomad@v0.5.7-0.20170516172507-d72e86cbe3d9/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  }