github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/client/fs_endpoint.go (about)

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